當伺服器需要處理一些比較花時間的任務時(如發送Email、上傳影片等等),讓user等待直到執行完畢,是個很不明智的選擇,這時候就很適合使用Queue,讓工作在背景執行,使用者就能立刻做下一件事,不必在那邊等待。
Queue可以想像成是一個裝待辦事項的容器,把所有需要耗時的任務(Job)往裡面丟,根據排隊順序依序處理。
本篇主要選用database driver來說明如何使用Laravel Queue,其他driver如redis則需要安裝相關套件,不在本篇討論範圍中。
config/queue.php:
這邊放queue connection driver相關設定。
.env: QUEUE_CONNECTION預設是sync
QUEUE_CONNECTION=sync
改成database:
QUEUE_CONNECTION=database
- sync是local queue,指當job被push到queue後(程式中執行dispatch()),會馬上執行job,而且是用main thread執行job,因此在開發階段才有用。
若要使用database driver,需要建立jobs table來記錄任務,首先透過這個指令,可以快速建立create_jobs_table migration:
$ php artisan queue:table
建立jobs表:
$ php artisan migrate
建立Queue Job指令:
$ php artisan make:job ${name}
假設建立一個SendEmail的job:
$ php artisan make:job SendEmail
- Queue Jobs在這個路徑: app/Jobs。
- handle()就是執行這個job的進入點。
- 當在執行job過程中有exception發生時,這個job會被釋放出來,再重新丟回Queue中retry,直到設定的最大重試次數為止,指定最大重試次數--tries後面會再提到。
- 如果想手動釋放job,可以使用InteractsWithQueue這個 trait提供的release method:
public function handle()
{
if (condition) {
$this->release(10);
}
}
- 檢查重試次數: 可以用attempts method:
public function handle()
{
if ($this->attempts() > 3) {
//
}
}
將Job push到Queue中: 在controller中,可以直接這樣寫:
use App\Jobs\SendEmail;
$job = (new SendEmail())->onQueue('email');
$this->dispatch($job);
- 由於我們.env中的QUEUE_CONNECTION設定為database,所以執行dispatch後,job會被push到database driver中。
- BTW,如果.env中QUEUE_CONNECTION設定為sync,則會馬上執行SendEmail這個job,並不會塞資料進jobs這張表。
- ->onQueue('email') 這邊其實是對應到jobs這張表的queue欄位,也就是把job push到email這個queue中。
- 注意,如果沒有->onQueue('email')這行,預設塞進去的queue name會是default,如下圖。
那麼若要將Job push到Queue中的地方不是在controller呢? 比如是在Service, Repository這些地方:
$job = (new SendEmail())->onQueue('email');
dispatch($job);
- 事實上,dispatch是一個全域的method,所以可以直接這樣用。
- 甚至是在Route中,也可以使用dispatch:
Route::get('/user', function () {
dispatch(new App\Jobs\SendEmail);
return 'Done!';
});
啟動 Queue Listener: 這邊就是用來執行被push到queue中的jobs:
$ php artisan queue:listen
- 這邊預設會優先執行default queue的jobs。
- Queue是先進先出的概念,先塞進來的job優先處理。
- attempts是指執行次數。
- created_at是指job被建立的時間。
- available_at是指job預計執行的時間。
- reserved_at是指job開始執行的時間。
可以使用--queue來指定優先順序,如下範例,email這個queue中的job永遠會優先被執行,接著才是default這個queue:
$ php artisan queue:listen --queue=email,default
--tries 用來指定job的最大重試次數,如下範例:
php artisan queue:listen --queue=email,default --tries=3
- 當job發生exception時,這個job會從queue中被釋放(這個job的row會從jobs table中被刪除)然後重新塞一個new job到queue中retry(重新塞一個job row 到jobs table中)。
- 這個範例表示job執行3次如果還是failed,就不會再重試了。
接著來看個例子,假設SendEmail job code如下:
假設已經塞了三筆email jobs:
接著執行:
$ php artisan queue:listen --queue=email
- 從console可以發現這三個job被依序處理了。
- 由於已經啟動queue listener,這時候如果又有job被塞進email queue,若舊的job都被執行完畢了,就會馬上執行這個job。
- 經過實測,如果沒有加上 --queue=email,會發現都沒反應,應該是因為只會處理default queue jobs,這邊需特別注意。
- 需特別注意,執行完成的job,該row data會從jobs table中被移除。
如果是希望這個job可以晚一點執行,比如可以延遲5分鐘後再執行:
$job = (new SendEmail())->onQueue('email')->delay(60 * 5);
$this->dispatch($job);
- 這時候塞進去jobs table中的available_at欄位時間就會晚五分鐘。
處理失敗的job
由於job retry至指定上限次數時,紀錄不會留在jobs table中。因此,可以建立一個failed_jobs table來存放failed job,當retry次數超過設定的上限時,可以將這個job寫入failed_jobs 表中。
- 建立fail job migrations & table:
$ php artisan queue:failed-table
$ php artisan migrate
- trigger這段程式,將job塞入queue中:
use App\Jobs\SendEmail;
$job = (new SendEmail())->onQueue('email');
$this->dispatch($job);
- 啟動queue listener,用--tries 來指定job的最大重試次數:
$ php artisan queue:listen --queue=email --tries=3
- 可以看到,SendEmail job執行了三次都failed,所以最終會把fail job紀錄在failed_jobs表,會記錄失敗時間及exception,方便查問題。
$ php artisan queue:failed
- ID就是table中紀錄的uuid,可以用來重試job:
$ php artisan queue:retry ${ID}
$ php artisan queue:retry all
- 刪除某個失敗的job: 這筆job會從failed_jobs表中刪除。
$ php artisan queue:forget ${ID}
- 刪除所有失敗的job: 所有job會從failed_jobs表中刪除。
$ php artisan queue:flush
另外,可以在job class中定義一個failed() method,用來處理當job失敗時要做什麼事情。
- 如同上面的例子,--tries=3 表示若job fail會retry3次,如果第3次還是失敗,就視為真正失敗,會trigger這個failed() method。
- 如果沒有加--tries參數,則執行一次失敗就會trigger failed() method了。
在生產環境啟動Queue Listener
- 在production環境不要用queue:listen來啟動,queue:listen適合在local開發時候用,更動程式碼會馬上生效,不須重新啟動。
- 相反的,在production環境應該要用queue:work,queue:work有daemon選項可以在背景啟動,CPU用量也比較低。
$ php artisan queue:work --daemon --queue=email --tries=3
- queue:work指令用法跟listen幾乎是一樣的,可以用這個指令查看用法:
$ php artisan help queue:work
- 由於在背景執行的queue listener是一個長時間的process,啟動的應用程式狀態會被儲存在memory中,所以只要程式有更改,就必須重新啟動:
$ php artisan queue:restart
這個指令會告訴所有queue listener,在執行完目前job後重新啟動,所以不會有job遺失的問題。
監控Queue Listener
確保Queue中的所有job都正常運作,是最重要的事情,那麼當queue listener啟動失敗或發生狀況的時候該怎麼辦呢? job不就都卡住了嗎?
其實可以透過Supervisor來幫忙做監控,Supervisor 是在 Linux 作業系統上的process監控軟體,用它來啟動和監控queue:work process,可以在queue:work指令失敗時自動重啟。
由於本文已經過於冗長了,這部分就等之後有機會再另外寫一篇來分享囉!