独角兽工人在使用Capistrano进行“零停机时间”部署后超时。

6
我正在运行一个Rails 3.2.21应用程序,并使用capistrano(nginx和unicorn)部署到Ubuntu 12.04.5盒子上。
我已经将我的应用程序设置为无停机部署(至少我是这样认为的),我的配置文件看起来更或多或少像这些
问题在于:当部署快要完成并重新启动unicorn时,当我观察我的unicorn.log时,我看到它启动了新的worker,收割了旧的worker……但是我的应用程序就会卡住2-3分钟。此时对应用程序的任何请求都会命中超时窗口(我将其设置为40秒),并返回我的应用程序的500错误页面。
以下是unicorn正在重新启动时从unicorn.log输出的第一部分内容(我有5个unicorn worker):
I, [2015-04-21T23:06:57.022492 #14347]  INFO -- : master process ready
I, [2015-04-21T23:06:57.844273 #15378]  INFO -- : worker=0 ready
I, [2015-04-21T23:06:57.944080 #15381]  INFO -- : worker=1 ready
I, [2015-04-21T23:06:58.089655 #15390]  INFO -- : worker=2 ready
I, [2015-04-21T23:06:58.230554 #14541]  INFO -- : reaped #<Process::Status: pid 15551 exit 0> worker=4
I, [2015-04-21T23:06:58.231455 #14541]  INFO -- : reaped #<Process::Status: pid 3644 exit 0> worker=0
I, [2015-04-21T23:06:58.249110 #15393]  INFO -- : worker=3 ready
I, [2015-04-21T23:06:58.650007 #15396]  INFO -- : worker=4 ready
I, [2015-04-21T23:07:01.246981 #14541]  INFO -- : reaped #<Process::Status: pid 32645 exit 0> worker=1
I, [2015-04-21T23:07:01.561786 #14541]  INFO -- : reaped #<Process::Status: pid 15534 exit 0> worker=2
I, [2015-04-21T23:07:06.657913 #14541]  INFO -- : reaped #<Process::Status: pid 16821 exit 0> worker=3
I, [2015-04-21T23:07:06.658325 #14541]  INFO -- : master complete

随后,当应用程序挂起2-3分钟时,以下是正在发生的事情:

E, [2015-04-21T23:07:38.069635 #14347] ERROR -- : worker=0 PID:15378 timeout (41s > 40s), killing
E, [2015-04-21T23:07:38.243005 #14347] ERROR -- : reaped #<Process::Status: pid 15378 SIGKILL (signal 9)> worker=0
E, [2015-04-21T23:07:39.647717 #14347] ERROR -- : worker=3 PID:15393 timeout (41s > 40s), killing
E, [2015-04-21T23:07:39.890543 #14347] ERROR -- : reaped #<Process::Status: pid 15393 SIGKILL (signal 9)> worker=3
I, [2015-04-21T23:07:40.727755 #16002]  INFO -- : worker=0 ready
I, [2015-04-21T23:07:43.212395 #16022]  INFO -- : worker=3 ready
E, [2015-04-21T23:08:24.511967 #14347] ERROR -- : worker=3 PID:16022 timeout (41s > 40s), killing
E, [2015-04-21T23:08:24.718512 #14347] ERROR -- : reaped #<Process::Status: pid 16022 SIGKILL (signal 9)> worker=3
I, [2015-04-21T23:08:28.010429 #16234]  INFO -- : worker=3 ready

最终,经过2到3分钟后,应用程序开始重新响应,但一切都变得更加缓慢。您可以在New Relic中清楚地看到这一点(水平线标记部署,浅蓝色区域表示Ruby):

New Relic graph during and after deploy

我有一个完全相同的演示服务器,但是我无法在演示环境中复制出现的问题...当然,演示环境没有负载(只有我一个人尝试进行页面请求)。

这是我的config/unicorn.rb文件:

root = "/home/deployer/apps/myawesomeapp/current"
working_directory root

pid "#{root}/tmp/pids/unicorn.pid"
stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

shared_path = "/home/deployer/apps/myawesomeapp/shared"

listen "/tmp/unicorn.myawesomeapp.sock"
worker_processes 5
timeout 40

preload_app true

before_exec do |server|
  ENV['BUNDLE_GEMFILE'] = "#{root}/Gemfile"
end

before_fork do |server, worker|
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end

  old_pid = "#{root}/tmp/pids/unicorn.pid.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH

    end
  end
end

after_fork do |server, worker|
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
end

为了完整地说明情况,在我的capistrano的deploy.rb文件中,unicorn重启任务如下:

namespace :deploy do
  task :restart, roles: :app, except: { no_release: true } do
    run "kill -s USR2 `cat #{release_path}/tmp/pids/unicorn.pid`"
  end
end

有什么想法可以解释为什么独角兽工作者在部署后立即超时?我认为零停机的重点是要保留旧的工作者,直到新工作者启动并准备好提供服务为止?

谢谢!

更新

我进行了另一个部署,并这次密切关注了production.log,以查看那里发生了什么。唯一可疑的是以下行,它们与正常请求混在一起:

Dalli/SASL authenticating as 7510de
Dalli/SASL: 7510de
Dalli/SASL authenticating as 7510de
Dalli/SASL: 7510de
Dalli/SASL authenticating as 7510de
Dalli/SASL: 7510de

更新 #2

根据以下答案的建议,我更改了before_fork块以添加sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU,以便逐步杀死工作进程。

相同的结果,部署非常缓慢,并出现了上面图表中所示的峰值。仅供参考,我的5个工作进程中,前4个发送了TTOU信号,第5个发送了QUIT信号。然而,似乎并没有产生任何区别。


1
你在部署期间预编译资产还是在首次访问应用程序时编译所有资产? - infused
@infused 看起来我是在部署期间对它们进行预编译。我在部署输出中看到一行,上面写着 executing "cd -- /home/deployer/apps/myawesomeapp/releases/20150422035053 && bundle exec rake RAILS_ENV=production RAILS_GROUPS=assets assets:precompile",这是一个需要大约30秒才能完成的命令。 - DelPiero
Op,你解决了这个问题吗?我在我的生产环境中也遇到了同样的问题。 - Ganesh Hegde
@GaneshHegde 是的,我做过。虽然是一段时间前的事情了,但我记不清是否需要进行任何重大配置更改,但我的服务器内存与此有很大关系。我转移到了另一个主机并增加了服务器内存。此外,如果我没记错的话,升级到Ruby 2 对内存管理和速度产生了很大影响。我仍在使用Rails 3.2,但现在我的部署非常快,零停机时间。您可以逐个减少Unicorn工作进程,然后进行部署测试,以查看部署是否开始变得更快。希望这可以帮助您! - DelPiero
2个回答

3

最近我在Digital Ocean上尝试设置Rails/Nginx/Unicorn时遇到了类似的问题。经过一些调整,我成功实现了零停机部署。以下是几个尝试的方法:

  1. 减少工作进程的数量。
  2. 增加服务器的内存。在512MB RAM droplet上,我遇到了超时问题。当我将其增加到1GB时,问题似乎就解决了。
  3. 使用"capistrano3-unicorn" gem。
  4. 如果preload_app为true,则使用restart(USR2)。如果为false,则使用reload(HUP)。
  5. 确保在deploy.rb中将"tmp/pids"设置为linked_dirs之一。
  6. 使用px aux | grep unicorn确保旧进程正在被删除。
  7. 使用kill [pid]手动停止任何仍在运行的unicorn进程。

以下是我的独角兽配置供参考:

working_directory '/var/www/yourapp/current'
pid '/var/www/yourapp/current/tmp/pids/unicorn.pid'
stderr_path '/var/www/yourapp/log/unicorn.log'
stdout_path '/var/www/yourapp/log/unicorn.log'
listen '/tmp/unicorn.yourapp.sock'
worker_processes 2
timeout 30
preload_app true

before_fork do |server, worker|
  old_pid = "/var/www/yourapp/current/tmp/pids/unicorn.pid.oldbin"
  if old_pid != server.pid
    begin
    sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
    Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

deploy.rb

lock '3.4.0'

set :application, 'yourapp'
set :repo_url, 'git@bitbucket.org:username/yourapp.git'
set :deploy_to, '/var/www/yourapp'
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml', 'config/application.yml')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')
set :format, :pretty
set :log_level, :info
set :rbenv_ruby, '2.1.3'

namespace :deploy do
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
    end
  end
end

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    #invoke 'unicorn:reload'
    invoke 'unicorn:restart'
  end
end

感谢提供故障排除步骤。以下是我的几个想法:
  1. & 2.) 减少工作进程的数量似乎并不必要,因为我的5个独角兽工作进程占用了服务器上可用的8GB中的3GB。 3.) 我正在使用Capistrano 2.15.4 4.) 为什么?我已经看到过这样做是一个坏主意,最好使用preload_app true。 5.) 不太理解这一点。 6.) 我可以确认它们已被删除。 7.) 不需要。
- DelPiero
1
  1. 忽略那一步。我刚测试了preload_app true,它也可以正常工作。
  2. 在我的设置中,当前目录下的tmp/pids是符号链接到共享目录中的tmp/pids,旧的unicorn pid存储在其中。 其他)你的设置看起来正确,我不确定为什么它不起作用。这是我部署结束时运行的命令:/usr/bin/env kill -s USR2 \cat /var/www/yourapp/current/tmp/pids/unicorn.pid``
- travisluong
这是用于我的独角兽重启运行的命令:kill -s USR2 cat /home/deployer/apps/myapp/releases/20150504213742/tmp/pids/unicorn.pid``,看起来它正在使用“发布路径”,而不是“当前路径”。此外,我认为它没有在/usr/bin/env中运行kill-s USR2...想知道这会有什么区别? - DelPiero

1
如果你正在使用Capistrano部署并且在运行时执行了bundle install,那么这可能是一个可执行文件的问题。
当你执行Capistrano部署时,Capistrano会为你的版本创建一个新的发布目录,并将“current”符号链接移动到新版本。如果你没有告诉正在运行的unicorn优雅地更新其可执行文件的路径,那么如果你添加以下代码,它应该可以工作:
Unicorn::HttpServer::START_CTX[0] = ::File.join(ENV['GEM_HOME'].gsub(/releases\/[^\/]+/, "current"),'bin','unicorn')
你可以在这里找到更多信息。我认为你的before_fork块看起来很好,但我也会像@travisluong's的答案中一样添加sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU行;这将随着新进程的生成逐步杀死工作进程。
顺便说一句,我不会删除preload_app true,因为它极大地提高了工作进程的生成时间。

你能详细说明一下“vendoring unicorn并在部署时运行cap bundle install”的内容吗?我如何确认我正在执行此操作?另外,我应该在哪里添加Unicorn::HttpServer::START_CTX[0] ...这行代码? - DelPiero
你应该在配置文件的顶部添加Unicorn::HttpServer这一行。 - Marcus Walser

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接