譯文GitHub https://github.com/yuansir/diving-laravel-...
原文連結 https://divinglaravel.com/queue-system/wor...
現在,我們知道了Laravel如何將作業推到不同的佇列中,讓我們來深入瞭解workers如何運作你的作業。 首先,我將workers定義為一個在後臺執行的簡單PHP程式,目的是從儲存空間中提取作業並針對多個配置選項執行它們。
php artisan queue:work
執行此命令將指示Laravel建立應用程式的一個例項並開始執行作業,這個例項將一直存活著,啟動Laravel應用程式的操作只在執行命令時發生一次,同一個例項將被用於執行你的作業,這意味著:
- 避免在每個作業上啟動整個應用程式來節省伺服器資源。
- 在應用程式中所做的任何程式碼更改後必須手動重啟worker。
你也可以這樣執行:
php artisan queue:work --once
這將啟動應用程式的一個例項,處理單個作業,然後幹掉指令碼。
php artisan queue:listen
queue:listen
命令相當於無限迴圈地執行 queue:work --once
命令,這將導致以下問題:
- 每個迴圈都會啟動一個應用程式例項。
- 分配的worker將選擇一個工作並執行。
- worker程式將被幹掉。
使用 queue:listen
確保為每個作業建立一個新的應用程式例項,這意味著程式碼更改以後不必手動重啟worker,同時也意味著將消耗更多的伺服器資源。
我們來看看 Queue\Console\WorkCommand
類的 handle()
方法,這是當你執行 php artisan queue:work
時會執行的方法:
public function handle()
{
if ($this->downForMaintenance() && $this->option('once')) {
return $this->worker->sleep($this->option('sleep'));
}
$this->listenForEvents();
$connection = $this->argument('connection')
?: $this->laravel['config']['queue.default'];
$queue = $this->getQueue($connection);
$this->runWorker(
$connection, $queue
);
}
首先,我們檢查應用程式是否處於維護模式,並使用 --once
選項,在這種情況下,我們希望指令碼正常執行,因此我們不執行任何作業,我們只需要在完全殺死指令碼前讓worker在一段時間內休眠。
Queue\Worker
的 sleep()
方法看起來像這樣:
public function sleep($seconds)
{
sleep($seconds);
}
為什麼我們不能在 handle() 方法中返回null來終止指令碼?
如前所述, queue:listen
命令在迴圈中執行 WorkCommand
:
while (true) {
// This process simply calls 'php artisan queue:work --once'
$this->runProcess($process, $options->memory);
}
如果應用程式處於維護模式,並且 WorkCommand
立即終止,這將導致迴圈結束,下一個在很短的時間內啟動,最好在這種情況下導致一些延遲,而不是通過建立我們不會真正使用的大量應用程式例項。
監聽事件
在 handle()
方法裡面我們呼叫 listenForEvents()
方法:
protected function listenForEvents()
{
$this->laravel['events']->listen(JobProcessing::class, function ($event) {
$this->writeOutput($event->job, 'starting');
});
$this->laravel['events']->listen(JobProcessed::class, function ($event) {
$this->writeOutput($event->job, 'success');
});
$this->laravel['events']->listen(JobFailed::class, function ($event) {
$this->writeOutput($event->job, 'failed');
$this->logFailedJob($event);
});
}
在這個方法中我們會監聽幾個事件,這樣我們可以在每次作業處理中,處理完或處理失敗時向使用者列印一些資訊。
記錄失敗作業
一旦作業失敗 logFailedJob()
方法會被呼叫
$this->laravel['queue.failer']->log(
$event->connectionName, $event->job->getQueue(),
$event->job->getRawBody(), $event->exception
);
queue.failer
容器別名在 Queue\QueueServiceProvider::registerFailedJobServices()
中註冊:
protected function registerFailedJobServices()
{
$this->app->singleton('queue.failer', function () {
$config = $this->app['config']['queue.failed'];
return isset($config['table'])
? $this->databaseFailedJobProvider($config)
: new NullFailedJobProvider;
});
}
/**
* Create a new database failed job provider.
*
* @param array $config
* @return \Illuminate\Queue\Failed\DatabaseFailedJobProvider
*/
protected function databaseFailedJobProvider($config)
{
return new DatabaseFailedJobProvider(
$this->app['db'], $config['database'], $config['table']
);
}
如果配置了 queue.failed
,則將使用資料庫佇列失敗,並將有關失敗作業的資訊簡單地儲存在資料庫表中的:
$this->getTable()->insertGetId(compact(
'connection', 'queue', 'payload', 'exception', 'failed_at'
));
執行worker
要執行worker,我們需要收集兩條資訊:
- worker的連線資訊從作業中提取
- worker找到作業的佇列
如果沒有使用 queue.default
配置定義的預設連線。您可以為 queue:work
命令提供 --connection=default
選項。
佇列也是一樣,您可以提供一個 --queue=emails
選項,或選擇連線配置中的 queue
選項。一旦這一切完成, WorkCommand::handle()
方法會執行 runWorker()
:
protected function runWorker($connection, $queue)
{
$this->worker->setCache($this->laravel['cache']->driver());
return $this->worker->{$this->option('once') ? 'runNextJob' : 'daemon'}(
$connection, $queue, $this->gatherWorkerOptions()
);
}
在worker類屬性在命令構造後設定:
public function __construct(Worker $worker)
{
parent::__construct();
$this->worker = $worker;
}
容器解析 Queue\Worker
例項,在runWorker()
中我們設定了worker將使用的快取驅動,我們也根據--once
命令來決定我們呼叫什麼方法。
如果使用 --once
選項,我們只需呼叫 runNextJob
來執行下一個可用的作業,然後指令碼就會終止。 否則,我們將呼叫 daemon
方法來始終保持程式處理作業。
在開始工作時,我們使用 gatherWorkerOptions()
方法收集使用者給出的命令選項,我們稍後會提供這些選項,這個工具是 runNextJob
或 daemon
方法。
protected function gatherWorkerOptions()
{
return new WorkerOptions(
$this->option('delay'), $this->option('memory'),
$this->option('timeout'), $this->option('sleep'),
$this->option('tries'), $this->option('force')
);
}
守護程式
讓我看看 Worker::daemon()
方法,這個方法的第一行呼叫了 Worker::daemon()
方法
protected function listenForSignals()
{
if ($this->supportsAsyncSignals()) {
pcntl_async_signals(true);
pcntl_signal(SIGTERM, function () {
$this->shouldQuit = true;
});
pcntl_signal(SIGUSR2, function () {
$this->paused = true;
});
pcntl_signal(SIGCONT, function () {
$this->paused = false;
});
}
}
這種方法使用PHP7.1的訊號處理, supportsAsyncSignals()
方法檢查我們是否在PHP7.1上,並載入pcntl
副檔名。
之後pcntl_async_signals()
被呼叫來啟用訊號處理,然後我們為多個訊號註冊處理程式:
- 當指令碼被指示關閉時,會引發
SIGTERM
。 SIGUSR2
是使用者定義的訊號,Laravel用來表示指令碼應該暫停。- 當暫停的指令碼繼續進行時,會引發
SIGCONT
。
這些訊號從Process Monitor(如 Supervisor )傳送並與我們的指令碼進行通訊。
Worker::daemon()
方法中的第二行讀取最後一個佇列重新啟動的時間戳,當我們呼叫queue:restart
時該值儲存在快取中,稍後我們將檢查是否和上次重新啟動的時間戳不符合,來指示worker在之後多次重啟。
最後,該方法啟動一個迴圈,在這個迴圈中,我們將完成其餘獲取作業的worker,執行它們,並對worker程式執行多個操作。
while (true) {
if (! $this->daemonShouldRun($options, $connectionName, $queue)) {
$this->pauseWorker($options, $lastRestart);
continue;
}
$job = $this->getNextJob(
$this->manager->connection($connectionName), $queue
);
$this->registerTimeoutHandler($job, $options);
if ($job) {
$this->runJob($job, $connectionName, $options);
} else {
$this->sleep($options->sleep);
}
$this->stopIfNecessary($options, $lastRestart);
}
確定worker是否應該處理作業
呼叫 daemonShouldRun()
檢查以下情況:
- 應用程式不處於維護模式
- Worker沒有暫停
- 沒有事件監聽器阻止迴圈繼續
如果應用程式在維護模式下,worker使用--force
選項仍然可以處理作業:
php artisan queue:work --force
確定worker是否應該繼續的條件之一是:
$this->events->until(new Events\Looping($connectionName, $queue)) === false)
這行觸發 Queue\Event\Looping
事件,並檢查是否有任何監聽器在 handle()
方法中返回false,這種情況下你可以強制您的workers暫時停止處理作業。
如果worker應該暫停,則呼叫 pauseWorker()
方法:
protected function pauseWorker(WorkerOptions $options, $lastRestart)
{
$this->sleep($options->sleep > 0 ? $options->sleep : 1);
$this->stopIfNecessary($options, $lastRestart);
}
sleep
方法並傳遞給控制檯命令的 --sleep
選項,這個方法呼叫
public function sleep($seconds)
{
sleep($seconds);
}
指令碼休眠了一段時間後,我們檢查worker是否應該在這種情況下退出並殺死指令碼,稍後我們看一下stopIfNecessary
方法,以防指令碼不能被殺死,我們只需呼叫 continue;
開始一個新的迴圈:
if (! $this->daemonShouldRun($options, $connectionName, $queue)) {
$this->pauseWorker($options, $lastRestart);
continue;
}
Retrieving 要執行的作業
$job = $this->getNextJob(
$this->manager->connection($connectionName), $queue
);
getNextJob()
方法接受一個佇列連線的例項,我們從佇列中獲取作業
protected function getNextJob($connection, $queue)
{
try {
foreach (explode(',', $queue) as $queue) {
if (! is_null($job = $connection->pop($queue))) {
return $job;
}
}
} catch (Exception $e) {
$this->exceptions->report($e);
$this->stopWorkerIfLostConnection($e);
}
}
我們簡單地迴圈給定的佇列,使用選擇的佇列連線從儲存空間(資料庫,redis,sqs,...)獲取作業並返回該作業。
要從儲存中retrieve作業,我們查詢滿足以下條件的最舊作業:
- 推送到
queue
,我們試圖從中找到作業 - 沒有被其他worker reserved
- 可以在給定的時間內執行,有些作業在將來被推遲執行
- 我們也取到了很久以來被凍結的作業並重試
一旦我們找到符合這一標準的作業,我們將這個作業標記為reserved,以便其他workers獲取到,我們還會增加作業監控次數。
監控作業超時
下一個作業被retrieved之後,我們呼叫 registerTimeoutHandler()
方法:
protected function registerTimeoutHandler($job, WorkerOptions $options)
{
if ($this->supportsAsyncSignals()) {
pcntl_signal(SIGALRM, function () {
$this->kill(1);
});the
$timeout = $this->timeoutForJob($job, $options);
pcntl_alarm($timeout > 0 ? $timeout + $options->sleep : 0);
}
}
再次,如果 pcntl
擴充套件被載入,我們將註冊一個訊號處理程式幹掉worker程式如果該作業超時的話,在配置了超時之後我們使用 pcntl_alarm()
來傳送一個 SIGALRM
訊號。
如果作業所花費的時間超過了超時值,處理程式將會終止該指令碼,如果不是該作業將通過,並且下一個迴圈將設定一個新的報警覆蓋第一個報警,因為程式中可能存在單個報警。
作業只在PHP7.1以上起效,在window上也無效 ¯_(ツ)_/¯
處理作業
runJob()
方法呼叫 process()
:
public function process($connectionName, $job, WorkerOptions $options)
{
try {
$this->raiseBeforeJobEvent($connectionName, $job);
$this->markJobAsFailedIfAlreadyExceedsMaxAttempts(
$connectionName, $job, (int) $options->maxTries
);
$job->fire();
$this->raiseAfterJobEvent($connectionName, $job);
} catch (Exception $e) {
$this->handleJobException($connectionName, $job, $options, $e);
}
}
raiseBeforeJobEvent()
觸發 Queue\Events\JobProcessing
事件, raiseAfterJobEvent()
觸發 Queue\Events\JobProcessed
事件。 markJobAsFailedIfAlreadyExceedsMaxAttempts()
檢查程式是否達到最大嘗試次數,並將該作業標記為失敗:
protected function markJobAsFailedIfAlreadyExceedsMaxAttempts($connectionName, $job, $maxTries)
{
$maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries;
if ($maxTries === 0 || $job->attempts() <= $maxTries) {
return;
}
$this->failJob($connectionName, $job, $e = new MaxAttemptsExceededException(
'A queued job has been attempted too many times. The job may have previously timed out.'
));
throw $e;
}
否則我們在作業物件上呼叫 fire()
方法來執行作業。
從哪裡獲取作業物件
getNextJob()
方法返回一個 Contracts\Queue\Job
的例項,這取決於我們使用相應的Job例項的佇列驅動程式,例如如果資料庫佇列驅動則選擇 Queue\Jobs\DatabaseJob
。
迴圈結束
在迴圈結束時,我們呼叫 stopIfNecessary()
來檢查在下一個迴圈開始之前是否應該停止程式:
protected function stopIfNecessary(WorkerOptions $options, $lastRestart)
{
if ($this->shouldQuit) {
$this->kill();
}
if ($this->memoryExceeded($options->memory)) {
$this->stop(12);
} elseif ($this->queueShouldRestart($lastRestart)) {
$this->stop();
}
}
shouldQuit
屬性在兩種情況下設定,首先listenForSignals()
內部的作為 SIGTERM
訊號處理程式,其次在 stopWorkerIfLostConnection()
中
protected function stopWorkerIfLostConnection($e)
{
if ($this->causedByLostConnection($e)) {
$this->shouldQuit = true;
}
}
在retrieving和處理作業時,會在幾個try ... catch語句中呼叫此方法,以確保worker應該處於被幹掉的狀態,以便我們的Process Control可能會啟動一個新的資料庫連線。
causedByLostConnection()
方法可以在 Database\DetectsLostConnections
trait中找到。memoryExceeded()
檢查記憶體使用情況是否超過當前設定的記憶體限制,您可以使用 --memory
選項設定限制。
轉載請註明: 轉載自Ryan是菜鳥 | LNMP技術棧筆記
如果覺得本篇文章對您十分有益,何不 打賞一下
本文連結地址: 剖析Laravel佇列系統--Worker
本作品採用《CC 協議》,轉載必須註明作者和本文連結