Remote procedure call (RPC)
(using php-amqplib)
In the second tutorial we learned how to use Work Queues to distribute time-consuming tasks among multiple workers.
在第二個指導中,我們學習了如何運用工作隊列在多個worker之間分發耗時的任務。
But what if we need to run a function on a remote computer and wait for the result? Well, that's a different story. This pattern is commonly known as Remote Procedure Call or RPC.
但是假使我們需要運行一個遠端函數並等待其返回結果時怎麼辦?好吧,那是兩碼事。這種模式通常被稱為遠端程序呼叫或者RPC。
In this tutorial we're going to use RabbitMQ to build an RPC system: a client and a scalable RPC server. As we don't have any time-consuming tasks that are worth distributing, we're going to create a dummy RPC service that returns Fibonacci numbers.
這回我們要用RabbitMQ打造一個RPC系統:一個用戶端和一個可擴充的RPC服務端。由於我們沒有什麼值得分發的耗時任務,所以我們將建立一個偽RPC服務,用來返回斐波那契數列。
Client interface(用戶端介面)
To illustrate how an RPC service could be used we're going to create a simple client class. It's going to expose a method named call which sends an RPC request and blocks until the answer is received:
我們將建立一個簡單的用戶端類來闡述RPC服務是如何使用的。這個類將揭示方法調用??發送一個RPC請求並阻塞直至收到回複。
$fibonacci_rpc = new FibonacciRpcClient();$response = $fibonacci_rpc->call(30);echo " [.] Got ", $response, "\n";
A note on RPC(關於RPC需要注意一點)
Although RPC is a pretty common pattern in computing, it's often criticised. The problems arise when a programmer is not aware whether a function call is local or if it's a slow RPC. Confusions like that result in an unpredictable system and adds unnecessary complexity to debugging. Instead of simplifying software, misused RPC can result in unmaintainable spaghetti code.
儘管RPC在電腦運算中很常見,但它十分挑剔。這個問題出現的原因是程式員不知道是否調用一個本地的方法或是否是一個很慢的RPC。這樣的困惑便導致不可預測的系統並增加不必要的調試複雜性。比起簡化的軟體,誤用RPC會導致不可維護的無頭緒代碼。
Bearing that in mind, consider the following advice:
銘記剛才問題,考慮下面的建議:
Make sure it's obvious which function call is local and which is remote.
確保可以明顯的看出哪個方法調用的是本地的哪個是遠端。
Document your system. Make the dependencies between components clear.
系統文檔化。讓組件之間的依賴變得清晰可見。
Handle error cases. How should the client react when the RPC server is down for a long time?
錯誤處理。當RPC服務長時間關閉用戶端該作何反應?
When in doubt avoid RPC. If you can, you should use an asynchronous pipeline - instead of RPC-like blocking, results are asynchronously pushed to a next computation stage.
如果有疑問,則盡量避免使用RPC。如果可以話,你應該使用非同步管道??而不是RPC??像阻塞,結果被非同步推送到下個計算階段。
Callback queue(回調隊列)
In general doing RPC over RabbitMQ is easy. A client sends a request message and a server replies with a response message. In order to receive a response we need to send a 'callback' queue address with the request. We can use the default queue. Let's try it:
一般來講在RabbitMQ上搞RPC很容易??用戶端發送請求訊息,服務端回複響應訊息。為了收到響應(訊息)我們得在發送請求時附帶一個回調隊列地址。可以試試預設隊列:
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 ...
Message properties(訊息屬性)
The AMQP protocol predefines a set of 14 properties that go with a message. Most of the properties are rarely used, with the exception of the following:
AMQP協議預定義了14個訊息屬性。大部分屬性都很少用到,下面的除外:
delivery_mode: Marks a message as persistent (with a value of 2) or transient (1). You may remember this property from the second tutorial.
delivery_mode:設定為2表示持久化,1為臨時的。你可能還記得我們在第二節指導的時候使用過。
content_type: Used to describe the mime-type of the encoding. For example for the often used JSON encoding it is a good practice to set this property to: application/json.
content_type:用來表述編碼mime-type,例如常用的JSON編碼,良好的做法是設定這個屬性為:application/json
reply_to: Commonly used to name a callback queue.
reply_to:常用作回調隊列名。
correlation_id: Useful to correlate RPC responses with requests.
correlation_id: 用來關聯RPC的請求與響應。
Correlation Id(關聯ID)
In the method presented above we suggest creating a callback queue for every RPC request. That's pretty inefficient, but fortunately there is a better way - let's create a single callback queue per client.
上面的方法我們暗示為每個RPC請求建立一個回調隊列。這非常低效,但幸運的是,有一種更好的方式??我們可以為每個用戶端只建立一個回調隊列。
That raises a new issue, having received a response in that queue it's not clear to which request the response belongs. That's when the correlation_id property is used. We're going to set it to a unique value for every request. Later, when we receive a message in the callback queue we'll look at this property, and based on that we'll be able to match a response with a request. If we see an unknown correlation_id value, we may safely discard the message - it doesn't belong to our requests.
可這將帶來新的問題,隊列收到一個響應時,我們不清楚它屬於哪個請求。這正是correlation_id發揮作用的地方。我們將為每一個請求設定一個唯一correlation_id。之後,當在回調隊列中收到(響應)訊息的時候我們來查看這個屬性,基於它我們就可以把請求和響應進行匹配。要是我們檢測到未知correlation_id,可能會安全丟棄這條訊息??因為它不屬於我們的請求嘛。
You may ask, why should we ignore unknown messages in the callback queue, rather than failing with an error? It's due to a possibility of a race condition on the server side. Although unlikely, it is possible that the RPC server will die just after sending us the answer, but before sending an acknowledgment message for the request. If that happens, the restarted RPC server will process the request again. That's why on the client we must handle the duplicate responses gracefully, and the RPC should ideally be idempotent.
你可能會問,為喵我們該忽略回調隊列中的未知訊息呢,而不是置為處理失敗並返回一個錯誤?是因為存在服務端紊亂的可能性。儘管幾率很小,可還是有可能??RPC服務在給我們發送完響應後宕掉,但還沒來得進行訊息確認。那樣的話,重啟的RPC服務會再次處理這個請求。 這也就是為喵用戶端必須優雅地處理重複響應,而RPC服務最好的等冪的。
Summary
Our RPC will work like this:
RPC運行流程:
When the Client starts up, it creates an anonymous exclusive callback queue.
當用戶端啟動,便將建立一個匿名的專用回調隊列。
For an RPC request, the Client sends a message with two properties: reply_to, which is set to the callback queue and correlation_id, which is set to a unique value for every request.
RPC請求中,用戶端訊息發送帶有兩個屬性:reply_to用來設定回調隊列,和correlation_id用來唯一標示每一個請求。
The request is sent to an rpc_queue queue.
請求被發送到一個叫做rpc_queue的隊列。
The RPC worker (aka: server) is waiting for requests on that queue. When a request appears, it does the job and sends a message with the result back to the Client, using the queue from the reply_to field.
RPC worker(也叫:服務)在rpc_queue上守護,等待請求。每當來一個請求,它會進行處理並將結果以訊息形式發送到用戶端,經由reply_to指定的隊列。
The client waits for data on the callback queue. When a message appears, it checks the correlation_id property. If it matches the value from the request it returns the response to the application.
用戶端在回調隊列中等待資料。當出現一條訊息,就會檢查correlation_id屬性。如果和請求時的correlation_id匹配,便會將響應返回給應用程式。
Putting it all together(合體!!!終極~\(???)/~啦啦啦!)
The Fibonacci task:
斐波那契任務:
function fib($n) {
if ($n == 0) return 0; if ($n == 1) return 1; return fib($n-1) + fib($n-2);}
We declare our fibonacci function. It assumes only valid positive integer input. (Don't expect this one to work for big numbers, and it's probably the slowest recursive implementation possible).
聲明斐波那契函數。假定它只接收正整數。(別指望它能處理很大的數字,它可能是最慢的遞迴實現)
The code for our RPC server rpc_server.php looks like this:
rpc_server.php代碼:
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();?>
The server code is rather straightforward:
服務端代碼相當直白:
As usual we start by establishing the connection, channel and declaring the queue.
一如既往先建立串連,頻道,然後聲明隊列。
We might want to run more than one server process. In order to spread the load equally over multiple servers we need to set the prefetch_count setting in $channel.basic_qos.
沒準我們想運行多個服務端進程。為了能在多個服務之間均等負載,我們得在$channel.basic_qos中設定prefech_count。
We use basic_consume to access the queue. Then we enter the while loop in which we wait for request messages, do the work and send the response back.
使用basic_consume訪問隊列。然後進入while迴圈等待請求訊息,處理訊息,返迴響應資訊。
The code for our RPC client rpc_client.php:
rpc_client.php代碼:
connection = new AMQPConnection( '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";?>
Now is a good time to take a look at our full example source code for rpc_client.php and rpc_server.php.
是時候看看整個例子的源碼了rpc_client.php and rpc_server.php.
Our RPC service is now ready. We can start the server:
RPC服務準備就緒,啟動!
$ php rpc_server.php [x] Awaiting RPC requests
To request a fibonacci number run the client:
運行用戶端請求一個斐波那契數字:
$ php rpc_client.php [x] Requesting fib(30)
The design presented here is not the only possible implementation of a RPC service, but it has some important advantages:
這裡展現的設計不是RPC服務的唯一可能實現,但它卻有一些重要的優勢:
If the RPC server is too slow, you can scale up by just running another one. Try running a second rpc_server.php in a new console.
如果RPC服務太慢,你可以按比例增加運行其數量。試試在新控制台運行第二個rpc_server.php服務
On the client side, the RPC requires sending and receiving only one message. No synchronous calls like queue_declare are required. As a result the RPC client needs only one network round trip for a single RPC request.
在用戶端,RPC要求只發送和接收一條訊息。必須沒有像隊列聲明一樣的同步調用。結果呢,對於單一的RPC請求,用戶端僅需要一個網路往返。
Our code is still pretty simplistic and doesn't try to solve more complex (but important) problems, like:
我們的代碼還是忒簡化,並沒有想解決更為複雜(但重要)的問題,像:
How should the client react if there are no servers running?
要是沒有服務端在守護,用戶端作何反應?
Should a client have some kind of timeout for the RPC?
RPC用戶端是不是該有一些逾時設定?
If the server malfunctions and raises an exception, should it be forwarded to the client?
如果服務端障礙引發異常,是否該把它發送至用戶端呢?
Protecting against invalid incoming messages (eg checking bounds, type) before processing.
處理前防止無效訊息的進入(例如:檢查界限,類型)
If you want to experiment, you may find the rabbitmq-management plugin useful for viewing the queues.
想嘗試嗎? rabbitmq-management plugin 這裡你可能會發現一些有用的外掛程式來查看隊列。