link: Remote procedure call (RPC)
简介
当我们需要运行一个远程的方法并且等待其返回结果时使用RPC(Remote Procedure Call) 可以使用MQ来实现一个简单的RPC系统: 一个客户端和一个可伸缩的
服务端
关于RPC的建议
尽管RPC在计算机领域已经是一种很常见的调用模式了,但还是有很多争议的,比如,在错误排查的时候不太容易知道是本地方法出错还是远程方法调用出错,增加了debug的复杂性,此外,相比于简单的本地调用RPC还增加了代码的复杂性 一些建议
明确哪些方法是本地调用哪些方法是远程调用 厘清代码之间的依赖关系 异常处理,当服务端宕机时客户端应当如何处理? 当不确定时,不要使用RPC。如果可以,请使用异步管道代替阻塞的RPC调用,将结果异步的推送到下一个处理流程
回调队列
使用MQ实现RPC比较简单,客户端只需发送request
服务端回复一个response
即可 为了能够接收到服务端
返回的response
,客户端在发送请求request
的时候需要附带上回调队列
地址
result = channel. queue_declare( queue= '' , exclusive= True )
callback_queue = result. method. queue
channel. basic_publish( exchange= '' ,
routing_key= 'rpc_queue' ,
properties= pika. BasicProperties(
reply_to = callback_queue,
) ,
body= request)
关于消息properties
AMQP 0.9.1 协议定义了14个消息相关的属性,其中大部分是很少用到的,除了以下4个
delivery_mode
: 标记消息是否持久化,value=2
时持久化,其他值不持久化content_type
: 描述mime-type
,比如使用JSON
编码时应该设置为application/json
reply_to
: 回调队列名称correlation_id
: 唯一ID,标记唯一一条消息,可用于防止消息重复消费
关于correlation_id
理论上可以给每一次RPC请求建立一个回调队列,但这样做太低效了 所有请求使用同一个回调队列带来的问题是,客户端无法确定服务端返回的response
对应哪一条reqeust
,因此我们给每一条请求分配一个全局唯一的correlation_id
来标记一条消息,当我们发现一条未知的correlation_id
时可以直接将其丢弃 为什么将未知的correlation_id
丢弃?什么情况下队列里会出现未知的correlation_id
?考虑一种情况,当服务端处理消息并将结果发回给客户端,并且在发送ack
消息之前宕机了,此时消息会重新回到服务端的队列,当然服务端被重启之后,会重复消费此消息,并将其返回给调用方(客户端)。这就是为什么客户端需要合理的处理重复消息,保证RPC的调用时幂等的
总体架构
总体的工作流程:
客户端启动时,创建一个匿名的独占回调队列
,该队列将接受服务端的处理结果,即response
对一次RPC调用来说,客户端发送的消息需要带有两个属性: reply_to
和correlation_id
,前者告诉服务端response
发往哪,后者标记唯一一条消息 客户端将消息发送到rpc_queue
,该队列由服务端声明,由服务端订阅 当服务端队列中出现消息时,即处理消息并将结果发送至reply_to
的匿名队列中去 客户端从匿名队列中获取消息并检查correlation_id
是否一致,如果匹配则表明此次通信成功
代码
"""
__title__ = ''
__author__ = 'wAIxi'
__date__ = '2020/8/21'
__description__ = doc description
"""
import dataclasses
import json
from typing import Callable
import pika
from RabbitMQ. _code. rpc. conf import Config, Code, RpcBody
class RpcServer :
def __init__ ( self, rpc_queue) :
credential = pika. PlainCredentials( username= Config. user, password= Config. password)
conn_params = pika. ConnectionParameters(
host= Config. host,
port= Config. port,
credentials= credential,
virtual_host= Config. virtual_host
)
self. conn = pika. BlockingConnection( conn_params)
self. chan = self. conn. channel( )
ret = self. chan. queue_declare( queue= rpc_queue)
self. chan. basic_qos( prefetch_count= 1 )
self. chan. basic_consume( queue= ret. method. queue, on_message_callback= self. callback, auto_ack= False )
self. call = None
def callback ( self, ch, method, props, body) :
print ( "start run time-consuming service" )
args = json. loads( body)
print ( f"body is: {json.dumps(args, ensure_ascii=False)}" )
if isinstance ( self. call, Callable) :
r = self. call( args)
ret = RpcBody( id = props. correlation_id, data= r)
else :
ret = RpcBody( id = props. correlation_id, code= Code. ERROR. value, message= "method is not callable" , data= [ ] )
self. chan. basic_publish( exchange= '' , routing_key= props. reply_to,
body= json. dumps( dataclasses. asdict( ret) , ensure_ascii= False ) ,
properties= pika. BasicProperties(
correlation_id= props. correlation_id,
content_type= "application/json"
) )
self. chan. basic_ack( delivery_tag= method. delivery_tag)
def run_with_callable ( self, method) :
print ( "start consuming..." )
self. call = method
self. chan. start_consuming( )
def fib ( n) :
if n == 0 :
return 0
elif n == 1 :
return 1
else :
return fib( n - 1 ) + fib( n - 2 )
s = RpcServer( rpc_queue= "rpc_queue" )
s. run_with_callable( fib)
"""
__title__ = ''
__author__ = 'wAIxi'
__date__ = '2020/8/21'
__description__ = doc description
"""
import json
import uuid
import pika
from RabbitMQ. _code. rpc. conf import Config
class RpcClient :
def __init__ ( self) :
credential = pika. PlainCredentials( username= Config. user, password= Config. password)
conn_params = pika. ConnectionParameters(
host= Config. host,
port= Config. port,
credentials= credential,
virtual_host= Config. virtual_host
)
self. conn = pika. BlockingConnection( conn_params)
self. chan = self. conn. channel( )
ret = self. chan. queue_declare( queue= "" , exclusive= True )
self. queue = ret. method. queue
self. chan. basic_qos( prefetch_count= 1 )
self. chan. basic_consume( queue= self. queue, on_message_callback= self. callback, auto_ack= True )
self. response = None
self. correlation_id = None
def callback ( self, ch, method, props, body) :
if self. correlation_id == props. correlation_id:
self. response = body
def call ( self, body, routing_key= 'rpc_queue' ) :
self. response = None
self. correlation_id = None
self. correlation_id = str ( uuid. uuid4( ) )
self. chan. basic_publish( exchange= '' , routing_key= routing_key,
body= json. dumps( body, ensure_ascii= False ) ,
properties= pika. BasicProperties(
correlation_id= self. correlation_id,
reply_to= self. queue
) )
while self. response is None :
self. conn. process_data_events( )
return json. loads( self. response)
c = RpcClient( )
r = c. call( 6 )
print ( r)