RabbitMQ 入門 - 工作佇列

學冰發表於2018-03-26

基於 官方文件 翻譯

工作佇列

(using php-amqplib)

img

在第一篇教程中,我們編寫了用於從命名佇列傳送和接收訊息的程式。 在這一個章節中,我們將建立一個工作佇列,用於在多個 Work Queue 之間分配耗時的任務。

工作佇列(又名:任務佇列)背後的主要思想是避免立即執行資源密集型任務,並且必須等待它完成。相反,我們安排稍後完成任務。我們將任務封裝為訊息並將其傳送到佇列。 在後臺執行的工作程式將彈出任務並最終執行作業。當你執行許多工人時,任務將在他們之間共享。

這個概念在 Web 應用程式中特別有用,因為在短的 HTTP 請求視窗中無法處理複雜的任務。

準備

在本教程的前一部分中,我們傳送了一條包含“Hello World!”的訊息。 現在我們將傳送代表複雜任務的字串。 我們沒有真實世界的任務,比如要調整大小的影像或要渲染的 PDF 檔案,所以讓我們假裝我們很忙 - 使用sleep() 函式來假裝。 我們將把字串中的點數作為它的複雜度;每一個點都會佔用一秒的“工作”。 例如,Hello ...描述的假任務將需要三秒鐘。

我們稍微修改前面例子中的 send.php 程式碼,以允許從命令列傳送任意訊息。 這個程式會把任務安排到我們的工作佇列中,所以讓我們把它命名為 new_task.php:

$data = implode(' ', array_slice($argv, 1));
if(empty($data)) $data = "Hello World!";
$msg = new AMQPMessage($data);

$channel->basic_publish($msg, '', 'hello');

echo " [x] Sent ", $data, "\n";

我們舊的 receive.php 指令碼還需要進行一些更改:它需要偽造郵件正文中每個點的第二個工作。 它會從佇列中彈出訊息並執行任務,所以我們稱之為 worker.php:

$callback = function($msg){
  echo " [x] Received ", $msg->body, "\n";
  sleep(substr_count($msg->body, '.'));
  echo " [x] Done", "\n";
};

$channel->basic_consume('hello', '', false, true, false, false, $callback);

請注意,我們的假任務模擬執行時間。

像在教程1中一樣執行它們:

# shell 1
php worker.php
# shell 2
php new_task.php "A very hard task which takes two seconds.."

迴圈排程

使用任務佇列的優點之一是可以輕鬆地平行工作。 如果我們正在積累積壓的工作,我們可以增加更多的工作人員,並且這種方式很容易擴充套件。

首先,我們試著同時執行兩個 worker.php 指令碼。 他們都會從佇列中獲取訊息,但具體到底是什麼? 讓我們來看看。

您需要開啟三個控制檯。 兩個將執行 worker.php 指令碼。 這些控制檯將成為我們的兩個消費者 - C1 和 C2。

# shell 1
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C
# shell 2
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C

In the third one we'll publish new tasks. Once you've started the consumers you can publish a few messages:

在第三個我們將釋出新的任務。 你可以啟動消費者(consumers )釋出幾條訊息:

# shell 3
php new_task.php First message.
php new_task.php Second message..
php new_task.php Third message...
php new_task.php Fourth message....
php new_task.php Fifth message.....

讓我們看看交付給我們工人的東西:

# shell 1
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....'
# shell 2
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

預設情況下,RabbitMQ 將按順序將每條訊息傳送給下一個使用者。 平均而言,每個消費者將獲得相同數量的訊息。 這種分配訊息的方式稱為迴圈法。 嘗試與三名或更多的工人。

訊息確認

做任務可能需要幾秒鐘的時間。 你可能想知道如果其中一個消費者開始一項長期任務並且只是部分完成而死亡會發生什麼。 使用我們當前的程式碼,一旦 RabbitMQ 向客戶傳送訊息,它立即將其標記為刪除。 在這種情況下,如果你殺了一個工人,我們將失去剛剛處理的資訊。 我們也會失去所有派發給這個特定工作人員但尚未處理的訊息。

但我們不想失去任何任務。 如果一名工人死亡,我們希望將任務交付給另一名工人。

為了確保訊息永不丟失,RabbitMQ 支援訊息確認。消費者發回詢問(acknowledgement),告訴 RabbitMQ 收到,處理了特定的訊息,並且 RabbitMQ 可以自由刪除它。

如果消費者死亡(其通道關閉,連線關閉或TCP連線丟失),RabbitMQ 將理解訊息未被完全處理,並將對其重新排隊。 如果有其他消費者同時線上,它會迅速將其重新傳送給另一位消費者。 這樣,即使工作人員偶爾死亡,也可以確保沒有任何資訊丟失。

沒有任何訊息超時;當消費者死亡時,RabbitMQ 將傳遞訊息。即使處理訊息需要很長時間也沒關係。

訊息確認預設關閉。 通過將第四個引數設定為 basic_consume 為 false(true表示不詢問),並在完成任務後向工作人員傳送適當的確認,現在是時候開啟它們了。

$callback = function($msg){
  echo " [x] Received ", $msg->body, "\n";
  sleep(substr_count($msg->body, '.'));
  echo " [x] Done", "\n";
  $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};

$channel->basic_consume('task_queue', '', false, false, false, false, $callback);

使用這段程式碼,我們可以確定,即使在處理訊息時使用 CTRL + C 來殺死一個工作者,也不會丟失任何東西。 工人死後不久,所有未確認的訊息將被重新傳送。

沒有確認?

錯過這一點是常見的錯誤。 這是一個很容易的錯誤,但後果是嚴重的。 當你的客戶退出時(這可能看起來像隨機的重新傳送),訊息將被重新傳遞,但是 RabbitMQ 將會消耗越來越多的記憶體,因為它將不能釋放任何未訊息的訊息。

為了除錯這種錯誤,您可以使用 rabbitmqctl 來列印 messages_unacknowledged 欄位:

sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

On Windows, drop the sudo:

rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged

訊息永續性

我們已經學會了如何確保即使消費者死亡,任務也不會丟失。 但是如果 RabbitMQ 伺服器停止,我們的任務仍然會丟失。

當 RabbitMQ 退出或崩潰時,它會忘記佇列和訊息,除非您告訴它不要。 需要做兩件事來確保訊息不會丟失:我們需要將佇列和訊息標記為持久。

首先,我們需要確保 RabbitMQ 永遠不會失去我們的佇列。 為了做到這一點,我們需要宣佈它是持久的。 為此,我們將第三個引數傳遞給 queue_declare 為 true :

$channel->queue_declare('hello', false, true, false, false);

雖然這個命令本身是正確的,但它在我們目前的設定中不起作用。 那是因為我們已經定義了一個名為 hello 的佇列,這個佇列並不持久。 RabbitMQ 不允許您使用不同的引數重新定義現有的佇列,並會向任何試圖執行該操作的程式返回錯誤。 但是有一個快速的解決方法 - 讓我們宣告一個具有不同名稱的佇列,例如 task_queue :

$channel->queue_declare('task_queue', false, true, false, false);

該標誌設定為 true 並需要應用於生產者和消費者程式碼。

此時我們確信,即使 RabbitMQ 重新啟動,task_queue 佇列也不會丟失。 現在我們需要將訊息標記為持久訊息 - 通過設定 AMQPMessage 作為屬性陣列的一部分所使用的 delivery_mode = 2 訊息屬性。

$msg = new AMQPMessage($data,
       array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT)
       );

關於訊息永續性的說明

將郵件標記為永久郵件並不能完全保證郵件不會丟失。 儘管它告訴 RabbitMQ 將訊息儲存到磁碟,但 RabbitMQ 接收到訊息並且尚未儲存訊息時仍有一段時間視窗。 此外,RabbitMQ 不會為每條訊息執行fsync(2) - 它可能只是儲存到快取中,並沒有真正寫入磁碟。 永續性保證不強,但對我們簡單的任務佇列來說已經足夠了。 如果您需要更強大的保證,那麼您可以使用 publisher confirms

負載均衡

您可能已經注意到排程仍然無法完全按照我們的要求工作。 例如,在有兩名工人的情況下,當所有奇怪的資訊都很重,甚至資訊很少時,一名工作人員會一直很忙,另一名工作人員幾乎不會做任何工作。 那麼, RabbitMQ 不知道任何有關這一點,並仍將均勻地傳送訊息。

發生這種情況是因為 RabbitMQ 只在訊息進入佇列時排程訊息。 它沒有考慮消費者未確認訊息的數量。 它只是盲目地將第 n 條訊息分發給第 n 個消費者。

img

為了解決這個問題,我們可以使用 basic_qos 方法和 prefetch_count = 1 設定。 這告訴 RabbitMQ 一次不要向工作人員傳送多個訊息。 或者換句話說,不要向工作人員傳送新訊息,直到它處理並確認了前一個訊息。 相反,它會將其分派給不是仍然忙碌的下一個工作人員。

$channel->basic_qos(null, 1, null);

有關佇列大小的說明

如果所有的工作人員都很忙,你的隊伍可以填滿。 你會想看看,也許會增加更多的工人,或者有其他的策略。

把它們放在一起

我們的最終程式碼 new_task.php 檔案:

<?php

require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('task_queue', false, true, false, false);

$data = implode(' ', array_slice($argv, 1));
if(empty($data)) $data = "Hello World!";
$msg = new AMQPMessage($data,
                        array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT)
                      );

$channel->basic_publish($msg, '', 'task_queue');

echo " [x] Sent ", $data, "\n";

$channel->close();
$connection->close();

?>

(new_task.php source)

And our worker.php:

<?php

require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('task_queue', false, true, false, false);

echo ' [*] Waiting for messages. To exit press CTRL+C', "\n";

$callback = function($msg){
  echo " [x] Received ", $msg->body, "\n";
  sleep(substr_count($msg->body, '.'));
  echo " [x] Done", "\n";
  $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};

$channel->basic_qos(null, 1, null);
$channel->basic_consume('task_queue', '', false, false, false, false, $callback);

while(count($channel->callbacks)) {
    $channel->wait();
}

$channel->close();
$connection->close();

?>

(worker.php source)

使用訊息確認和預取可以設定工作佇列。 即使 RabbitMQ 重新啟動、永續性選項也可讓任務繼續存在。

現在我們可以繼續閱讀 教程 3 並學習如何向許多消費者傳遞相同的訊息。

相關文章