加载中...

异步处理


Nine people can’t make a baby in a month.  — Fred Brooks, The Mythical Man-Month 作者

通常一个HTTP request/response的工作时间理想上都要在 200ms 以内完成,要不然 web server 通常也会限制在 30 秒以内,不然就会出现 timeout 错误。一个运算时间太久的 request 除了让使用者感受不佳之外,对于服务器效能上的影响也很巨大。使用者可能等待不及重新reload,于是相同的任务又在重头执行一遍。一个 request 长时间占据了一个 rails process,也让其他 reuqest 无法进行处理。

常见的异步任务包括:

  • 寄出E-mail
  • 汇入大笔资料
  • 汇出大笔资料
  • 呼叫第三方服务

对于这种任务,异步的处理就非常重要。异步的意思是让任务的处理在背景完成,而不在浏览器的HTTP request/response流程中完成,等完成之后再通知使用者即可。

Rails 4.2之后内建了一个统一的处理接口叫做ActiveJob,就像ActiveRecord透过不同的Adapter可以支援不同数据库,ActiveJob也支援了非常多种不同的排程工具,最多人使用的有:

  • delayed_job 使用关联式数据库,非常方便安装使用。
  • sidekiq 使用高效能的Redis: key-value store来储存要执行的任务,并且善用多执行序来增加效能,号称可以以一个process抵上20个delayed_job的processes。

我们来用sidekiq举例,本机Mac需要安装Redis:

brew install redis

而在Ubuntu服务器上可以透过sudo apt-get install redis-server进行安装。 在Gemfile新增gem 'sidekiq'然后bundle

默认的ActiveJob Adapter是:inline,也就是没有异步。我们必须编辑config/environments/production.rb切换成改用:sidekiq如下:

# be sure to have the adapter gem in your Gemfile and follow the adapter specific
# installation and deployment instructions
config.active_job.queue_adapter = :sidekiq

接着编辑config/application.rb加入一行设定让Rails可以找到job档案:

config.eager_load_paths += %W( #{config.root}/app/jobs )

接下来要建立一个Worker非常容易,执行rails g job hard_worker会产生app/jobs/hard_worker_job.rb这个档案,

# app/jobs/hard_worker_job.rb
class HardWorkerJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end

接着在需要异步的地方使用以下程式,就会将工作排程进sidekiq:

HardWorkerJob.perform_later

最后,我们需要启动另外的sidekiq process来执行这些异步的任务:

bundle exec sidekiq

sidekiq提供了一个Web UI接口让我们可以观察目前有哪些任务在执行。如果搭配Devise的话,需要在Gemfile加上gem 'sinatra', '>= 1.3.0', :require => nil,以及routes.rb加入:

require 'sidekiq/web'
authenticate :user, lambda { |u| u.admin? } do
  mount Sidekiq::Web => '/sidekiq'
end

Action Mailer

我们在“ActionMailer: E-mail发送”那一章介绍过deliver_later方法,如果我们有设定好ActiveJob,那Rails就会用异步寄信。

GlobalID

因为异步的工作是另一个process在执行,在从Rails这端指派工作的时候,设计的参数会避免将物件进行序列化(serialize)动作,以免另一个process无法顺利deserialize回来,例如这中间刚好程式码有变更,造成类别的定义不同,更别提从enqueue到真正执行之间会有时间差,资料内容可能改变了。因此参数最好是简单的基本型态,例如字串、数字、阵列或杂凑等等。例如你想要传递一个使用者物件当作参数,我们不传整个user物件,而是传user id而已:

HardWorkerJob.perform_later(user.id)

接着在worker那端设计成根据user id从数据库再拉出来:

  def perform(user_id)
    user = User.find(user_id)
  end

事实上,由于这是非常常见的设计,Rails甚至自动会针对ActiveRecord物件进行转换,例如你写成

HardWorkerJob.perform_later(user)

那在Rails内部会自动帮你把user物件转成一个GlobalID字串放进queue里,让以下的job可以直接运作:

  def perform(user)
    # user 就是 activerecord 物件了,Rails 自動幫你 query 資料庫轉換回來
  end

不过如果你面对的不是ActiveRecord物件,就要自行注意了。

固定排程

另一种执行异步任务的方式,则是透过作业系统的Cron排程工具,你可以将需要执行的工作写成一个rake指令,在主机上用crontab指令进行编辑。例如每天凌晨四点进行备份、每周一凌晨一点产生报表等等。

由于crontab的格式不是非常友善,我们可以透过whenever这个Gem来编辑,这也可以搭配Capistrano做自动化部署,非常方便。

参考资料

  • Active Job Basics
  • ActiveJob::QueueAdapters
  • Distributed Ruby and Rails

还没有评论.