异步处理

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 无法进行处理。

常见的异步任务包括:

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

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

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

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

  1. brew install redis
  2. redis-server /usr/local/etc/redis.conf

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

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

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

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

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

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

  1. # app/jobs/hard_worker_job.rb
  2. class HardWorkerJob < ActiveJob::Base
  3. queue_as :default
  4. def perform(*args)
  5. # Do something later
  6. end
  7. end

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

  1. HardWorkerJob.perform_later

或是你也可以设定延迟多久才执行:

  1. HardWorkerJob.set( wait: 20.minutes ).perform_later

接着新增 sidekiq 设定 config/sidekiq.yml如下:

  1. ---
  2. :queues:
  3. - default
  4. - mailers

在 Production 服务器上,需要修改 database.yml 补上 pool: 25 允许更多数据库连线。这是因为默认 sidekiq 会跑 25 个执行绪(Thread)平行执行任务去连接数据库。如果没有改的话,任务一多就会发生错误。

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

  1. bundle exec sidekiq

sidekiq提供了一个Web UI接口让我们可以观察目前有哪些任务在执行,并搭配Devise检查必须登入和检查权限,在routes.rb加入:

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

Action Mailer

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

GlobalID

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

  1. HardWorkerJob.perform_later(user.id)

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

  1. def perform(user_id)
  2. user = User.find(user_id)
  3. end

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

  1. HardWorkerJob.perform_later(user)

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

  1. def perform(user)
  2. # user 就是 activerecord 物件了,Rails 自动帮你 query 数据库转换回来
  3. end

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

固定排程

上述的异步是不定时由用户的某个行为来触发,但有时候我们需要的是某个固定时间由系统排程来执行,例如每天凌晨四点进行备份、每天凌晨寄信提醒缴费、每周一凌晨一点产生报表等等。这种情况可以透过 Linux 内建的例行性排程机制 Cron

首先,你先将需要执行的任务写成一个 rake 指令,这样就可以在主机上用crontab指令去执行这个 rake。

不过由于crontab的格式不是非常友善,我们可以透过 whenever 这个 Gem 来编辑,用 Ruby 的语法撰写 crontab 语法,并且他支援搭配 capistrano 在布署时自动更新 crontab,非常方便。

安装方式

修改 Gemfile 加上

  1. gem 'whenever', :require => false

接着执行

  1. $ bundle
  2. $ wheneverize .

排程设定

修改 config/schedule.rb 加入你要的排程工作,例如:

  1. env :PATH, ENV['PATH']
  2. set :output, 'log/cron.log'
  3. every 1.hour do
  4. rake "check_event_registrations"
  5. end
  6. every 1.day do
  7. rake "fetch_user_feeds"
  8. end

Capistrano 设定

Capfile 加入

  1. require "whenever/capistrano"

这样就会在 cap production deploy 自动化布署时,自动更新服务器上的 crontab。

范例程式:异步汇出

汇出 CSV 并寄送 E-mail 完成通知: https://github.com/ihower/shopping-exercise-ac4/pull/2/files

在 Ubuntu 上布署 Sidekiq

安装

在 Ubuntu Linux 上安装 Redis,让 sidekiq 使用:

  1. sudo apt-get install redis-server

设定 Capistrano

使用 https://github.com/seuros/capistrano-sidekiq

  • Gemfile 加上 gem 'capistrano-sidekiq'
  • Capfile 加上 require 'capistrano/sidekiq'

这样每次 cap production deploy 进行布署的时候,就会重开 sidekiq 了。

设定 Monit

很不幸运地,sidekiq 并不是一个非常可靠的 process。有时候会自己死掉,造成非常大的困扰。所以实务上还会需要额外再装一个监控工具,如果发现它挂了,就自动重开它。

我们可以使用 Monit 这个监控工具,这一套工具可以设定监控任何 Process,需要设定启动和重开的方式即可。

  • sudo apt-get install monit
  • 将 sidekiq.conf (范例参考如下,请将 dojo 置换成你的APP名称 ) 到 到 /etc/monit/conf.d
  • 编辑 monitrc 打开 set httpd 的那四行
  • 输入 sudo service monit restart 重启
  • 输入 sudo monit status 可以看到应该有成功在监测 sidekiq
  1. check process sidekiq_dojo_production0
  2. with pidfile "/home/deploy/dojo/shared/tmp/pids/sidekiq-0.pid"
  3. start program = "/bin/su - deploy -c 'cd /home/deploy/dojo/current && /usr/bin/env bundle exec sidekiq --index 0 --pidfile /home/deploy/dojo/shared/tmp/pids/sidekiq-0.pid --environment production --logfile /home/deploy/dojo/shared/log/sidekiq.log -d'" with timeout 30 seconds
  4. stop program = "/bin/su - deploy -c 'cd /home/deploy/dojo/current && /usr/bin/env bundle exec sidekiqctl stop /home/deploy/dojo/shared/tmp/pids/sidekiq-0.pid'" with timeout 20 seconds
  5. group dojo-sidekiq

参考资料