介绍独角兽
如果您是 Rails 开发人员,您可能听说过 Unicorn,这是一种可以同时处理多个请求的 HTTP 服务器。
Unicorn 使用分叉的进程来实现并发。 由于分叉的进程本质上是彼此的副本,这意味着 Rails 应用程序不需要是线程安全的。
这很好,因为很难确保 our 自己的代码是线程安全的。 如果我们不能确保我们的代码是线程安全的,那么并发 Web 服务器,例如 Puma,甚至是利用并发和并行性的替代 Ruby 实现,例如 JRuby 和 Rubinius[X222X ] 是不可能的。
因此,Unicorn 为我们的 Rails 应用程序提供了并发性,即使它们不是线程安全的。 然而,这是有代价的。 在 Unicorn 上运行的 Rails 应用程序往往会消耗更多的内存。 如果不注意应用程序的内存消耗,您很可能会发现自己的云服务器负担过重。
在本文中,我们将探讨几种利用 Unicorn 并发性的方法,同时控制内存消耗。
使用 Ruby 2.0!
如果您使用的是 Ruby 1.9,那么您应该认真考虑切换到 Ruby 2.0。 要理解为什么,我们需要了解一点关于分叉的知识。
分叉和写时复制 (CoW)
当子进程被派生时,它与父进程完全相同。 但是,不需要复制实际的物理内存。 由于它们是精确的副本,' 子进程和父进程可以共享相同的物理内存。 只有当 write 被创建时——然后我们将子进程复制到物理内存中。
那么这与 Ruby 1.9/2.0 和 Unicorn 有什么关系呢?
回想一下独角兽使用分叉。 理论上,操作系统将能够利用 CoW。 不幸的是,Ruby 1.9 无法做到这一点。 更准确地说,Ruby 1.9 的垃圾回收实现并没有使这成为可能。 这是一个极其简化的版本——当 Ruby 1.9 的垃圾收集器启动时,会进行写入,从而使 CoW 变得无用。
无需过多介绍,可以说 Ruby 2.0 的垃圾收集器解决了这个问题,我们现在可以利用 CoW。
调整 Unicorn 的配置
我们可以在 config/unicorn.rb
中调整一些设置,以尽可能多地从 Unicorn 获得性能。
worker_processes
这将设置要启动的工作进程的数量。 了解 one 进程占用多少内存很重要。 这样您就可以安全地预算工作人员的数量,以免耗尽您的 VPS 的 RAM。
timeout
这应该设置为一个较小的数字:通常 15 到 30 秒是一个合理的数字。 此设置设置工作人员超时之前的时间量。 您想要设置一个相对较低的数字的原因是为了防止长时间运行的请求阻碍其他请求的处理。
preload_app
这应该设置为 true
。 将此设置为 true
可减少启动 Unicorn 工作进程的启动时间。 这使用 CoW 在分叉其他工作进程之前预加载应用程序。 但是,有一个 big 陷阱。 我们必须特别注意任何套接字(例如数据库连接)都正确关闭和重新打开。 我们使用 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的配置。
驯服独角兽员工的内存消耗
显然,这不是所有的彩虹和独角兽(双关语!)。 如果您的 Rails 应用程序正在泄漏内存 - Unicorn 会使情况变得更糟。
这些分叉进程中的每一个都会消耗内存,因为它们是 Rails 应用程序的副本。 因此,虽然拥有更多工作人员意味着我们的应用程序可以处理更多传入请求,但我们受到系统上物理 RAM 数量的限制。
Rails 应用程序很容易泄漏内存。 即使我们设法堵住了所有的内存泄漏,仍然需要处理不太理想的垃圾收集器(我指的是 MRI 实现)。
上面显示了一个运行 Unicorn 的 Rails 应用程序,但存在内存泄漏。
随着时间的推移,内存消耗将继续增长。 使用多个 Unicorn worker 只会加快内存消耗的速度,直到没有更多的 RAM 可用。 然后该应用程序将停止运行——导致成群结队的用户和客户不满意。
需要注意的是,这是 而不是 Unicorn 的错。 但是,这是您迟早会面临的问题。
进入独角兽工人杀手
我遇到的最简单的解决方案之一是 unicorn-worker-killer gem。
来自 README:
unicorn-worker-killer
gem 提供基于 Unicorn Worker 的自动重启
- 最大请求数,以及
- 进程内存大小 (RSS),不影响任何请求。
这将通过避免应用程序节点的意外内存耗尽来极大地提高站点的稳定性。
请注意,我假设您已经设置并运行了 Unicorn。
第1步:
将 unicorn-worker-killer
添加到您的 Gemfile。 将此 放在 的 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
在 2 个条件下杀死工人:最大请求和最大内存。
最大请求
在这个例子中,如果一个 worker 处理了 500 到 600 个请求之间的 ,它就会被杀死。 请注意,这是一个范围。 这最大限度地减少了同时终止超过 1 个工作人员的情况。
最大内存
在这里,如果一个工人在 240 到 260 MB 之间消耗 内存,它就会被杀死。 出于与上述相同的原因,这是一个范围。
每个应用程序都有独特的内存要求。 您应该对正常运行期间应用程序的内存消耗有一个粗略的衡量标准。 这样,您可以更好地估计工作人员的最小和最大内存消耗。
一旦您正确配置了所有内容,在部署您的应用程序时,您会注意到内存的不稳定行为要少得多:
注意图中的扭结。 这就是宝石在做它的工作!
结论
Unicorn 为您的 Rails 应用程序提供了一种轻松的方式来实现并发,无论它是否是线程安全的。 但是,它伴随着 RAM 消耗增加的成本。 平衡 RAM 消耗对于应用程序的稳定性和性能是绝对必要的。
我们已经看到了 3 种方法来调整 Unicorn 工作者以获得最佳性能:
- 使用 Ruby 2.0 为我们提供了一个大大改进的垃圾收集器,它允许我们利用写时复制语义。
- 调整
config/unicorn.rb
中的各种配置选项。 - 使用
unicorn-worker-killer
解决worker过于臃肿时优雅地杀掉重启的问题。