rabbitMQ Connection reset by peer的常见原因及解决办法

  1. 业务场景
    在消费到rabbiMQ消息后,需要做业务操作,最后确认消息。由于业务代码处理数据量比较大(几十万),导致消息在确认时连接已被sever断开,报错:RabbitMQ Error: fwrite(): send of 12 bytes failed with errno=104 Connection reset by peer

  2. 原因分析:
    在查看其他开发人员面临的类似问题并深入研究了AMQP库的源码之后,原因变的明确;我们已根据RabbitMQ最佳实践使用心跳(heartbeats),但根本没有发送心跳。
    RabbitMQ使用'heatbeats'作为保持连接的机制,客户端发送心跳到服务器端确保连接仍然存在并且正在运行。如果在连续两个心跳都没有生命迹象,则服务器将断开,因为它假定客户端已完成操作,心跳间隔可以为每个连接配置。
    但是,由于php是一种同步语言,所以AMQP库无法在长时间运行的任务中继续发送心跳信号,如果处理时间超过了心跳,服务器端会断开连接,并且客户端只有在尝试继续使用队列时才会发现,此时就会抛出异常。

  3. php-amqp 如何处理心跳
    查看源码发现php-amqp库通过AbstractIO::check_heartbeat()方法来处理心跳,每次使用连接时,都会调用此IO方法ex:AMQPChannel::basic_consume() , AMQPChannel::queue_declare() , or AMQPChannel::basic_publish()
     

    /**
         * Heartbeat logic: check connection health here
         * @return void
         * @throws \PhpAmqpLib\Exception\AMQPRuntimeException
         */
        public function check_heartbeat()
        {
            // ignore unless heartbeat interval is set
            if ($this->heartbeat !== 0 && $this->last_read && $this->last_write) {
                $t = microtime(true);
                $t_read = round($t - $this->last_read);
                $t_write = round($t - $this->last_write);
    
                // server has gone away
                if (($this->heartbeat * 2) < $t_read) {
                    $this->close();
                    throw new AMQPHeartbeatMissedException('Missed server heartbeat');
                }
    
                // time for client to send a heartbeat
                if (($this->heartbeat / 2) < $t_write) {
                    $this->write_heartbeat();
                }
            }
        }

    如果配置了心跳,check_heartbeat() 方法确认自上次使用连接以来过去了多长时间,如果这个时间超过两个心跳的间隔,连接将关闭,或者这个时间超过心跳的一半将会发送心跳。

  4. 解决方案
    快速解决方案:断开重连
    如果我们不关心重连带来的影响,那我们可以直接重新打开连接即可,直接调用reconnect()
    更好的解决方案:手动发送心跳
    function callback(AMQPMessage $message): void
    {
      try {
        do_something_that_may_take_a_while();
        send_heartbeat();
        do_something_else_that_may_take_a_while();
        ack_message($message);
      } catch (\Exception $e) {
        nack_message($message);
      }
    }

    这个解决方案很简单,直接调用前面提到的check_heartbeat()似乎是个正确的方案,但是这个方法有自己的规则确定是否应该发送心跳,在了解过该方法的功能并进程测试之后,我发现只有在通过与RabbitMQ服务器的socket 连接交换消息时才会触发心跳,这就意味着必须进行socket 上的写入或读取操作,才能使check_heartbeat() 成功运行。
     

    function send_heartbeat() use ($connection): void
    {
      $connection->getIO()->read(0);
    }

    通过socket上的0字节读取(队列实际上没有消耗任何字节),check_heartbeat()方法被欺骗来发送心跳

 

你可能感兴趣的:(问题解决)