今年因為工作的關係開始用PHP+Laravel,以前都是用Nodejs,可能因為Nodejs有所謂的非同步概念,平常在開發上很少用到Event,如果要用的話都會搭配個幫忙管理queue的套件像是RabbitMQ,之前在nodejs需要用到event & queue時是在在面對高併發、耗時任務、micro-services之間的非同步溝通的時候。
但php應用這個概念算是蠻廣泛的,實作上也容易,需求量不大的話其實用mysql + supervisor就好。
laravel給我的感覺是把很多東西abstract了,挺好的,維護性我認為比刻Nodejs好太多,如果要手刻寫到維護性像lararvel一樣乾淨好懂,肯定要花很多心力,但laravel讓他變得簡單。Nodejs要做到類似的感覺的話可能要用Nestjs吧。
用Event的好處
抽象化邏輯,把一條api的side effect塞到事件當中,之前我是把它們幫成不同的function,舉例:
學校圖書預約系統,預約更新路由:
- input驗證->使用Form Validation
- 更新預約資料 (主要任務)
- 連動更新其他預約 (ex: 如果這筆預約被接受,其他預約狀態要改成佔用中)
- 寄發Email通知預約被接受
之前我是直接把它做成3個func:
updateReservation, SyncReservation, SendEmail
但其實仔細看會發現
(1)連動更新其他預約跟
(2)寄發Email通知預約被接受都是side effect,我們不需要等代這些動作都完成才response,此時可以利用event+queue的力量來優化響應時間,也讓代碼更整潔。
What is Event & listenr
Event: 某事件發生了,像是訂單完成、使用者註冊。Hey, sth just happen, anyone who wants to do sth, you are free to do it now(handle by listener).
Listener:當某事件發生時要做的事,像是當使用者註冊事件發生,要做寄發認證信。一個Listener可以監聽多個事件。
1:1的寫法
自己手動綁定event&listenr:
在EventServiceProvider中的listen變數維護:
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\OrderShipped' => [
'App\Listeners\SendShipmentNotification',
],
'事件' => ['監聽器A', '監聽器B']
];
利用type hint + 自動偵測的方式來做:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Handle the event.
*
* @param \App\Events\OrderShipped $event
* @return void
*/
public function handle(OrderShipped $event)
{
if (true) {
$this->release(30);
}
}
}
在EventServiceProvider加上:
/**
* Determine if events and listeners should be automatically discovered.
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return true;
}
它就會自動將function handle (EventClass $event)跟這個listenr class綁在一起了。
監聽多個event的寫法:(待補)
防雷提醒:如果已經啟用shouldDiscoverEvents,要小心event重複發送兩次。因為在subscribe的時候就會幫我們做好綁定,此時我們要把原本的type hint拿掉避免自動監測又在綁定一次。
將event變成非同步! Queue worker job基本概念
事前設定
這邊用的drvier用database,queue跟job的維護會用mysql,你也可以換成用redis。
設定queue (queue設定檔跟env)
// .env
QUEUE_CONNECTION=database
Migrate
php artisan queue:table
php artisan migrate
變成非同步,放到queue之後在處理
在listener class加上implements ShouldQueue,就代表綁定到的事件發生時,要做的這個處理會先被放到queue中,之後worker會處理這些job。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
//
}
代表我這次的監聽器的handle會變job被放到queue。
為了要讓這些任務能夠被處理,我們需要啟動worker!這邊我用supervisor來管理,算是一種優化吧!如下。
優化:自動化啟動與重啟woker
因為worker不會知道新的code,所以需要重啟worker才能處理新寫的event, listener
有時worker可能自己掛掉也需要重啟
這樣太手動, 可以利用supervisor幫我們管理worker
安裝
首先需要安裝supervisor,mac可以用homebrew,linux根據官方文件走就可以很簡單。
啟動supervisor (Mac)
brew services start supervisor
寫設定檔
在supervisor檔可以知道要把設定檔放哪:
[include]
files = /usr/local/etc/supervisor.d/*.ini
嗯嗯,看來需要在supervisor.d資料夾下,新增xx專案.ini設定檔。
這邊直接附上最終的設定檔,下面紀錄我踩雷的過程。
[program:goodlink-laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/local/opt/php@7.2/bin/php /Users/maomao/reservation-api/artisan queue:work database --sleep=3 --tries=3
autostart=true
autorestart=true
numprocs=8
redirect_stderr=true
stdout_logfile=~/reservation-api/storage/worker.log
stopwaitsecs=3600
踩雷過程
踩雷:conf檔有錯,會跳出
error: <class 'FileNotFoundError'>, [Errno 2] No such file or directory: file: /usr/local/Cellar/supervisor/4.2.5/libexec/lib/python3.11/site-packages/supervisor/xmlrpc.py line: 557
仔細看發現是我在設定檔中把檔案路徑多打一個\,所以這是一個supervisory在運行時遇到檔案無法讀取的問題。
Debug: 檢查supervisor狀態
sudo brew services list
回傳結果:
Name Status User File
php none
php@7.2 none root
redis none
supervisor none root
看起來有成功啟動服務,Supervisor 已經被安裝並且正在運行,但是沒有管理任何程序。
這才得知設定檔出現了問題!
修正路徑typo後遇到的其他問題:
sudo supervisorctl reread
// No config updates to processes
sudo supervisorctl update
//
sudo supervisorctl start all
// README.md: ERROR (no such process)
// app: ERROR (no such process)
// artisan: ERROR (no such process)
// bootstrap: ERROR (no such process)
// composer.json: ERROR (no such process)
// composer.lock: ERROR (no such process)
// config: ERROR (no such process)
// creds: ERROR (no such process)
// database: ERROR (no such process)
// package.json: ERROR (no such process)
// phpunit.xml: ERROR (no such process)
// public: ERROR (no such process)
// resources: ERROR (no such process)
// routes: ERROR (no such process)
// server.php: ERROR (no such process)
// storage: ERROR (no such process)
// tests: ERROR (no such process)
// vendor: ERROR (no such process)
// webpack.mix.js: ERROR (no such process)
根據錯誤,Supervisor 似乎無法找到要運行的程序。可能配置文件有誤或者你的程序不存在。
此時去看stdout_logfile(worker有錯誤時會寫的log檔):
supervisor: couldn't setuid to 0: Can't drop privilege as nonroot user
supervisor: child process was not spawned
因為我的conf檔中寫了user=root,但是在運行時就不是root了,所以會錯,這時我把user=這一整行拿掉就解決了。
修正完之後重新運行:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start all
就沒有再報錯了
檢查是否啟動成功:
sudo supervisorctl status
這個命令會顯示所有supervisor管理的進程的狀態,如果某個進程是RUNNING狀態,則表示它已經成功啟動。
可以看到成功了,而且運行8個(在設定檔中寫的numprocs=8)
看似一切就緒,實際運行又出一個問題:
踩雷:處理event的時候,發現一直有fail job,問題:
(1)沒有用queue來處理event的話,可以正確操作到firestore,不會跳出需要啟用gRPC的extension。
(2)沒有用queue來處理event的話,能正確讀取到listener的方法,反之跳說找不到方法
可以看出是兩個問題,解決方式為:
(1) php.ini讀取錯誤=>明確指出要用哪一個php,因為我電腦有不同的版本跟不同版本的php.ini檔
(2) 無法正確讀取到listener的方法=>重啟supervisor
解法:強制指定要用哪一份php運行
因為我有多個不同版本的php,只有7.2版本的php.ini有啟用grpc extension。修改我的設定檔:
把php 改成 /usr/local/opt/php@7.2/bin/php
[program:goodlink-laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/local/opt/php@7.2/bin/php /Users/maomao/reservation-api/artisan queue:work database - sleep=3 - tries=3
可以透過運行which php跟php -v我可以知道要用哪一個,要再確認的話就是用php — ini來看預設的php ini是用哪一個。
改完一樣要運行:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start all