Vagrant下Node.js程序缓慢问题
2015-05-30
缘起
第一个项目基于 Node.js 和 Express,开发环境 Windows 系统。尽管使用npm
的--no-bin-links
参数解决了不少依赖包的安装问题,但依然有一些依赖包无法正常安装。无奈之下,转而采用 VirtualBox 虚拟机内安装 Ubuntu 的方式。但这依然要做很多配置才能实现开发文件夹自动同步,以及通过 ip 直接从 Host 端访问 Guest 端运行的服务。偶然的机会,在 V2EX 发现有人提及 Vagrant,从此踏上不归路。似乎到这里,就应该一切圆满,从此过上自在的日子?不,一切才刚刚开始。
双向同步
Vagrant 默认使用 VirtualBox 自带的文件同步方式。这种方式的好处是不用配置,坏处是文件数量不能多。随着文件数量的增加,磁盘 IO 性能逐渐下降。当共享文件夹内文件数量达到 1000+时,相对于本地运行服务,跑在虚拟机中的程序可能要延迟 5 秒到 5 分钟不等。想想前端开发每天要刷新多少次页面,这简直无法忍受!
解决办法是换用其它文件同步方式,比如 NFS 和 SMB。其中,Mac 平台下使用 NFS,Windows 平台下使用 SMB。
使用 NFS 有两个限制,一是必须在Vagrantfile
中配置private network
,二是必须在每次启动虚拟机时输入管理员密码。不过后者可以通过修改系统配置来解决,需要熟悉 Shell 命令。具体配置方式在官方文档中有详细介绍:Vagrant NFS。
同样的,使用 SMB 也有限制,那就是每次需要使用管理员方式打开命令行,而后再执行相应的Vagrant
命令。具体配置方式在官方文档中有详细介绍:Vagrant SMB。
配置文件
以下为我的Vagrantfile
配置文件内容,在OS X 10.9.5
,Vagrant 1.7.4
,VirtualBox 4.3.30
下测试通过。其中trusty-server-cloudimg-amd64-vagrant-disk1.box
为 Ubuntu 官方镜像,保存在~/Downloads
目录;sources.list
为Ubuntu 14.04
的源列表文件,和Vagrantfile
同目录。
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
url = '~/Downloads/trusty-server-cloudimg-amd64-vagrant-disk1.box'
box = 'ubuntu/trusty64'
hostname = 'trusty'
ram = '1024'
cpu = '2'
$script = <<SCRIPT
echo custom configuration start... http://biped.me
cp /vagrant/sources.list /etc/apt/
apt-get update
apt-get upgrade -y
apt-get autoremove -y
SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = box
config.vm.box_url = url
config.vm.hostname = hostname
config.vm.network "private_network", ip: "192.168.168.168"
config.vm.synced_folder ".", "/vagrant",
:nfs => true,
:mount_options => ['fsc', 'actimeo=2']
config.vm.provider "virtualbox" do |v|
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
v.customize ["modifyvm", :id, "--name", hostname]
v.customize ["modifyvm", :id, "--ostype", "Ubuntu_64"]
v.customize ["modifyvm", :id, "--memory", ram]
v.customize ["modifyvm", :id, "--cpus", cpu]
end
config.vm.provision "shell", inline: $script
end
单向同步
采用 NFS 或 SMB 方式同步后,在涉及到大量小文件双向同步时,速度有明显提升,但和本地运行程序相比,依然有一定差距。那么问题来了,能否再快一些?
双向同步意味着 Host 端改动的文件会实时同步到 Guest 端,同样的,Guest 端改动的文件也会实时同步到 Host 端。这样一去一来,性能自然下降不少。如果说,只在 Host 端开发,然后自动将 Host 端改动的文件实时同步到 Guest 端运行,而不把 Guest 端自动生成的文件同步到 Host 端,岂不是就可以避免这个性能上的损耗?答案是肯定的,使用 RSync 方式可以在很大程度上提升速度。
和使用 NFS 或 SMB 方式一样,使用 RSync 方式也有限制。如果想实现自动同步,就必须在虚拟机启动后,单开一个窗口用以运行 Vagrant 的自动同步服务,意即执行vagrant rsync-auto
命令。
这里有两个地方需要注意:
一,默认情况下 Vagrant 会删除所有 Host 端不存在而 Guest 端存在的文件,也就是说默认情况下会在下一次同步时删除 Guest 端在此之前自动生成的文件。例如,在虚拟机启动后,完成第一次从 Host 端到 Guest 端的同步,而后在 Guest 端执行bower install
命令和npm install
命令,那么生成的bower_components
目录和node_modules
目录将会在下一次从 Host 端到 Guest 端的同步时被删除。想解决这个问题需要修改rsync__args
选项,去掉--delete
参数。
二,并不是每个文件都需要从 Host 端同步到 Guest 端。例如,.git
目录下所有 Git 相关文件就不需要实时同步。解决办法是在rsync__exclude
选项中指定这些不需要从 Host 端同步到 Guest 端的文件。类似的,如果是在 Host 端执行bower install
和npm install
,那么生成的bower_components
目录和node_modules
目录也可以被排除在待同步内容之外。如此,瞬间少了大量小文件。记得在第一个基于 Node.js 的项目中,前前后后有将近一万多个小文件,其中大部分来自node_modules
目录。将这个目录排除在外后,可想而知性能会有多大提升。
配置文件
配置说明见上文,默认前端开发在 Host 端进行,后台程序如 Django 在 Guest 端运行,前端开发过程中在 Host 端生成的文件不同步到 Guest 端。
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
url = '~/Downloads/trusty-server-cloudimg-amd64-vagrant-disk1.box'
box = 'ubuntu/trusty64'
hostname = 'trusty'
ram = '1024'
cpu = '2'
$script = <<SCRIPT
echo custom configuration start... http://biped.me
cp /vagrant/sources.list /etc/apt/
apt-get update
apt-get upgrade -y
apt-get autoremove -y
SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = box
config.vm.box_url = url
config.vm.hostname = hostname
config.vm.network "private_network", ip: "192.168.168.168"
config.vm.synced_folder ".", "/vagrant", type: "rsync",
rsync__exclude: ["**/.git/", "**/bower_components/", "**/node_modules/"],
rsync__args: ["--verbose", "--archive", "-z", "--copy-links"]
config.vm.provider "virtualbox" do |v|
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
v.customize ["modifyvm", :id, "--name", hostname]
v.customize ["modifyvm", :id, "--ostype", "Ubuntu_64"]
v.customize ["modifyvm", :id, "--memory", ram]
v.customize ["modifyvm", :id, "--cpus", cpu]
end
config.vm.provision "shell", inline: $script
end