非同步處理
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
- 匯入大筆資料
- 匯出大筆資料
- 呼叫第三方服務
- 更多範例 Real World Rails Background Jobs
對於這種任務,非同步的處理就非常重要。非同步的意思是讓任務的處理在背景完成,而不在瀏覽器的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
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
如下:
# 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
或是你也可以設定延遲多久才執行:
HardWorkerJob.set( wait: 20.minutes ).perform_later
接著新增 sidekiq 設定 config/sidekiq.yml
如下:
---
:queues:
- default
- mailers
在 Production 伺服器上,需要修改 database.yml 補上
pool: 25
允許更多資料庫連線。這是因為預設 sidekiq 會跑 25 個執行緒(Thread)平行執行任務去連接資料庫。如果沒有改的話,任務一多就會發生錯誤。
最後,我們需要啟動另外的sidekiq process來執行這些非同步的任務:
bundle exec sidekiq
sidekiq提供了一個Web UI介面讓我們可以觀察目前有哪些任務在執行,並搭配Devise檢查必須登入和檢查權限,在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物件,就要自行注意了。
固定排程
上述的非同步是不定時由用戶的某個行為來觸發,但有時候我們需要的是某個固定時間由系統排程來執行,例如每天凌晨四點進行備份、每天凌晨寄信提醒繳費、每週一凌晨一點產生報表等等。這種情況可以透過 Linux 內建的例行性排程機制 Cron。
首先,你先將需要執行的任務寫成一個 rake 指令,這樣就可以在主機上用crontab指令去執行這個 rake。
不過由於crontab的格式不是非常友善,我們可以透過 whenever 這個 Gem 來編輯,用 Ruby 的語法撰寫 crontab 語法,並且他支援搭配 capistrano 在佈署時自動更新 crontab,非常方便。
安裝方式
修改 Gemfile
加上
gem 'whenever', :require => false
接著執行
$ bundle
$ wheneverize .
排程設定
修改 config/schedule.rb
加入你要的排程工作,例如:
env :PATH, ENV['PATH']
set :output, 'log/cron.log'
every 1.hour do
rake "check_event_registrations"
end
every 1.day do
rake "fetch_user_feeds"
end
Capistrano 設定
在 Capfile
加入
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 使用:
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
可以看到應該有成功在監測 sidekiqcheck process sidekiq_dojo_production0 with pidfile "/home/deploy/dojo/shared/tmp/pids/sidekiq-0.pid" 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 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 group dojo-sidekiq