Fei

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.5Vagrant 1.7.4VirtualBox 4.3.30下测试通过。其中trusty-server-cloudimg-amd64-vagrant-disk1.box为 Ubuntu 官方镜像,保存在~/Downloads目录;sources.listUbuntu 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 installnpm 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