遠端呼叫 (RPC)
本文基於 官方文件 翻譯
RPC = Remote procedure call
(using php-amqplib)
在第二篇教程中,我們學習瞭如何使用工作佇列在多個工作人員之間分配耗時的任務。
但是如果我們需要在遠端計算機上執行某個功能並等待結果呢? 那麼,這是一個不同的故事。 這種模式通常稱為遠端呼叫(Remote procedure call)。
在本教程中,我們將使用 RabbitMQ 構建一個 RPC 系統:一個客戶端和一個可擴充套件的 RPC 伺服器。 由於我們沒有任何值得分發的耗時任務,我們將建立一個返回斐波那契數字的虛擬 RPC 服務。
客戶端介面
為了說明如何使用 RPC 服務,我們將建立一個簡單的客戶端類。 它將公開一個名為 call 的方法,它傳送一個 RPC 請求並阻塞,直到收到答案:
$fibonacci_rpc = new FibonacciRpcClient();
$response = $fibonacci_rpc->call(30);
echo " [.] Got ", $response, "\n";
有關 RPC 的說明
雖然 RPC 是計算中很常見的模式,但它經常受到批評。 當程式設計師不知道函式呼叫是本地的還是慢速的 RPC 時會出現這些問題。 像這樣的混亂導致了不可預測的系統,並增加了除錯的不必要的複雜性。 而不是簡化軟體,濫用 RPC 會導致不可維護的義大利麵程式碼。
銘記這一點,請考慮以下建議:
- 確保顯而易見哪個函式呼叫是本地的,哪個是遠端的。
- 記錄您的系統,清楚元件之間的依賴關係。
- 處理錯誤情況。 比如,當 RPC 伺服器長時間關閉時,客戶端應該如何反應?
有疑問時避免 RPC。 如果可以的話,你應該使用非同步管道 - 而不是類似於 RPC 的阻塞,結果被非同步推送到下一個計算階段。
回撥佇列
一般來說,通過 RabbitMQ 來執行 RPC 是很容易的。 客戶端傳送請求訊息,伺服器回覆響應訊息。 為了收到回應,我們需要傳送一個 “callback” 佇列地址與請求。 我們可以使用預設佇列。 讓我們試試看:
list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);
$msg = new AMQPMessage(
$payload,
array('reply_to' => $queue_name));
$channel->basic_publish($msg, '', 'rpc_queue');
# ... then code to read a response message from the callback_queue ...
訊息屬性
AMQP 0-9-1 協議預定義了一組包含14個屬性的訊息。 大多數屬性很少使用,但以下情況除外:
- delivery_mode:將訊息標記為持久(值為2)或瞬態(1)。 你可能會記得第二篇教程中的這個屬性。
- content_type:用於描述編碼的MIME型別。 例如,對於經常使用的 JSON 編碼,將此屬性設定為 application/json 是一種很好的做法。
- reply_to:通常用於命名回撥佇列。
- correlation_id:用於將 RPC 響應與請求關聯起來。
相關性 ID (correlation_id)
在上面介紹的方法中,我們建議為每個 RPC 請求建立一個回撥佇列。 這是非常低效的,但幸運的是有一個更好的方法 - 讓我們為每個客戶端建立一個回撥佇列。
這引發了一個新問題,在該佇列中收到回覆後,不清楚回覆屬於哪個請求。 那是什麼時候使用 correlation_id 屬性。 我們將把它設定為每個請求的唯一值。 稍後,當我們在回撥佇列中收到訊息時,我們會檢視該屬性,並基於此屬性,我們將能夠將響應與請求進行匹配。 如果我們看到未知的 correlation_id 值,我們可以放心地丟棄該訊息 - 它不屬於我們的請求。
您可能會問,為什麼我們應該忽略回撥佇列中的未知訊息,而不是因為錯誤而失敗? 這是由於伺服器端可能出現競爭狀況。 儘管不太可能,但在傳送給我們答案之後和傳送請求的確認訊息之前,RPC 伺服器可能會死亡。 如果發生這種情況,重新啟動的 RPC 伺服器將再次處理該請求。 這就是為什麼在客戶端,我們必須優雅地處理重複的響應,理想情況下 RPC 應該是冪等的。
概要
我們的 RPC 會像這樣工作:
- 當客戶端啟動時,它建立一個匿名且獨佔的回撥佇列。
- 對於 RPC 請求,客戶端將傳送具有兩個屬性的訊息:reply_to,該訊息設定為回撥佇列和 correlation_id,該值設定為每個請求的唯一值。
- 該請求會被髮送到 rpc_queue 佇列。
- RPC worker(又名:伺服器)正在等待該佇列上的請求。 當出現請求時,它執行該作業,並使用 reply_to 欄位中的佇列將結果傳送回客戶端。
- 客戶端在回撥佇列中等待資料。 當出現訊息時,它會檢查 correlation_id 屬性。 如果它匹配到來自請求的值,則返回對應用程式的響應。
把它們放在一起
斐波那契任務:
function fib($n) {
if ($n == 0)
return 0;
if ($n == 1)
return 1;
return fib($n-1) + fib($n-2);
}
我們宣告我們的斐波那契函式。 它只假定有效的正整數輸入。 (不要指望這個版本適用於大數字,它可能是最慢的遞迴實現)。
我們的 RPC 伺服器 rpc_server.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('rpc_queue', false, false, false, false);
function fib($n) {
if ($n == 0)
return 0;
if ($n == 1)
return 1;
return fib($n-1) + fib($n-2);
}
echo " [x] Awaiting RPC requests\n";
$callback = function($req) {
$n = intval($req->body);
echo " [.] fib(", $n, ")\n";
$msg = new AMQPMessage(
(string) fib($n),
array('correlation_id' => $req->get('correlation_id'))
);
$req->delivery_info['channel']->basic_publish(
$msg, '', $req->get('reply_to'));
$req->delivery_info['channel']->basic_ack(
$req->delivery_info['delivery_tag']);
};
$channel->basic_qos(null, 1, null);
$channel->basic_consume('rpc_queue', '', false, false, false, false, $callback);
while(count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
?>
伺服器程式碼非常簡單:
- 像往常一樣,我們首先建立連線,通道並宣告佇列。
- 我們可能想要執行多個伺服器程式。 為了在多個伺服器上平均分配負載,我們需要在 $channel.basic_qos 中設定 prefetch_count 設定。
- 我們使用 basic_consume 來訪問佇列。 然後,我們進入 while 迴圈,在其中我們等待請求訊息,完成工作併發迴響應。
我們的 RPC 客戶端 rpc_client.php 的程式碼:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
class FibonacciRpcClient {
private $connection;
private $channel;
private $callback_queue;
private $response;
private $corr_id;
public function __construct() {
$this->connection = new AMQPStreamConnection(
'localhost', 5672, 'guest', 'guest');
$this->channel = $this->connection->channel();
list($this->callback_queue, ,) = $this->channel->queue_declare(
"", false, false, true, false);
$this->channel->basic_consume(
$this->callback_queue, '', false, false, false, false,
array($this, 'on_response'));
}
public function on_response($rep) {
if($rep->get('correlation_id') == $this->corr_id) {
$this->response = $rep->body;
}
}
public function call($n) {
$this->response = null;
$this->corr_id = uniqid();
$msg = new AMQPMessage(
(string) $n,
array('correlation_id' => $this->corr_id,
'reply_to' => $this->callback_queue)
);
$this->channel->basic_publish($msg, '', 'rpc_queue');
while(!$this->response) {
$this->channel->wait();
}
return intval($this->response);
}
};
$fibonacci_rpc = new FibonacciRpcClient();
$response = $fibonacci_rpc->call(30);
echo " [.] Got ", $response, "\n";
?>
我們的 RPC 服務已經準備就緒。 我們可以啟動伺服器:
php rpc_server.php
# => [x] Awaiting RPC requests
執行要請求斐波那契數字的客戶端:
php rpc_client.php
# => [x] Requesting fib(30)
這裡介紹的設計不是 RPC 服務的唯一實現,但它有一些重要的優點:
- 如果 RPC 伺服器速度太慢,可以通過執行另一個來擴充套件。 嘗試在新的控制檯中執行第二個 rpc_server.php。
- 在客戶端,RPC 需要傳送和接收一條訊息。 不需要像 queue_declare 這樣的同步呼叫。 因此,RPC 客戶端僅需要一次網路往返即可獲得單個 RPC 請求。
我們的程式碼仍然非常簡單,不會嘗試解決更復雜(但重要)的問題,比如:
- 如果沒有伺服器在執行,客戶應該如何應對?
- 客戶端是否應該對 RPC 有某種超時?
- 如果伺服器發生故障並引發異常,是否應將其轉發給客戶端?
- 在處理之前防止無效的傳入訊息(例如檢查邊界,型別)。
如果您想進行實驗,您可能會發現管理介面對檢視佇列很有用。
本系列入門教程至此完結