先说结论:RabbitMQ 集群本身不会给你做接入层的高可用,真正把三台节点用起来的那道门,是 HAProxy。我是在一次内部系统升级时意识到这一点的——三节点集群已经 join 成功,cluster_status显示三个节点都在运行,可生产流量还是全部打在第一个节点上,另外两台完全闲得长草。把 HAProxy 架到 RabbitMQ 集群前面之后,才算是把这套集群真正“用起来”了。
这篇文章就把我完整搭建 HAProxy + RabbitMQ 集群的过程写清楚:集群初始化前必须处理的 Cookie 与 hosts、镜像队列策略、HAProxy 的 TCP/HTTP 双入口配置、健康检查的坑,以及最后的故障验证。适合刚做完 RabbitMQ 集群、准备接入生产流量,或者正在被“客户端直连节点”折磨的同学参考。
1. 先看本质:RabbitMQ 集群解决的是存储和复制,不是接入和路由
1.1 集群到底替你做了什么
RabbitMQ 的集群机制基于 Erlang 分布式节点,节点之间通过 distribution 端口通信,共享元数据(交换机、绑定、队列定义这些)。但这不意味着你的队列数据会自动撑满三个节点。事实上,一个普通(非镜像)队列仍然只存在于某个节点上,集群做的是把这个队列的元数据广播给其他节点,让它们知道“这个队列归谁管”。
所以集群带来的核心收益是两个:一是整体吞吐可以水平扩展(交换机分发可以分布在多节点上),二是配合镜像策略后,单个节点宕机时队列还能在其他节点继续服务。它没有帮你解决的问题是:客户端到底该连接哪台节点。如果没有统一入口,每个客户端都要自己配置三个节点的地址,自己处理连接断开后的重试逻辑,这会把客户端代码搞得非常肿胀。
1.2 为什么选 HAProxy 而不是 DNS 轮询或客户端直连
这个选择题我也纠结过一阵,把常见方案列出来对比一下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 客户端直连多节点 | 少一层代理,链路最短 | 每个客户端都要维护节点列表,故障摘除靠客户端自己 |
| DNS 轮询 | 接入简单,改 DNS 即可 | 客户端和系统 DNS 缓存不可控,节点挂了摘除不及时 |
| HAProxy 统一代理 | 单一接入点,健康检查自动摘除,配置灵活 | 多一层代理,HAProxy 自身需要做高可用 |
我最后选了 HAProxy,主要看中它的健康检查能力。它可以在前端做 TCP 层的连接检查,也能对后端的管理接口发 HTTP 探测,节点一旦处于 alarm 状态,HAProxy 会主动把它从转发池里摘掉。DNS 轮询做不到这种细粒度判断,客户端直连方案则把复杂度全推给了业务方。HAProxy 本身也是纯 C 实现的七层代理,做四层 TCP 转发时开销很低,加在 AMQP 长连接前面不会成为瓶颈。
2. 集群动手前必须统一的三件事:Cookie、主机名、节点角色
2.1 Erlang Cookie:绝大多数 join 失败的根因
RabbitMQ 节点之间通信靠 Erlang distribution,而 Erlang 节点间认证用的是同一个 Cookie 文件。这个文件通常位于/var/lib/rabbitmq/.erlang.cookie,所有节点必须保证内容完全一致。我见过太多人在这里翻车:只把第一个节点的 Cookie 拷贝到了一部分节点,剩下一个节点 join 的时候直接报authentication failure或者Credentials were rejected。
正确做法是先把主节点的 Cookie 文件内容取出来,发到每台节点上覆盖,然后立刻修正权限:
# 在三台节点上分别执行 chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie chmod 600 /var/lib/rabbitmq/.erlang.cookie # 然后重启 rabbitmq-server systemctl restart rabbitmq-server这里有个小细节:不只是内容要一致,文件属主和权限也必须对。如果属主不是rabbitmq用户,RabbitMQ 进程读取 Cookie 时会静默失败或报奇怪的错误,排查起来非常耗时。所以建议第一步统一 Cookie,第二步统一权限,再去做 join。
2.2 主机名解析:节点名必须稳定可解析
RabbitMQ 的节点名格式是rabbit@hostname,这个 hostname 会被用来做节点间通信。如果各节点之间的/etc/hosts没有互相解析的通路,join 时会出现unable to connect to epmd之类的错误。我在搭建时统一用了短主机名,并且把三台节点的映射都写进了各自的/etc/hosts:
10.0.1.11 node1 10.0.1.12 node2 10.0.1.13 node3还要提一句:节点名一旦确认,join 成功后就不要随便改主机名。集群节点之间记录的是rabbit@node1这种完整节点名,如果机器重启后主机名变了,节点会以新名字启动,看起来就像集群里多了一个陌生节点,老节点反而掉线。生产环境里建议把/etc/hostname也写死,并在 RabbitMQ 配置里固定 node name,避免被 DHCP 或云主机初始化脚本改掉。
2.3 磁盘节点与内存节点,以及分区策略
集群里节点类型分 disk 节点和 RAM 节点,RAM 节点只把元数据放内存,性能好但可靠性差。生产环境我建议全部使用 disk 节点,别贪这个性能。RAM 节点的存在会让整个集群的元数据持久化能力缩水,一旦盘节点挂了,RAM 节点会因为无法持久化而拒绝一些操作,得不偿失。
另外一个必须在动手前想清楚的是分区策略。当节点间网络抖动导致集群分裂时,RabbitMQ 默认会停下所有节点等待恢复,这会让业务直接断掉。所以在rabbitmq.conf里要显式配置:
cluster_partition_handling = pause_minority把分区处理策略设为pause_minority,意思是谁是少数派谁先暂停,保住多数派的可用性。还有一种autoheal模式会自动恢复,但行为相对激进。具体选哪个要看业务容忍度,我个人的习惯是优先保证多数派可用,所以用pause_minority。
3. 三节点 join 实操:命令能跑通,校验和策略也不能省
3.1 初始化顺序和 join 命令
第一个节点直接启动即可,不需要手动初始化。第二、第三个节点才需要 join。我的操作顺序是这样:
# node2 和 node3 上执行 rabbitmqctl stop_app rabbitmqctl reset rabbitmqctl join_cluster rabbit@node1 rabbitmqctl start_app这里有个容易困惑的地方:为什么是stop_app而不是systemctl stop rabbitmq-server?因为stop_app只停掉 RabbitMQ 应用但保留 Erlang 节点进程,join_cluster 需要在 Erlang 层面对话。如果你把整个服务停掉,join_cluster 反而找不到对方。reset则会清空本节点的已有数据,所以只建议在全新节点或明确要重新加入集群的节点上执行,千万别在有存量数据的节点上随手跑。
3.2 用 cluster_status 验证而不是靠感觉
join 完成后不要急着切流量,先用命令确认集群状态:
rabbitmqctl cluster_status期望看到的Nodes里包含三个节点,Running Nodes也包含三个,Partitions为空。如果发现某个节点状态异常,可以用rabbitmq-diagnostics -q ping检查节点间连通性,也可以直接看/var/log/rabbitmq/下的日志,大部分时候问题都出在 Cookie 或 hosts 上。
3.3 镜像队列策略:集群真正高可用的关键
这一步是最容易被漏掉的。普通队列创建在哪个节点上就只存在于那个节点,节点挂了队列也就跟着没了。要让队列存活在多个节点上,必须设置镜像策略:
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all","ha-sync-mode":"automatic"}' --priority 1这条策略匹配所有名字的队列,把它们镜像到所有节点上,并开启自动同步。ha-sync-mode选automatic是为了让新加入的镜像节点自动补全消息副本,否则新节点只保存新增消息,老消息全丢。
这里我想多说一句:如果你用的是 RabbitMQ 3.8 之后的版本,新业务建议优先考虑 quorum queue(仲裁队列)。它基于 Raft 协议做多副本,天然带高可用,不会像经典镜像队列那样在同步大队列时产生明显的性能抖动。但存量业务可以先用镜像策略过渡,不必一刀切重写。
3.4 顺手建一个监控专用账号
后续 HAProxy 对管理接口做 HTTP 健康检查时需要一个账号,默认的guest只能本机访问,远程探测必然被拒。所以我在集群初始化阶段顺手建了一个权限极小的监控账号:
rabbitmqctl add_user monitor YourPassword rabbitmqctl set_user_tags monitor monitoring rabbitmqctl set_permissions monitor "" "" ""monitoring标签保证它能访问/api/health/checks/alarms这类监测端点,但没有任何业务读写权限,安全风险最小。
4. HAProxy 配置拆解:5672 走 TCP 代理,15672 走 HTTP 健康检查
4.1 安装与版本确认
HAProxy 在常见 Linux 发行版上直接装就行,apt 或 yum 二选一。装完先确认版本,因为后面用到的http-check send指令要求 HAProxy 2.2 以上,版本太老的话健康检查方案要换:
haproxy -v如果版本偏老,建议先升级,不然管理接口的 HTTP 健康检查不好配。
4.2 一个完整的 haproxy.cfg 示例
我直接把线上用的那份配置简化后贴出来,核心是三个 listen:AMQP 的 5672 TCP 代理、管理 Web 的 15672 HTTP 代理、以及 HAProxy 自己的监控页:
global log /dev/log local0 info maxconn 10000 user haproxy group haproxy spread-checks 5 defaults log global mode tcp option tcplog option dontlognull retries 3 timeout connect 5s timeout client 300s timeout server 300s timeout check 5s listen amqp_cluster bind 0.0.0.0:5672 mode tcp balance roundrobin server rabbit1 10.0.1.11:5672 check inter 10s rise 2 fall 3 server rabbit2 10.0.1.12:5672 check inter 10s rise 2 fall 3 server rabbit3 10.0.1.13:5672 check inter 10s rise 2 fall 3 listen rabbitmq_web bind 0.0.0.0:15672 mode http option httplog balance roundrobin option httpchk http-check send meth GET uri /api/health/checks/alarms hdr Authorization "Basic bW9uaXRvcjpZb3VyUGFzc3dvcmQ=" http-check expect status 200 server rabbit1 10.0.1.11:15672 check inter 10s rise 2 fall 3 server rabbit2 10.0.1.12:15672 check inter 10s rise 2 fall 3 server rabbit3 10.0.1.13:15672 check inter 10s rise 2 fall 3 listen stats_page bind 0.0.0.0:1080 mode http stats enable stats uri /stats stats auth haproxy_admin:YourAdminPassword有几个地方值得展开讲。
4.3 健康检查选型:TCP 检查为什么不够
AMQP 入口我默认用了 TCP 连接检查,很多教程到这里就结束了。但 TCP 检查只代表“这个端口能连上”,代表不了“这个节点还能正常服务”。RabbitMQ 节点在内存水位告警或磁盘告警时会进入 alarm 状态,此时 5672 端口照样能建立起 TCP 连接,可是生产者和消费者已经明显异常了。如果 HAProxy 还继续往里导流,等于把消息堆积问题再放大一遍。
所以我单独把管理 Web 入口配成 HTTP 检查,检测/api/health/checks/alarms这个端点。这个接口只有在节点没有磁盘、内存等 alarm 时才会返回 200,一旦节点进入告警状态,健康检查就失败,HAProxy 会自动把这个节点摘掉。
这里要特别提醒:管理接口默认需要认证,所以http-check send里的Authorization头必须带上。那串Basic后面的内容是用监控账号生成的:
printf 'monitor:YourPassword' | base64把输出的结果替换到配置里即可。这个做法比在管理插件里临时关闭认证安全得多,也经得起生产环境审计。
4.4 超时参数与客户端心跳的配合
timeout client和timeout server我统一设成了 300 秒,这是有讲究的。AMQP 协议本身有心跳机制,客户端和服务端会协商一个心跳间隔,比如默认 60 秒。如果 HAProxy 的转发超时设得比心跳间隔还短,连接在空闲窗口期就可能被 HAProxy 掐断,客户端拿到的是莫名其妙的连接重置。把超时放到心跳间隔的好几倍以上(我习惯 300 秒),再加一层 TCP keepalive 兜底:
option tcpkatcpka可以理解为让 HAProxy 主动维护底层 TCP 连接的活性,避免中间设备把空闲连接静默回收。
5. 踩坑实录:四个差点让集群“假完成”的问题
5.1 join 报 authentication failure:最后发现是 Cookie 没同步干净
第一次搭集群时我自信满满,node2 的 join 却直接失败,日志里反复出现auth failed。我以为是网络问题,排查了半天才发现是 node3 的.erlang.cookie是从一个临时容器里带出来的,根本没和 node1 同步。这个坑的教训是:集群初始化前先统一跑一遍 Cookie 检查和权限检查,别只盯着 join 命令本身。最稳妥的做法是用同样的方式(比如配置管理工具或一键脚本)给三台节点写同一个内容,再逐个重启 RabbitMQ 服务。
5.2 健康检查把活节点摘掉:HTTP 检查超时设太短
上完 HAProxy 之后出现过一个诡异现象:业务高峰期某个节点经常被自动摘除,低谷期又恢复了。查看 HAProxy 前端日志,发现是 rabbitmq_web 的健康检查连续失败。问题出在默认的timeout check 5s上——管理接口在高峰期响应变慢,偶尔超过 5 秒,健康检查就判定失败。把timeout check调到 10 秒,同时把fall 3保留,给它连续失败三次的容错空间之后,误摘现象就消失了。健康检查的节奏一定要根据后端实际响应时间设计,检查太频繁、超时太短,都是在给自己制造故障。
5.3 客户端空闲连接被重置:超时和心跳没对齐
另一个经典故障是:消费者客户端挂在那里不动,过一会儿就收到连接被重置的报错,过一会儿又恢复。用 tcpdump 抓包发现断连时间点恰好是 HAProxy 的转发超时窗口。原因就是客户端和服务端协商的心跳是 60 秒,而我之前把timeout client和timeout server设成了 30 秒测试,忘了改回来。短于心跳间隔的超时等于主动杀连接。后来我把客户端心跳统一配置成 30 秒、HAProxy 超时 300 秒,空闲连接再也没有莫名其妙断过。
5.4 节点宕机后队列不可用:镜像策略没设置
这是最典型的“假完成”场景。集群状态看起来一切正常,但当你真的停掉队列所在节点时,队列消失了,消费者报队列不存在。原因就是忘了执行set_policy设置镜像。这里给大家一个验证习惯:rabbitmqctl list_queues name slave_nodes,看一眼队列的slave_nodes字段是否包含了多个节点。如果只有空列表,说明镜像策略没生效,赶紧回去补策略。
6. 最后验证:把“完成”变成故障演练里的事实
6.1 故障演练清单
配置全部完成后,我建议强制自己做一个完整的故障演练,而不是看一眼界面就宣布完成。我的演练清单大概是这样的:
# 1. 模拟节点故障:在 node2 上停止 RabbitMQ 应用 rabbitmqctl stop_app # 2. 确认集群自动摘除 rabbitmqctl cluster_status # 3. 观察 HAProxy 统计页,确认 node2 已从健康后端移除 # 4. 客户端继续生产和消费,确认无大量失败 # 5. 恢复 node2 rabbitmqctl start_app # 6. 再次确认队列镜像恢复 rabbitmqctl list_queues name slave_nodes这里有个容易被忽略的点:客户端必须开启自动重连恢复机制。只靠 HAProxy 做摘除还不够,如果客户端连接在 node2 上被断开,但客户端不会自动重连,流量依然会断。所以客户端侧的automatic connection recovery也要确认打开,两层配合才是完整的高可用链路。
6.2 日常监控别只盯集群状态
集群做完之后,日常巡检我主要看几类指标:内存和磁盘水位、队列积压数量、以及 HAPRoxy 统计页里每个后端节点的状态。命令层面的快捷检查是:
rabbitmq-diagnostics -q check_running管理接口上可以用监控账号拉/api/overview,里面能直接看到集群汇总状态。HAProxy 那边最好把统计页接入到已有监控系统,后端节点一旦出现状态变化,能第一时间告警,而不是等业务投诉才去翻页面。
6.3 HAProxy 自身的高可用:下一步自然要做的事
HAProxy 解决了 RabbitMQ 集群的接入问题,但它自己成了新的单点。所以我的建议是:搭两台 HAProxy,前面用 keepalived 挂一个虚拟 IP,主备切换。这一步做完,从客户端到虚拟 IP、再到 RabbitMQ 任意节点,每一条链路都有冗余。我这个项目当前做到的就是 HAProxy 统一接入层,虚拟 IP 是下一步计划,但配置思路已经预留好了——只要在 keepalived 配置里把 VIP 绑到 HAProxy 所在网卡即可,RabbitMQ 侧完全不用动。
最后分享一个我的个人习惯:每次调整完集群或 HAProxy 配置,我都会做一次“断电演习”。不用真断电,只要依次停掉每个节点,观察流量是否自动切换、恢复后镜像是否自动补齐。这个流程看着简单,但能把集群的“完成”从写在标题里变成真正被验证过的事实。毕竟高可用系统的价值不在平时,就在你下一次不得不重启节点的那一刻。