Link Search Menu Expand Document

非同步處理

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

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,需要設定啟動和重開的方式即可。

  1. sudo apt-get install monit
  2. 將 sidekiq.conf (範例參考如下,請將 dojo 置換成你的APP名稱 ) 到 到 /etc/monit/conf.d
  3. 編輯 monitrc 打開 set httpd 的那四行
  4. 輸入 sudo service monit restart 重啟
  5. 輸入 sudo monit status 可以看到應該有成功在監測 sidekiq

     check 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
    

參考資料


Copyright © 2010-2022 Wen-Tien Chang All Rights Reserved.