遠程過程調用(RPC)
在第二節裏我們學會了如何使用工作隊列在多個工人中分佈時間消耗性任務。
但如果我們想要運行存在於遠程計算機上的方法並等待返回結果該如何去做呢?這就不太一樣了,這種模式就是常說的遠程過程調用(RPC)。
在本節我們會
在本節我們會使用RabbitMQ創建一個RPC系統:一個客戶端和一個可擴展(scalable)的RPC服務。由於我們沒什麼真正的時間消耗型任務去分配,我們就創建一個擺樣子的RPC服務,它可以返回Fibonacci數。
客戶端接口
爲了闡釋RPC服務如何使用,我們將創建一個簡單的客戶端類,這個類暴露一個叫做call的方法,來發送RPC請求,並等待返回答案:
fibonacci_rpc = FibonacciRpcClient()
result = fibonacci_rpc.call(4)
print("fib(4) is %r" % result)
RPC注意事項
雖然RPC在計算機科學中是一個相當常見的模式,但它也經常飽受批評。但一個程序員沒有弄清楚一個方法調用是本地還是緩慢的RPC,問題就來了。像這種迷惑就會導致一個無法預測的系統並給調試帶來不必要的複雜性,相比較簡化軟件的思想,亂用RPC會帶來難維護的一團糟的代碼
把上面這些問題記在腦裏,考慮以下建議:確認哪個方法調用是本地哪個調用是遠程能夠很明瞭。 使你的系統文檔化,組件之間的依賴很清晰。
應對錯誤情況,當RPC服務宕機很久時客戶端應該如何反應? 什麼時候考慮避免使用 RPC.
如果可以你應該使用異步管線-而不是RPC-把結果推到下一個計算階段。
回調隊列
通常在 RabbitMQ上用RPC都很容易。客戶端發送請求消息,服務端用響應消息迴應。爲了接收響應,客戶端需要用這個請求發送一個 ‘callback’ 隊列的地址。我們試一下:
result = channel.queue_declare(exclusive=True)
callback_queue = result.method.queue
channel.basic_publish(exchange='',
routing_key='rpc_queue',
properties=pika.BasicProperties(
reply_to = callback_queue,
),
body=request)
# ... 此處爲從callback_queue隊列讀取響應消息的代碼 ...
Message properties
AMQP 0-9-1 協議爲一個消息預定義了一組包含14個屬性的集合,大多數屬性都很少使用,除了以下幾種:
delivery_mode: 標記一個消息爲持久的(值爲2)或者暫時的(任何其他值)。你可能記得這個屬性在第二節中。
content_type: 常用來描述編碼的mime-type。例如常用的JSON格式最好把這個屬性設置爲:application/json。
reply_to:通常用來命名一個回調隊列。
correlation_id: 讓請求(requests)關聯到RPC響應(responses)時很有用。
Correlation id
在上面呈現的方法中,我們建議爲每個RPC調用創建一個回調隊列。那會相當沒效率,幸運的是有一種更好的方法——我們每個客戶端創建一個單一回調隊列。
但又出現了一個新的問題,在這個隊列中接收到一個響應,不清楚這個響應屬於哪個請求。這時correlation_id 屬性就派上用場了。我們將爲每個請求設定一個唯一值。稍後當我們從回調隊列收到消息我們會查看這個屬性,基於此我們能把這個響應和一個請求匹配。如果我們發現了一個不認識的correlation_id值,我們可以平安無事地忽略掉這條消息。——它不屬於我們的請求。
你可能會問,爲什麼我們要忽略回調隊列中不認識的消息,而不是提出個錯誤?那是由於服務端會有競爭條件(race condition)的可能。RPC服務在把結果發回來後,但還沒有發送請求的通知消息之前就死掉了,雖然不太可能,但如果發生了,重啓的RPC服務會再次處理之前的請求。所以我們要在客戶端優雅地處理重複的響應,並且RPC應該是等冪的。
總結
我們的RPC工作流程如下:
當客戶端啓動,創建一個匿名的專用的回調隊列。對於一個RPC請求,客戶端發送一個帶有兩個屬性的消息:reply_to,這個設置給回調隊列;correlation_id,這個爲每個請求設置唯一值。
請求發送給 rpc_queue 隊列。
RPC工人 (也叫做服務)等待那個隊列上的請求。當一個請求到達,它完成任務並且發送一個帶有結果的消息返回給客戶端,使用源自reply_to域的隊列。
整合
rpc_server.py完整代碼:
#!/usr/bin/env python
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='rpc_queue')
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
def on_request(ch, method, props, body):
n = int(body)
print(" [.] fib(%s)" % n)
response = fib(n)
ch.basic_publish(exchange='',
routing_key=props.reply_to,
properties=pika.BasicProperties(correlation_id = \
props.correlation_id),
body=str(response))
ch.basic_ack(delivery_tag = method.delivery_tag)
channel.basic_qos(prefetch_count=1)
channel.basic_consume(on_request, queue='rpc_queue')
print(" [x] Awaiting RPC requests")
channel.start_consuming()
rpc_client.py完整代碼:
#!/usr/bin/env python
import pika
import uuid
class FibonacciRpcClient(object):
def __init__(self):
self.connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
self.channel = self.connection.channel()
result = self.channel.queue_declare(exclusive=True)
self.callback_queue = result.method.queue
self.channel.basic_consume(self.on_response, no_ack=True,
queue=self.callback_queue)
def on_response(self, ch, method, props, body):
if self.corr_id == props.correlation_id:
self.response = body
def call(self, n):
self.response = None
self.corr_id = str(uuid.uuid4())
self.channel.basic_publish(exchange='',
routing_key='rpc_queue',
properties=pika.BasicProperties(
reply_to = self.callback_queue,
correlation_id = self.corr_id,
),
body=str(n))
while self.response is None:
self.connection.process_data_events()
return int(self.response)
fibonacci_rpc = FibonacciRpcClient()
print(" [x] Requesting fib(30)")
response = fibonacci_rpc.call(30)
print(" [.] Got %r" % response)
現在我們的RPC服務已經準備好了。我們可以啓動服務:
python rpc_server.py
# => [x] Awaiting RPC requests
在客戶端運行請求一個fibonacci數:
python rpc_client.py
# => [x] Requesting fib(30)
當前的設計並不是RPC服務的唯一實現,但它有一些重要優勢:
如果服務太慢,你只需要再運行一個服務進行擴展。試着在新控制檯再運行一個rpc_server.py。
在客戶端,RPC只需要發送和接收一個消息,不需要像queue_declare這樣的異步調用。這樣RPC客戶端的單個RPC請求只需要一次網絡往返。
我們的代碼已經相當簡化,沒有試圖處理更復雜(但重要)的問題,如:
如果沒有正在運行的服務客戶端該如何反應?
客戶端需要爲RPC設置超時嗎?
如果服務功能故障或拋出異常,它應給被返回給客戶端麼?
在處理之前防止無效的輸入消息。