如何优化Unicorn工人在Ruby on Rails应用程序

在本文中,我们将探讨利用Unicorn的并发性的几种方法,同时控制内存消耗。

介绍Unicorn


制作人:Github

如果你是一个Rails开发者,你可能听说过的麒麟 ,可同时处理多个请求的HTTP服务器。

Unicorn使用分叉进程来实现并发。 由于分支进程本质上是彼此的副本,这意味着Rails应用程序不需要线程安全。

这是伟大的,因为它是很难保证我们自己代码是线程安全的。 如果我们不能保证我们的代码是线程安全的,那么并发的网络服务器,如Puma ,甚至Ruby实现了利用并发和并行,如JRuby的Rubinius的是出了问题。

因此,Unicorn给我们的Rails应用程序并发性,即使他们不是线程安全的。 然而,这是有代价的。 在Unicorn上运行的Rails应用程序倾向于消耗更多的内存。 如果不注意你的应用程序的内存消耗,你可能会发现自己有一个负担过重的云服务器。

在本文中,我们将探讨利用Unicorn的并发性的几种方法,同时控制内存消耗。

使用Ruby 2.0!


如果你使用Ruby 1.9,你应该认真考虑切换到Ruby 2.0。 要理解为什么,我们需要了解一点分叉。

分岔和写时复制(CoW)


当子进程被分叉时,它是与父进程完全相同的副本。 但是,不需要复制实际的物理内存。 因为它们是精确副本, 子和父进程可以共享相同的物理存储器。 只有当一个操作made--那么我们复制子进程到物理内存。

那么这与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_forkafter_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

在这个例子中,我们确保关闭连接并在worker被分叉时重新打开。 除了数据库连接之外,我们还需要确保其他需要套接字的连接被类似处理。 上述包括用于配置Resque

驯服你的Unicorn工人的内存消耗


显然,这不是所有的彩虹和Unicorn(双关语意图!)。 如果你的Rails应用程序泄漏记忆 - Unicorn会使情况更糟。

每个分叉进程都消耗内存,因为它们是Rails应用程序的副本。 因此,虽然有更多的工作者意味着我们的应用程序可以处理更多的传入请求,我们受到我们的系统上的物理RAM的数量的约束。

Rails应用程序很容易泄漏内存。 即使我们设法堵塞所有内存泄漏,仍然有一个不太理想的垃圾收集器来应对(我指的是MRI实现)。

存在内存泄漏的Rails应用程序

上面显示了一个Rails应用程序运行Unicorn与内存泄漏。

随着时间的推移,内存消耗将继续增长。 使用多个Unicorn工作器将简单地加速内存消耗的速率,直到没有更多的RAM可用时。 然后应用程序会停止 - 导致大量不快乐的用户和客户。

要注意的是不是Unicorn的故障是重要的。 但是,这是一个问题,你会迟早面对。

进入Unicorn工人杀手


一个我遇到的最简单的解决方案是麒麟工人杀手Gem。

自述

unicorn-worker-killerGem提供了基于Unicorn工人自动重启

1)最大请求数,和
2)进程内存大小(RSS),而不影响任何请求。

这将通过避免应用节点处的意外内存耗尽而极大地提高站点的稳定性。

注意,我假设你已经设置并运行Unicorn。

第1步:

加入unicorn-worker-killer到您的Gemfile。 将这个下面unicornGem。

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个条件:最大请求和最大内存。

最大请求数

在本实施例中,如果它已经为500〜600请求之间处理的工人被杀死。 请注意,这是一个范围。 这最小化了同时终止多于1个工人的情况。

最大内存

这里,如果240〜260 MB的存储器之间消耗的工人被杀死。 这是一个范围的原因与上述相同。

每个应用程序都有独特的内存要求。 在正常操作期间,应该粗略估计应用程序的内存消耗。 这样,你可以更好地估计你的工作者的最小和最大内存消耗。

一旦正确配置了所有内容,在部署应用程序时,您会注意到一个不太稳定的内存行为:

Rails应用程序与Unicorn工人杀手

注意图中的扭结。 这是Gem做它的工作!

结论

Unicorn给你的Rails应用程序一个无痛的方法来实现并发,无论是线程安全还是不安全。 然而,它带来了增加RAM消耗的成本。 平衡RAM消耗对于您的应用程序的稳定性和性能是绝对必要的。

我们已经看到3种方式调整你的Unicorn工人的最大性能:

  1. 使用Ruby 2.0为我们提供了一个大大改进的垃圾收集器,它允许我们利用写时复制语义。

  2. 在调整各种配置选项config/unicorn.rb

  3. 使用unicorn-worker-killer杀死并重启工人时,他们太臃肿解决摆好的问题。

资源


  • 一个美妙的解释了Ruby 2.0垃圾收集和写入时复制的语义是如何工作的。

  • 名单的Unicorn配置选项。

:提交本杰明·谭