网络与连接——心跳、连接恢复与排障
RabbitMQ 系列 · 第 14/22 篇
上一篇:《AMQP 1.0 与多协议——MQTT、STOMP、Stream》
下一篇预告:《RabbitMQ 安全——认证、授权与 TLS》
开头:连接半夜被掐断,消息发不出去
线上有个现象很常见:白天一切正常,到了半夜流量低谷,部分应用连不上 RabbitMQ,日志里一片 I/O 异常。这类问题九成不是 RabbitMQ 的 bug,而是网络层——心跳、空闲超时、NAT/防火墙、文件句柄——没配对。
本篇对照官方 Networking 与 Heartbeats 两篇文档(截至 RabbitMQ 4.3),把端口、心跳、连接恢复、代理/负载均衡、连接数调优、常见排障串成一条线。
一、端口一览:哪些端口要开
RabbitMQ 节点会绑定一组端口来监听客户端、集群节点和 CLI 工具。先记住这张表,配防火墙时照着放行:
| 端口 | 协议 / 用途 | 是否默认开启 |
|---|---|---|
| 5672 | AMQP 0-9-1 与 AMQP 1.0(明文) | ✅ |
| 5671 | AMQP 0-9-1 与 AMQP 1.0 over TLS(amqps) | 需配 TLS |
| 15672 | HTTP API、管理 UI、rabbitmqadmin(明文) | 装管理插件 |
| 15671 | 同上 over TLS | 装管理插件 + TLS |
| 25672 | 节点间通信 + CLI 工具(Erlang distribution) | ✅ |
| 4369 | epmd,节点与 CLI 的端口发现服务 | ✅ |
| 35672–35682 | CLI 工具与节点通信的客户端端口段 | ✅ |
| 1883 / 8883 | MQTT 明文 / TLS | 装 MQTT 插件 |
| 61613 / 61614 | STOMP 明文 / TLS | 装 STOMP 插件 |
| 5552 / 5551 | Stream 协议明文 / TLS | 装 Stream 插件 |
| 6000–6500 | Stream 复制 | Stream 场景 |
| 15674 / 15675 | STOMP-over-WebSockets / MQTT-over-WebSockets | 对应插件 |
| 15692 / 15691 | Prometheus 指标明文 / TLS | 装 Prometheus 插件 |
几个关键记忆点:
- 25672 = 5672 + 20000:节点间通信端口由 AMQP 端口加 20000 算出。
- 35672–35682 = 25672 + 10000 起 10 个端口:CLI 工具用的客户端端口段,缩成单端口会导致没法并发跑多个
rabbitmqctl。 - 25672、4369 只对内网放行,别暴露到公网。
- epmd(4369):节点启动时自动拉起,作用是「告诉对方我这个节点在 25672 上」。改了端口或地址后,必须先
epmd -kill再重启节点。
防火墙规则的核心:对外只开客户端要用的那个端口(通常 5672/5671),节点间端口只放行集群各节点 IP 和运维机。
监听接口用 listeners.tcp.* 控制,默认监听所有网卡的 5672。要限制只监听某块网卡:
# 只在 192.168.1.99 上监听 AMQP
listeners.tcp.1 = 192.168.1.99:5672
# 只监听本地回环(IPv4 + IPv6)
listeners.tcp.1 = 127.0.0.1:5672
listeners.tcp.2 = ::1:5672
# 完全关闭明文,只允许 TLS 连接
listeners.tcp = none
listeners.ssl.default = 5671需要临时禁止新连接(比如做维护),可以暂停监听器——已建立的连接不受影响,这是节点进入维护模式的步骤之一:
rabbitmqctl suspend_listeners # 暂停:不再接受新连接
rabbitmqctl resume_listeners # 恢复:重新接受新连接
rabbitmqctl suspend_listeners -n rabbit@node2.cluster.svc # 对指定节点操作二、心跳(Heartbeats):连接的「还活着吗」
2.1 为什么需要心跳
网络会以各种方式出问题,有的还很隐蔽(比如高丢包率)。Linux 默认配置下,操作系统要花大约 11 分钟才能发现一条 TCP 连接已经断了——生产者可能对着死连接发了 10 分钟消息,全丢在半路。AMQP 0-9-1 的心跳机制让应用层尽快发现死连接,顺带防住一类「会杀空闲连接」的网络设备(NAT、负载均衡器、防火墙)。
2.2 超时值与协商
心跳有一个核心参数——心跳超时值(heartbeat timeout),单位秒,RabbitMQ 端默认建议 60 秒。这个值在连接建立时由客户端和服务端协商确定:
- 服务端给出自己的建议值(默认 60)
- 客户端拿自己的配置值与之比对
- 官方 Java / .NET / Erlang 客户端的协商规则:
| 双方值 | 最终取值 |
|---|---|
任一方为 0(建议关闭) | 取较大的那个 |
| 双方都非 0 | 取较小的那个 |
换句话说:客户端只能把心跳调小,不能调大(只要服务端非 0)。想让心跳生效,客户端必须主动「请求」一个非 0 值。
2.3 心跳帧与「死」的判定
实际发送频率约为 timeout / 2 秒,这个值叫心跳间隔(heartbeat interval)。判定逻辑:大约每 timeout / 2 秒发一次心跳帧,连续两次没收到回应(即 timeout × 2 内无任何流量),就认为对方不可达,连接被关闭。
关键:任何流量都算心跳——发布消息、消费回 Ack、协议操作,都算「这连接还活着」。心跳帧只是「没别的话说时」补的空帧。所以高吞吐连接几乎永远不会有真正的心跳超时。注意别把 timeout(超时值) 和 interval(间隔值) 搞混——RabbitMQ 和官方客户端暴露的都是 timeout。
2.4 为什么千万别设 0
两端都设 0 才会真正关闭心跳,但官方强烈不建议:关了之后发现死连接只能靠操作系统(约 11 分钟)或 TCP keepalive(默认甚至能拖到 1 小时以上),对发布者尤其危险——连接断了你还不知道,消息全部石沉大海。如果确实不想用心跳,必须保证每台机器都正确配置了 TCP keepalive 并设了足够短的检测周期,否则别动这个念头。非要用「几乎关闭」的方式,可以在两端都设一个很大的值(比如 1800 秒)。
2.5 值太小:误判(false positive)
把心跳设得太低会误判——对方其实好好的,只是短暂网络抖动或服务端流控,就被当成死了。官方经验:
| 心跳值区间 | 风险 |
|---|---|
| < 5 秒 | 很容易误判 |
| ≤ 1 秒 | 几乎必然误判 |
| 5–20 秒 | 大多数环境最优 |
| 60 秒(默认) | 通用安全值 |
选值要权衡:越低发现死连接越快,但越容易误杀。5–20 秒是兼顾两者的甜点区间。
2.6 客户端怎么配
Java / .NET 客户端在创建连接前设置请求心跳(服务端默认非 0 时,客户端只能调低):
ConnectionFactory cf = new ConnectionFactory();
cf.setRequestedHeartbeat(60); // 请求 60 秒心跳MQTT 里心跳叫 keepalive,STOMP 1.2 用 heart-beat 头,机制都一样,只是名字不同。
三、连接恢复:客户端自动重连与拓扑恢复
3.1 官方客户端的自动恢复
心跳只能告诉你「这条连接死了」,真正的恢复要靠客户端自己重连。官方 Java / .NET 客户端提供自动连接恢复(Automatic Recovery),默认开启,包含两个阶段:
| 阶段 | 做什么 |
|---|---|
| 连接恢复(Connection Recovery) | 按指数退避重试连接服务端 |
| 拓扑恢复(Topology Recovery) | 重连成功后,重新声明 exchange / queue、重建 binding、恢复 consumer |
也就是说,网络抖一下、节点重启一下,客户端会自动把连接、信道、消费者都「补回来」,应用代码大多无感知。
3.2 与 auto-delete + 固定名队列的竞态
拓扑恢复有个经典坑,正好呼应《第 4 篇 · 队列核心概念》里讲过的 auto-delete 队列:
- 你声明了一个 auto-delete + 固定名字 的队列
- 消费者断开时,队列因「最后一个消费者离开」被自动删除
- 客户端拓扑恢复时又用同样的名字去声明它 → 报错:队列已不存在 / 参数不一致
建议:auto-delete 队列尽量用服务端生成名字(
queueDeclare().getQueue()),或确保恢复时声明参数完全一致。生产环境优先用 Quorum 仲裁队列,它不支持 exclusive、不会因消费者离开而消失,恢复路径更干净。
3.3 客户端恢复相关参数
以 Java 客户端为例,几个可调项:
ConnectionFactory cf = new ConnectionFactory();
cf.setAutomaticRecoveryEnabled(true); // 默认 true
cf.setTopologyRecoveryEnabled(true); // 默认 true,恢复 exchange/queue/binding/consumer
cf.setNetworkRecoveryInterval(5000); // 重试间隔(ms),默认 5000
cf.setRequestedHeartbeat(60); // 心跳 60s自动恢复默认开,别手贱关掉。关了就得自己在应用层处理所有重连逻辑,绝大多数团队没必要。
四、NAT、代理与负载均衡
生产环境客户端很少直连 RabbitMQ,中间往往隔着负载均衡器(HAproxy、AWS ELB/ALB、Nginx)或 NAT 网关。这些中间层会带来两类典型问题。
4.1 空闲连接被杀(最经典的坑)
很多代理/NAT 会杀掉「空闲」TCP 连接——一段时间没流量就单方面掐断。表现就是开头说的「半夜连接断」。
心跳恰好是这事的对症药:心跳帧周期性产生流量,让中间层觉得这连接「还活着」。心跳 30 秒时大约每 15 秒就有一次流量,足以满足大多数代理和 LB 的默认空闲阈值(通常 5–15 秒级别)。
铁律:心跳间隔必须小于 NAT/代理的空闲超时。 比如 AWS NLB 默认 idle timeout 350 秒,你心跳设 60 秒就稳;但如果你自己用 HAproxy 配了
timeout tunnel 30s,心跳 60 秒就会被杀,得把心跳调到 20 秒以内,或者把代理超时调大。
4.2 Proxy Protocol:让服务端看到真实客户端 IP
经过代理后,RabbitMQ 看到的连接源 IP 全是代理的 IP,排查问题很不方便。Proxy Protocol(支持 v1 文本 / v2 二进制)就是解决这个:代理在连接握手时带上客户端真实 IP,RabbitMQ 就能在管理 UI / CLI 里显示来源。
# 为 AMQP 0-9-1 / 1.0 开启 proxy protocol
proxy_protocol = trueProxy Protocol 是全有或全无的——开了之后,不支持的客户端直连会失败,所有连接必须都经过支持该协议的代理。HAproxy、AWS ELB 都支持,开启方式见各自文档。MQTT、STOMP、Web STOMP、Web MQTT 各自有独立的开关。
4.3 直连 vs 经过 LB
| 方式 | 适合 | 注意 |
|---|---|---|
| 直连某个节点 | 开发 / 单节点 | 节点挂了应用要自己切 |
| 经过 LB | 生产多节点 | LB 可能成吞吐瓶颈;注意 idle timeout 与心跳配合 |
代理多一跳延迟、可能成为带宽争用点,吞吐敏感场景代理带宽要留足并监控。
4.4 连接握手超时
连接建立有握手阶段,默认超时 10 秒。客户端在受限网络(弱网、TLS 握手慢)下可能不够,可以调大:
# 连接握手超时(毫秒),默认 10000
handshake_timeout = 20000
# TLS 握手超时(毫秒)
ssl_handshake_timeout = 10000正常环境如果频繁触发握手超时,多半是别处出了问题(DNS 解析慢、TCP 握手慢),别急着只调这个值。
五、连接数调优:撑住海量并发
IoT、传感器这类场景,每个节点要扛成千上万甚至几十万条并发连接,每条流量都很小。这时候瓶颈往往不是吞吐,而是「能开多少条连接」。几个关键限制因素:
5.1 文件句柄上限
每条 TCP 连接占一个文件句柄。系统对单进程的句柄数有限制,到顶了节点就不再接受新连接。
# 查看当前 Erlang VM 的 port 上限
rabbitmqctl eval 'erlang:system_info(port_limit).'- 粗估:目标连接数 × 1.5 作为句柄上限。比如 10 万连接,上限至少 15 万。
- 调大系统句柄上限时,必须同步调
ERL_MAX_PORTS环境变量(默认 65536),否则 Erlang 运行时仍会被自己的上限卡住。 - Linux 用
sysctl fs.file-max和 systemd 的LimitNOFILE调;Windows 只用ERL_MAX_PORTS。
5.2 每连接内存:TCP 缓冲区
4.1 起,连接会根据消息速率和大小自动调整 TCP 缓冲区,手动设的值只在连接最早期生效,重要性比旧版本低很多。
要扛最大连接数,可以调小缓冲区省内存(代价是吞吐下降);反过来要冲吞吐就调大。收发缓冲区要设一样大,低于 8 KiB 不推荐:
tcp_listen_options.sndbuf = 32768 # 32 KiB(省内存)
tcp_listen_options.recbuf = 327685.3 每连接信道数
信道(channel)也吃内存。可以用 channel_max 给每条连接的信道数封顶。不过大多数应用一条连接用不了几个信道,超过 200 都很罕见,按需调即可。
5.4 统计采集间隔
海量连接下,每个连接/信道/队列都会周期性产生指标事件,默认每 5 秒采集一次,空闲连接也会吃 CPU。调大采集间隔能显著降负载:
# 采集间隔改为 60 秒(默认 5000ms)
collect_statistics_interval = 60000代价是管理 UI 里的指标刷新变慢(最长 60 秒),有外部监控系统的生产环境完全可以接受。
5.5 连接 backlog
并发连接多、又遇集中重连(比如网络恢复后几万客户端一起重连),未完成的 TCP 握手会排队。队列长度由 tcp_listen_options.backlog 控制,默认只有 128,高并发建议调到 4096+:
tcp_listen_options.backlog = 4096
tcp_listen_options.nodelay = true同时内核的 net.core.somaxconn 也要相应调大(默认也是 128)。
六、高连接抖动(Connection Churn)
「抖动」指应用频繁打开又关闭连接——比如每次发消息都 new 一条连接、发完就关。这比「连接数多」更危险,会快速耗尽句柄、Erlang 进程、临时端口等资源,最终节点拒绝新连接。
6.1 TIME_WAIT 堆积
TCP 关闭后连接会进入 TIME_WAIT 状态(防止旧连接的重传包串到新连接上),在繁忙系统上可能停留数十秒到数分钟,堆积起来就爆。内核可调:
# 缩短 TIME_WAIT 停留(默认约 60s,建议 30s)
net.ipv4.tcp_fin_timeout = 30
# 允许复用 TIME_WAIT 的 socket(仅在不使用 NAT 时安全!)
net.ipv4.tcp_tw_reuse = 1
tcp_tw_reuse=1在走 NAT 的环境不安全,会引发难定位的问题。客户端机器或代理机上可以考虑开,节点上谨慎。
6.2 TCP Keepalive 配合心跳
高抖动场景下,光靠心跳检测死连接不够快,建议心跳 + TCP keepalive 一起用。Linux 内核参数(名字带 ipv4 但对 IPv6 也生效):
# 连接空闲 30 秒后开始探测
net.ipv4.tcp_keepalive_time = 30
# 每 10 秒探一次
net.ipv4.tcp_keepalive_intvl = 10
# 探 4 次没回应就判死(30 + 10×4 = 70 秒)
net.ipv4.tcp_keepalive_probes = 4默认的 TCP keepalive 检测周期能拖到 1 小时以上,默认值不可用,必须手动调小。开 TCP keepalive 时,建议心跳设 8–20 秒。
6.3 根本治法:用长连接
所有 RabbitMQ 支持的协议都假设连接是长生命周期的——频繁开关连接本身就是错误用法。正确做法:应用启动时建连接并复用、多线程共享连接用多信道并发、配合官方客户端的自动恢复。
七、排障清单
7.1 连接数持续涨 / 被拒绝
| 可能原因 | 排查 |
|---|---|
| 句柄耗尽 | rabbitmqctl status 看 file descriptor 上限与占用;调高上限 + ERL_MAX_PORTS |
| 连接泄漏 | 应用没复用连接,每次新建;查代码改成连接池 / 长连接 |
| 抖动堆积 | ss -s 看 TIME_WAIT 数量;调 tcp_fin_timeout、tcp_tw_reuse |
7.2 连接被频繁断开
| 可能原因 | 排查 |
|---|---|
| 心跳太低误判 | 心跳值 < 5 秒容易误判,调到 5–20 秒 |
| 代理/LB idle timeout 杀连接 | 心跳间隔必须 < 代理空闲超时 |
| 服务端流控 | 内存/磁盘告警触发流控;查 rabbitmqctl status 与节点日志 |
| 网络质量差 | 高丢包导致心跳丢失,排查链路 |
7.3 连接建立慢 / 超时
| 可能原因 | 排查 |
|---|---|
| DNS 反向解析慢 | 关 reverse_dns_lookups(默认关);查 /etc/hosts、/etc/resolv.conf |
| 主机名解析失败 | 节点名短/长格式不能混用;rabbitmq-diagnostics resolve_hostname 验证 |
| TLS 握手慢 | 调大 ssl_handshake_timeout;证书链过长 |
| 握手超时 | 弱网环境调大 handshake_timeout(默认 10s) |
7.4 集群节点间通信异常
| 可能原因 | 排查 |
|---|---|
| 25672 / 4369 未放行 | 节点间端口只对集群内网开放,确认防火墙 |
| 主机名解析 | 每个节点必须能解析所有对端节点名 |
| net_ticktime 过低 | < 5 秒会误判节点下线,保持默认或 ≥ 10 秒 |
排障第一步永远是看日志——节点和客户端都会记录心跳超时关闭的连接,日志会直接告诉你关闭原因。
netstat、ss、lsof是看连接状态的常用工具。
小结
| 主题 | 一句话 |
|---|---|
| 端口 | 客户端走 5672/5671,管理走 15672,集群间 25672 + epmd 4369;对外只开客户端端口 |
| 心跳 | 默认 60 秒,最优 5–20 秒;连续两次无响应判死;别设 0 |
| 连接恢复 | 官方客户端默认开自动恢复 + 拓扑恢复;注意 auto-delete + 固定名竞态 |
| 代理 / NAT | 心跳间隔必须 < 空闲超时;要真实客户端 IP 就开 Proxy Protocol |
| 连接数调优 | 句柄 ×1.5 估算;调小 TCP 缓冲区省内存;调大采集间隔降 CPU |
| 抖动 | 根本是别频繁开关连接;TIME_WAIT 靠内核参数缓解;心跳 + TCP keepalive 配合 |
下一篇:《RabbitMQ 安全——认证、授权与 TLS》,讲怎么用用户权限和 TLS 把这扇连接的大门锁好。