【RabbitMQ】官方文档学习六

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)

# ... and some code to read a response message from the callback_queue ...

关于消息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的调用时幂等的

总体架构

MQ的RPC调用过程

  • 总体的工作流程:
    • 客户端启动时,创建一个匿名的独占回调队列,该队列将接受服务端的处理结果,即response
    • 对一次RPC调用来说,客户端发送的消息需要带有两个属性: reply_tocorrelation_id,前者告诉服务端response发往哪,后者标记唯一一条消息
    • 客户端将消息发送到rpc_queue,该队列由服务端声明,由服务端订阅
    • 当服务端队列中出现消息时,即处理消息并将结果发送至reply_to的匿名队列中去
    • 客户端从匿名队列中获取消息并检查correlation_id是否一致,如果匹配则表明此次通信成功

代码

  • 服务端
# !/usr/bin/env python
# -*- coding: utf-8 -*-
"""
__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()
        # 指定rpc队列,用于服务端消费
        ret = self.chan.queue_declare(queue=rpc_queue)
        self.chan.basic_qos(prefetch_count=1)
        # 指定消费的队列、回调方法,这里auto_ack=False,因为服务端是耗时操作,为确保消息处理的可靠性,这里每次处理完消息后主动发ack进行确认
        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=[])
        # 消息处理完,发送response给调用方,这里指定调用方定义的队列`reply_to`,并将调用方定义的`correlation_id`返回给调用方以验证消息
        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)
  • 客户端
# !/usr/bin/env python
# -*- coding: utf-8 -*-
"""
__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)
        # 从匿名队列获取服务端的回复,这里auto_ack=True,无需手动确认
        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):
        # 回调方法,确认id是否一致
        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())
        # 往rpc队列中发送消息
        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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值