[译]RabbitMQ教程六(PHP版)--RPC

  • 作者:卡牌
  • 时间:2019-03-05
  • 阅读量:76

RPC

(基于 php-amqplib)

在第二篇教程中,我们学习了如何使用工作队列在多个工作者之间分配耗时的任务。但是如果我们需要在远程计算机上运行一个函数并等待结果呢?嗯,这是一个不同的故事。 此模式通常称为远程过程调用或RPC。

在本教程中,我们将使用RabbitMQ构建RPC系统:客户端和可伸缩的RPC服务器。 由于我们没有任何值得分发的耗时任务,我们将创建一个返回Fibonacci数字的虚拟RPC服务。

客户端接口

为了说明如何使用RPC服务,我们将创建一个简单的客户端类。 它将公开一个名为call的方法,该方法发送一个RPC请求并阻塞,直到收到答案为止:

$fibonacci_rpc = new FibonacciRpcClient();
$response = $fibonacci_rpc->call(30);
echo ' [.] Got ', $response, "\n";

有关RPC的说明

尽管RPC在计算中是一种非常常见的模式,但它经常受到批评。 当程序员不知道函数调用是本地的还是慢的RPC时,会出现问题。 这样的混淆导致系统不可预测,并增加了调试的不必要的复杂性。 错误使用RPC可以导致不可维护的意大利面条代码,而不是简化软件。

考虑到这一点,请考虑以下建议:

  • 确保说明哪个函数调用是本地的,哪个是远程的。
  • 记录您的系统。 使组件之间的依赖关系变得清晰。
  • 处理错误案例。 当RPC服务器长时间停机时,客户端应该如何反应?

回调队列

一般来说,通过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 ...

消息属性:

AMQP 0-9-1协议预定义了一组带有消息的14个属性。 大多数属性很少使用,但以下情况除外:

delivery_mode:将消息标记为持久性(值为2)或瞬态(1)。 你可能还记得第二篇教程中的这个属性

content_type:用于描述编码的mime类型。 例如,对于经常使用的JSON编码,将此属性设置为:application / json是一种很好的做法

reply_to:通常用于命名回调队列。

correlation_id:用于将RPC响应与请求相关联。

correlation_id

在上面介绍的方法中,我们建议为每个RPC请求创建一个回调队列。 这是非常低效的,但幸运的是有更好的方法 - 让我们为每个客户端创建一个回调队列。

这引发了一个新问题,在该队列中收到响应后,不清楚响应属于哪个请求。那是在使用correlation_id属性的时候。我们将为每个请求将其设置为唯一值。稍后,当我们在回调队列中收到消息时,我们将查看此属性,并根据该属性,我们将能够将响应与请求进行匹配。如果我们看到未知的correlation_id值,我们可以安全地丢弃该消息 - 它不属于我们的请求。

您可能会问,为什么我们应该忽略回调队列中的未知消息,而不是因为错误而失败?这是由于服务器端存在竞争条件的可能性。尽管不太可能,但是在向我们发送答案之后,但在发送请求的确认消息之前,RPC服务器可能会死亡。如果发生这种情况,重新启动的RPC服务器将再次处理请求。这就是为什么在客户端上我们必须优雅地处理重复的响应,理想情况下RPC应该是幂等的。

总结

python-six.png

我们的RPC将这样工作:

当客户端启动时,它会创建一个匿名的独占回调队列。

对于RPC请求,客户端发送带有两个属性的消息:reply_to,设置为回调队列,correlation_id,设置为每个请求的唯一值。

请求被发送到rpc_queue队列。

RPC worker(aka:server)正在等待该队列上的请求。 当出现请求时,它会执行作业并使用reply_to字段中的队列将结果返回给客户端。

客户端等待回调队列上的数据。 出现消息时,它会检查correlation_id属性。 如果它与请求中的值匹配,则将响应返回给应用程序。

代码如下:

The Fibonacci task:

function fib($n)
{
    if ($n == 0) {
        return 0;
    }
    if ($n == 1) {
        return 1;
    }
    return fib($n-1) + fib($n-2);
}

我们申明我们的斐波那契函数。 它假定只有有效的正整数输入。 (不要指望这个适用于大数字,并且它可能是最慢的递归实现)。

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_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,
            true,
            false,
            false,
            array(
                $this,
                'onResponse'
            )
        );
    }

    public function onResponse($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设置某种超时?

如果服务器出现故障并引发异常,是否应将其转发给客户端?

在处理之前防止无效的传入消息(例如检查边界,类型)。

文章评论

共有0条评论来说两句吧...

提交
Top