使用 Puma Web 服务器部署 Rails 应用

n342 7年前

使用 Puma Web 服务器部署 Rails 应用

相对于一次只处理一个请求的 Web 应用程序,并发处理请求的 Web 应用程序,能够更高效的使用动态资源。Puma 是和 Unicorn 相竞争的 Web 服务器,它能够处理并发请求。

Puma 使用线程,以及工作者进程,能够更多的利用可用的 CPU。在 Puma 中,如果整个基础代码是线程安全的,那么你可用利用线程。否则,在使用 Puma 的时候,你只能使用工作者进程进行拓展。

这个指南会简单介绍,使用 Puma Web 服务器部署新的 Rails 应用程序到 Heroku 平台。 对于基本的Rails 安装,请参看 Rails 入门.

在部署到生产环境之前,请先在准生产环境(staging environment)测试。

将 Puma 加入应用程序

Gemfile

首先,将 Puma 添加到应用程序的 Gemfile 文件:

gem 'puma'

Procfile

设置 Puma 为应用程序 Procfile 文件中Web 进程的服务器。你可以在一行中设置大多数值:

web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development}

但是,我们推荐生成一个配置文件:

web: bundle exec puma -C config/puma.rb

请确保 Procfile 大写正确,并且签入到 git。

Config

在 config/puma.rb 中为 Puma 创建一个配置文件,或者任选一个目录。对于一个简单的 Rails 应用程序,我们推荐下面的基本配置:

workers Integer(ENV['WEB_CONCURRENCY'] || 2)  threads_count = Integer(ENV['MAX_THREADS'] || 5)  threads threads_count, threads_count    preload_app!    rackup      DefaultRackupport        ENV['PORT']     || 3000environment ENV['RACK_ENV'] || 'development'on_worker_boot do    # Worker specific setup for Rails 4.1+    # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot    ActiveRecord::Base.establish_connectionend

你同样必须保证,Rails 应用程序,为所有的线程和工作者(这将在后续介绍),提供足够的连接池中的可用数据库连接。

如果你的应用程序不是线程安全的,你将只能使用工作者(worker)。设置最小和最大线程数为1:

$ heroku config:set MAX_THREADS=1

查看下面关于线程安全的部分,以获取更多信息。

工作者(Worker)

workers Integer(ENV['WEB_CONCURRENCY'] || 2)

环境变量WEB_CONCURRENCY 可能被设置为基于dyno大小的默认值。手动设置可以使用 heroku 配置:set WEB_CONCURRENCY。

Puma 在每个dyno内部生成多个操作系统进程,以允许 Rails应用程序支持多个并发请求。 用Puma的术语来说,它们称为工作者进程(不要和Heroku工作者进程相混淆,Heroku工作者进程是在它们自己的dyno里面运行的)。工作者进程处于操作系统级别的相互隔离,因此,无需保持线程安全。

如果你在使用JRuby或者Windows,多进程模式无法工作,因为JVM和Windows不支持多进程。如果你在使用JRuby或者Windows,请从配置中忽略这行。

每个工作者进程使用额外的内存。这限制了在一个dyno中运行的进程数。依据典型的Rails内存占用,在1x dyno中,你大概可以运行2-4个Puma工作者进程。我们推荐在配置变量中指定这个数值,以便于应用程序更快的调整。请监控应用程序日志的R14错误(内存配额溢出),使用 附加日志(logging addons)或者 heroku日志.

线程

threads_count = Integer(ENV['MAX_THREADS'] || 5)  threads threads_count, threads_count

Puma 可以一个线程服务一个请求,这些线程来自内部的线程池。对于Web应用程序来说,这让Puma获得了额外的并发性。宽泛的说,工作者消耗更多的RAM,线程消耗更多的CPU,两者都提供了更高的并发性。

在MRI之中,存在一个全局解释锁(Global Interpreter Lock (GIL)),它确保,在任意时刻,只有一个线程在运行。IO操作,如数据库调用,文件系统交互,或者外部HTTP调用,都不会锁定GIL。大多数Rails应用程序大量使用IO,因此,增加额外的线程将允许Puma处理多线程,获得更大的吞吐量。JRuby和Rubinius也从Puma受益。这些Ruby实现并没有GIL,无论什么情况下,它们都会以并行的方式运行所有的线程。

Puma允许配置线程池中的线程数,通过设置min和max来控制每个Puma实例使用的线程数。当处于非负载状态下,最小线程可以让应用程序降低资源消耗。这个特性在Heroku上并不需要,因为应用程序可以消耗给定的dyno上的所有资源。我们推荐将min和max设置为相同的值。

每个Puma工作者都将可以生成指定数目的线程。

预加载应用程序(Preload app)

preload_app!

预加载应用程序减少了Puma工作者进程启动的时间,同时,可以使用on_worker_boot调用,管理每个单一工作者的外部连接。在上面的配置中,这些调用,用来为每个工作者进程和Postgres正常建立连接。

关于工作者引导(On worker boot)

这里的on_worker_boot块,是在工作者生成后运行的,但是,是在它开始接受请求之前。这个块对于连接到不同的服务非常有用,因为在多个进程之间,连接不能共享。这和Unicorn的after_fork块非常类似。它只在使用多进程模式(也就是,有指定的工作者)的情况下才需要。

如果你正在使用Rails 4.1+,你可以使用database.yml 来设置连接池的大小 ,按照下面的来做即可:

on_worker_boot do    # Valid on Rails 4.1+ using the `config/database.yml` method of setting `pool` size    ActiveRecord::Base.establish_connection  end

否则,你必须使用重连代码:

on_worker_boot do    # Valid on Rails up to 4.1 the initializer method of setting `pool` size    ActiveSupport.on_load(:active_record) do      config = ActiveRecord::Base.configurations[Rails.env] ||          Rails.application.config.database_configuration[Rails.env]      config['pool'] = ENV['MAX_THREADS'] || 5      ActiveRecord::Base.establish_connection(config)      end  end

如果你正在使用初始化器(initializer),你应该尽快切换到database.yml方法。如果在Puma中采用混合模式(hybrid mode),使用初始化器需要复制代码。关于正在发生什么,以及源头在何处,初始化器方法可能会导致混乱。

我们在默认配置里设置数据库数据池尺寸。需要更多信息请阅读在Ruby中的并发与数据库连接ActiveRecord(Concurrency and Database Connections in Ruby with ActiveRecord)。我们也要确定创建的新连接连到数据库哪里。

很多数据存储诸如 Postgres, Redis 或者 memcache,都需要你重连接。在预加载那一节,我们会展示如何重连接 Active Record。如果你使用 Resque,它连接到 Redis 时,你将需要重连接:

on_worker_boot do    # ...    if defined?(Resque)           Resque.redis = ENV["<redis-uri>"] || "redis://127.0.0.1:6379"    end  end

如果在你启动应用时没有成功连接,可以查阅 gem 文档,了解如何在这个模块上重新连接。

Rackup

rackup      DefaultRackup

使用rackup命令,告知Puma如何启动rack应用程序。这应该在应用程序的config.ru文件中,这个文件是新建一个项目时,由Rails自动生成的。

在新版本的Puma中,这行是不需要的。

端口(Port)

port        ENV['PORT']     || 3000

这是Puma将会绑定的端口。当Web进程启动时,HeroKu会设置ENV['PORT']。本地默认设置端口为3000,以匹配Rails的默认值。

环境(Environment)

environment ENV['RACK_ENV'] || 'development'

这是设置Puma的环境。在Heroku之中,ENV['RACK_ENV']会默认设置为'production'。

超时(Timeout)

在Puma之中,没有请求超时。 Heroku 路由将 超过30秒的请求设为超时。 尽管会返回错误给客户端,但是Puma仍会继续处理这个请求,就像路由没有采用任何方式,提前告知Puma,此请求已经终止了一样。

增加 Rack 超时 到项目的gem,然后在初始化器(initializer)中设置低于30的值:

# config/initializers/timeout.rbRack::Timeout.timeout = 20  # seconds

现在,任何超过20秒的请求都会被终止,同时栈跟踪信息会输出到日志之中。栈跟踪信息,有助于查明应用程序的哪个部分会导致超时,以便修复。

样例代码

开源codetriage.com项目使用 Puma 并且你可以在这个配置文件中看 Puma

线程安全

线程安全代码可以被多线程运行并且无错误。并不是多有的 Ruby 代码是 threadsafe 代码,而且决定你的代码和所有的库文件在多线程上运行是有点困难的。

直到 Rails4 版本,出现了一个伴有疑虑的线程安全互通模式。纵然 Rails 是线程安全,但是它不保证你的代码能运行在安全线程上。如果你还没有在线程化的环境下运行你的应用,我们建议部署以及设置 MIN_THREADS 和 MAX_THREADS 二者为1:

$ heroku config:set MIN_THREADS=1 MAX_THREADS=1

你仍可以通过增加工作器来获得并发。因为工作器运行在不同的进程上,而且不会共享内存,不是安全线程的代码可以运行在多工作器进程上。

一旦你在工作器上运行了应用,你可以增加在工作台上的线程数量以及部署为2:

$ heroku config:set MIN_THREADS=2 MAX_THREADS=2

你需要监视异常情况并且查找错误,像(致命的)死锁,竞争条件以及能够修改全局或者共享变量的位置。

并发程序的bug很难被侦测和修复,因此必须确定你的应用在部署前已经被充分地测试。如果让你的应用线程安全,是非常值得的,比起单独使用工作者(worker),使用扩展的Puma线程和工作者(worker)取得了更多的生产力.

一旦你对应用的预期行为有信心,你可以增加你的线程数。

优化线程数,我们被推荐着眼于请求的延迟。如果你的应用是在负载下额外的线程,在一定程度上,将会减少请求的延迟。一旦额外的新线程不再给你的应用提升可测的请求时间,那就不再需要添加额外的线程。

数据库连接

随着应用程序并发量的增加,你会需要更多的与数据库的连接。确定每个应用需要连接数的一个准则是用PUMA_WORKERS乘以MAX_THREADS。这将会确定每个dyno消费多少个连接。

Rails维护自己的数据库连接池,同时对于每个工作者进程新建一个连接池。一个工作者的线程将会在同一个连接池中操作。确保你的Rails数据库连接池中有足够的连接,这样就有MAX_THREADS个数的连接可以使用了。如果你看见这个错误:

ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds

这表明你的Rails连接池太小了。想要深入了解该话题,请阅读dev center中的文章并发和数据库连接

慢客户端

一个慢客户端发送和接收数据是非常慢的。举个例子,一个 app 接收用户上传的手机图片网络类型非 WiFi,4G 或其他网络类型。这种连接可能导致某些服务器拒绝服务,如 unicorn,整个上传过程必须等待请求完成。

puma 可以允许多个慢客户端连接而不需要一个被请求的事件。因此,puma 处理慢客户端非常快速。你要是有满客户端的情况,使用 puma 是不错的选择。

Backlog

在Puma中可以设置 “backlog” 的值。这个值是,Puma在开始拒绝HTTP请求之前,socket队列的请求数。它的默认值为1024,建议不要修改或降低这个值。减小这个值,看起来像是一个好主意,这样,当一个dyno处于繁忙之中的时候,请求可以发送到不那么繁忙的dyno。当Heroku重新路由(reroute)一个弹回(bounced)请求的时候,它假定整个应用程序已经饱和。此时,每个连接延迟5秒,因此,每个请求惩罚性的自动延迟5秒。你可以获取关于 路由运转 的更多信息。另外,当一个dyno开始弹回(bounce)请求的时候,很可能是由于负载增加,所有的dyno都将弹回请求。重复弹回同样的请求,将导致客户端更高的错误率。

一个随意的高backlog值允许dyno处理陡然增加的请求。降低这个值,不能明显加速应用程序,却更可能导致客户端更多的失败请求。Heroku建议 不要 设置backlog的值,而使用其默认值。