-
业务场景:
在消费到rabbiMQ消息后,需要做业务操作,最后确认消息。由于业务代码处理数据量比较大(几十万),导致消息在确认时连接已被sever断开,报错:RabbitMQ Error: fwrite(): send of 12 bytes failed with errno=104 Connection reset by peer -
原因分析:
在查看其他开发人员面临的类似问题并深入研究了AMQP库的源码之后,原因变的明确;我们已根据RabbitMQ最佳实践使用心跳(heartbeats),但根本没有发送心跳。
RabbitMQ使用'heatbeats'作为保持连接的机制,客户端发送心跳到服务器端确保连接仍然存在并且正在运行。如果在连续两个心跳都没有生命迹象,则服务器将断开,因为它假定客户端已完成操作,心跳间隔可以为每个连接配置。
但是,由于php是一种同步语言,所以AMQP库无法在长时间运行的任务中继续发送心跳信号,如果处理时间超过了心跳,服务器端会断开连接,并且客户端只有在尝试继续使用队列时才会发现,此时就会抛出异常。 -
php-amqp 如何处理心跳
查看源码发现php-amqp库通过AbstractIO::check_heartbeat()方法来处理心跳,每次使用连接时,都会调用此IO方法ex:AMQPChannel::basic_consume()
,AMQPChannel::queue_declare()
, orAMQPChannel::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() 方法确认自上次使用连接以来过去了多长时间,如果这个时间超过两个心跳的间隔,连接将关闭,或者这个时间超过心跳的一半将会发送心跳。
- 解决方案
快速解决方案:断开重连
如果我们不关心重连带来的影响,那我们可以直接重新打开连接即可,直接调用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()方法被欺骗来发送心跳