RabbitMQ 入門 - 遠端呼叫 (RPC)

學冰發表於2018-03-28

遠端呼叫 (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 應該是冪等的。

概要

img

我們的 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 有某種超時?
  • 如果伺服器發生故障並引發異常,是否應將其轉發給客戶端?
  • 在處理之前防止無效的傳入訊息(例如檢查邊界,型別)。

如果您想進行實驗,您可能會發現管理介面對檢視佇列很有用。

本系列入門教程至此完結

相關文章