如何在 Ruby on Rails 应用程序中优化 Unicorn Workers
介绍 Unicorn
如果您是 Rails 开发人员,您可能听说过Unicorn,这是一个可以同时处理多个请求的 HTTP 服务器。
Unicorn 使用分叉进程来实现并发。由于分叉进程本质上是彼此的副本,这意味着 Rails 应用程序不需要是线程安全的。
这很好,因为很难确保我们自己的代码是线程安全的。如果我们不能确保我们的代码是线程安全的,那么并发 Web 服务器(如Puma)甚至利用并发和并行的替代 Ruby 实现(如JRuby和Rubinius)都将无法实现。
因此,即使我们的 Rails 应用不是线程安全的,Unicorn 也能为其提供并发性。然而,这是有代价的。在 Unicorn 上运行的 Rails 应用往往会消耗更多的内存。如果不注意应用的内存消耗,你很可能会发现自己的云服务器负担过重。
在本文中,我们将探讨几种利用 Unicorn 并发性的方法,同时控制内存消耗。
使用 Ruby 2.0!
如果你正在使用 Ruby 1.9,你应该认真考虑切换到 Ruby 2.0。要理解原因,我们需要了解一些有关分叉的知识。
分叉和写时复制 (CoW)
当子进程被分叉时,它与父进程是完全相同的副本。但是,不需要复制实际的物理内存。由于它们是精确的副本,因此子进程和父进程都可以共享相同的物理内存。只有当进行写入时,我们才会将子进程复制到物理内存中。
那么这与 Ruby 1.9/2.0 和 Unicorn 有何关系?
回想一下,Unicorn 使用分叉。理论上,操作系统可以利用 CoW。不幸的是,Ruby 1.9 无法实现这一点。更准确地说,Ruby 1.9 的垃圾收集实现无法实现这一点。一个极其简化的版本是这样的 — 当 Ruby 1.9 的垃圾收集器启动时,会进行一次写入,从而使 CoW 变得毫无用处。
无需过多赘述,可以说 Ruby 2.0 的垃圾收集器修复了这个问题,现在我们可以利用 CoW 了。
调整Unicorn的配置
我们可以调整一些设置来config/unicorn.rb
最大限度地发挥 Unicorn 的性能。
worker_processes
这将设置要启动的工作进程数。了解一个进程占用多少内存很重要。这样您就可以安全地预算工作进程的数量,以免耗尽 VPS 的 RAM。
timeout
应将其设置为较小的数字:通常 15 到 30 秒是合理的数字。此设置设置工作器超时之前的时间量。要设置相对较小的数字的原因是为了防止长时间运行的请求阻碍其他请求的处理。
preload_app
应将其设置为true
。将其设置为 可true
减少启动 Unicorn 工作进程的启动时间。这使用 CoW 在分叉其他工作进程之前预加载应用程序。但是,有一个很大的问题。我们必须特别注意正确关闭和重新打开任何套接字(例如数据库连接)。我们使用before_fork
和 来做到这一点after_fork
。
以下是一个例子:
before_fork do |server, worker|
# Disconnect since the database connection will not carry over
if defined? ActiveRecord::Base
ActiveRecord::Base.connection.disconnect!
end
if defined?(Resque)
Resque.redis.quit
Rails.logger.info('Disconnected from Redis')
end
end
after_fork do |server, worker|
# Start up the database connection again in the worker
if defined?(ActiveRecord::Base)
ActiveRecord::Base.establish_connection
end
if defined?(Resque)
Resque.redis = ENV['REDIS_URI']
Rails.logger.info('Connected to Redis')
end
end
在此示例中,我们确保在分叉工作进程时关闭并重新打开连接。除了数据库连接之外,我们还需要确保其他需要套接字的连接得到类似处理。以上包括Resque的配置。
控制你的 Unicorn Workers 内存消耗
显然,事情并不都是美好的(双关语!)。如果您的 Rails 应用程序正在泄漏内存 - Unicorn 会使情况变得更糟。
由于这些分叉进程是 Rails 应用程序的副本,因此每个分叉进程都会消耗内存。因此,虽然拥有更多工作进程意味着我们的应用程序可以处理更多传入请求,但我们受到系统上物理 RAM 数量的限制。
Rails 应用程序很容易出现内存泄漏。即使我们设法堵住所有内存泄漏,仍然需要应对不太理想的垃圾收集器(我指的是 MRI 实现)。
上图显示了运行 Unicorn 的 Rails 应用程序存在内存泄漏。
随着时间的推移,内存消耗将持续增长。使用多个 Unicorn 工作器只会加速内存消耗的速度,直到没有更多的 RAM 可用。然后应用程序将陷入停滞 — 导致大量用户和客户不满意。
值得注意的是,这不是Unicorn的错。但这是你迟早会遇到的问题。
独角兽工人杀手登场
我遇到的最简单的解决方案之一就是unicorn-worker-killer gem。
来自自述文件:
unicorn-worker-killer
gem 提供基于 Unicorn Workers 的自动重启功能
- 最大请求数,以及
- 进程内存大小(RSS),而不会影响任何请求。
这将避免应用程序节点出现意外的内存耗尽,从而大大提高网站的稳定性。
请注意,我假设您已经设置并运行 Unicorn。
步骤 1:
添加unicorn-worker-killer
到你的 Gemfile。将其放在gem下方unicorn
。
group :production do
gem 'unicorn'
gem 'unicorn-worker-killer'
end
第 2 步:
跑步bundle install
。
步骤3:
接下来是有趣的部分。找到并打开您的config.ru
文件。
# --- Start of unicorn worker killer code ---
if ENV['RAILS_ENV'] == 'production'
require 'unicorn/worker_killer'
max_request_min = 500
max_request_max = 600
# Max requests per worker
use Unicorn::WorkerKiller::MaxRequests, max_request_min, max_request_max
oom_min = (240) * (1024**2)
oom_max = (260) * (1024**2)
# Max memory size (RSS) per worker
use Unicorn::WorkerKiller::Oom, oom_min, oom_max
end
# --- End of unicorn worker killer code ---
require ::File.expand_path('../config/environment', __FILE__)
run YourApp::Application
首先,我们检查是否处于production
环境中。如果是,我们将继续执行后面的代码。
unicorn-worker-killer
在两个条件下杀死工人:最大请求数和最大内存。
最大请求数
在此示例中,如果某个 worker 处理了 500 到 600 个请求,则该 worker 会被终止。请注意,这是一个范围。这最大限度地减少了同时终止多个 worker 的情况。
最大内存
这里,如果一个 worker 消耗了 240 到 260 MB 的内存,它就会被杀死。这个范围的原因与上面相同。
每个应用都有独特的内存需求。您应该粗略估计一下应用程序在正常运行期间的内存消耗。这样,您就可以更好地估计工作器的最小和最大内存消耗。
正确配置所有配置后,在部署应用程序时,您会注意到内存不稳定行为大大减少:
注意图中的扭结。这就是宝石在发挥作用!
结论
Unicorn 为您的 Rails 应用程序提供了一种轻松实现并发的方法,无论它是否是线程安全的。但是,这会增加 RAM 消耗。平衡 RAM 消耗对于应用程序的稳定性和性能绝对至关重要。
我们已经看到了 3 种调整 Unicorn Worker 以获得最佳性能的方法:
-
使用 Ruby 2.0 为我们提供了一个改进的垃圾收集器,使我们能够利用写时复制语义。
-
调整中的各种配置选项
config/unicorn.rb
。 -
当工人过于臃肿时,通过杀死并重新启动工人来
unicorn-worker-killer
优雅地解决问题。