
前段时间我在给一个业务项目搭消息中间件时遇到了一个很典型的场景RabbitMQ 单节点跑了半年随着接入方变多连接数从几十涨到上千CPU 和内存开始吃紧业务方又在提“你到底能不能保证消息不丢、机器挂了怎么办”。于是我决定把 RabbitMQ 从单机升级到三节点集群同时在入口处加一层 HAProxy 做负载均衡。现在回看这个决定最大的收获不是“集群有多稳”而是搞清楚了一个容易被忽略的问题RabbitMQ 集群本身并没有一个统一入口客户端该连谁、连接断了怎么切、节点挂了怎么探测这些都得靠入口层的负载均衡来解决。这篇文章就把我这次搭建的完整过程写下来包括为什么选了 HAProxy、RabbitMQ 集群侧的细节怎么配、HAProxy 到底怎么写才能避开各种坑、以及我在联调时踩过的几个真实故障。适合正在规划 RabbitMQ 高可用、或者已经把集群搭起来但不知道负载均衡层该怎么弄的同学参考。如果你只是单机跑着玩玩也建议花十分钟看完前两节里面关于权限和虚拟主机的问题八成你迟早会遇到。1. 为什么集群里还要再插一个 HAProxy1.1 RabbitMQ 集群最大的错觉很多人第一次接触 RabbitMQ 集群时脑子里想的都是“我搞三个节点客户端连一个地址消息自动负载均衡到各个节点”。这个想法对了一半——存储和队列确实分布到多节点了但客户端连接的入口问题RabbitMQ 自己并不解决。AMQP 协议是面向长连接的客户端在建立连接之后所有的 channel、消息收发都跑在同一条 TCP 连接上。也就是说一个客户端一旦连上了 node1它的所有流量就走 node1负载均衡器只能在“建立连接的那一刻”做选择没办法把单条连接里的消息再分到别的节点。这和 HTTP 那种短连接、每请求一轮分发的模式有本质区别。那能不能让客户端直接连集群里的多个地址能官方客户端支持配置多个 host 做 failover比如 Java 客户端里可以传一个 Address 数组。但问题也随之而来如果其中的某个节点挂了客户端层面虽然能切换但配置是写死的节点扩容或缩容都得改客户端配置监控也不方便。尤其是当接入方很多的时候让每个业务方都去维护一份节点列表运维上就是一场灾难。所以入口层负载均衡的真正价值是屏蔽集群内部节点变化给客户端一个恒定不变的地址。客户端根本不知道后面是三个节点还是五个节点它只认识那个 HAProxy 的 IP 和端口。1.2 选 HAProxy 而不是 Nginx 或 LVS做负载均衡的现成方案不少Nginx 的 stream 模块也能做四层代理LVS 更是老牌的四层负载均衡为什么这里我选的是 HAProxy先说 LVS。它的性能确实顶级工作在内核态大流量场景下几乎没有对手。但代价是部署复杂度高需要配置 VIP、DR/TUN/NAT 模式对网络环境有要求很多云环境不支持非 NAT 模式的 LVS 漂移而且和 RabbitMQ 这种长连接场景相比有点杀鸡用牛刀。如果你公司的网络团队很专业用 LVS 完全没问题但对大多数业务团队来说维护成本偏高了。Nginx stream 做四层代理是可行的胜在很多人本来就在用 Nginx不用再多引入一个组件。但我实测下来Nginx stream 对连接数的统计、健康检查的细致程度、配置的灵活性都不如 HAProxy。HAProxy 本身就是为“高可用、健康检查、会话保持、四七层混合代理”这些场景设计的ACL、复用、连接限制、细粒度超时配置都比 Nginx 灵活得多。另外还有一个比较务实的原因HAProxy 自带一个 stats 页面可以实时看到每个后端的连接数、状态、流量。做故障排查的时候这个页面能省下很多时间——后端点开一眼就知道哪台节点的连接在暴涨是不是出现了踢皮球式的连接雪崩。1.3 整体拓扑从客户端到节点之间发生了什么我这次用的是三节点 RabbitMQ 加一个单 HAProxy 的拓扑。生产环境如果对入口的可用性要求很高可以在 HAProxy 前面再挂一组 keepalived 做 VIP 漂移两台 HAProxy 做热备这个我后面会提到。先把本次实际搭建的拓扑画出来说清楚客户端生产者/消费者统一连到 10.0.0.100:5672这是 HAProxy 的监听地址HAProxy 通过健康检查感知三台 RabbitMQ 节点的存活状态把新建连接轮询分发到 healthy 节点三台 RabbitMQ 节点组成一个普通集群队列在其中配置冗余策略管理界面对外服务是独立的 HTTP 端口 15672这个入口也经由 HAProxy 转发但走的是 HTTP 模式和 AMQP 走 TCP 模式是分开的。这看起来很简单但实际操作中有几个细节会在后面的配置部分详细展开。一个典型的坑就是有人把 HAProxy 的 mode 配成了 http然后 AMQP 协议数据在七层被解析结果客户端一连上去就握手失败。AMQP 的 5672 端口必须走四层 TCP 转发只有管理界面 15672 才适合走七层 HTTP。2. 前置准备先把 RabbitMQ 集群搭稳2.1 节点规划与安装方式负载均衡只是把流量引到节点上节点本身要是豆腐渣HAProxy 配得再漂亮也没用。所以先把 RabbitMQ 集群这一侧说透。我这次是三个节点rabbit1、rabbit2、rabbit3分别三台 4C8G 的 ECS云主机系统是 Debian 12。为什么是三台因为 RabbitMQ 的 quorum queue 需要多数节点存活才能正常工作三节点集群可以允许一台宕机两节点集群挂一台就只剩 50% 可用性很多机制都会开始摆烂所以最小可用生产配置我建议就是三台。安装方式上我建议你优先考虑官方提供的方式不要自己从零编译。Debian/Ubuntu 下直接加 RabbitMQ 的官方 apt 源然后安装 rabbitmq-serverDocker 方式也很成熟直接跑rabbitmq:3.13-management或 4.x 镜像都行。需要注意的一点是如果你用 Docker 跑集群容器重启可能导致节点主机名变化进而引发集群无法重新加入的问题这个我放到后面的踩坑部分详聊。2.2 加入集群的细节erlang cookie 与主机名RabbitMQ 是基于 Erlang/OTP 的集群成员之间通过 Erlang distribution 通信这里有两个关键点决定了集群能不能建起来.erlang.cookie和主机名解析。.erlang.cookie相当于集群内部的共享密钥所有节点的 cookie 文件内容必须完全一致。手工部署时默认会在/var/lib/rabbitmq/.erlang.cookie用scp把第一个节点的 cookie 拷到另外两台然后重启 rabbitmq-server 即可。这一步看似简单但很多人栽在权限上——cookie 文件的所有者必须是 rabbitmq 用户权限必须是 400文件权限松了Erlang 会直接拒绝通信。主机名解析这件事经常被忽略。如果你在 hosts 文件里给三台节点相互配了内网 IP 和主机名后面能少很多麻烦。集群节点加入时RabbitMQ 会做主机名到节点的映射反向解析失败会导致集群节点内存中保存的节点名和实际不符。我的习惯是/etc/hosts里静态写死三个节点的主机名映射绝不依赖 DNS消息中间件的网络层面越简单越可靠。然后按传统步骤把节点加进集群# 在 rabbit2 和 rabbit3 上分别执行 rabbitmqctl stop_app rabbitmqctl reset rabbitmqctl join_cluster rabbitrabbit1 rabbitmqctl start_appreset会清掉该节点原有的数据所以只在空节点上执行。加完之后用rabbitmqctl cluster_status看 Membership 列表三个节点都显示{running, [rabbitrabbit1, rabbitrabbit2, rabbitrabbit3]}才算真正建成。2.3 队列冗余镜像队列与 quorum queue 怎么选节点建起来了队列如果只存在某一个节点上那和没建集群没区别——节点一挂队列也没了。所以队列必须做冗余。老的方案是镜像队列mirrored queue通过x-ha-policyall这类参数把队列镜像到多个节点但这种队列在同步数据时性能损失较大而且故障恢复机制一直比较别扭。RabbitMQ 3.8 之后官方主推 quorum queue这是基于 Raft 协议实现的复制队列写入需要多数节点确认一致性更强。到了 RabbitMQ 4.0经典镜像队列已经被移除所以如果你是新装的 4.x 版本根本没有选择余地直接用 quorum queue 就好。一个很容易踩的坑是quorum queue 有个默认行为——当集群少于多数节点3 节点集群少于 2 个存活时队列会变得不可用。这意味着三节点集群挂一台没事挂两台整个 quorum queue 就罢工了。这种场景下 HAProxy 能做的只是不再把新连接发给死掉的节点已有的连接也会被断开但集群本身能不能恢复取决于剩余节点数量。所以生产上不要为了省钱只搭两个节点三节点是最低配置。2.4 用户、虚拟主机和权限管理界面上不去最常见的原因我把这部分单独拎出来因为热搜里那些“admin 账号不能创建虚拟主机”、“管理界面能打开但是连不上服务器”的坑我这段时间见得太多了很多都是同一个根因。RabbitMQ 的权限模型是三层用户、虚拟主机vhost、权限。用户是账号vhost 是逻辑隔离单元权限则精确到“某个用户在某个 vhost 上能对哪些资源做什么操作”。很多人只创建了用户忘了分配 vhost或者忘了set_permissions然后就在管理界面上对着“创建 virtual host”的报错发呆。用命令行一次性搞定# 在任意一个集群节点上执行 rabbitmqctl add_user admin Str0ng-Password rabbitmqctl set_user_tags admin administrator rabbitmqctl add_vhost /main rabbitmqctl set_permissions -p /main admin .* .* .*第三条add_vhost /main里的/main是 vhost 名字默认的 vhost 是/。第四条set_permissions后面三组正则分别对应 configure、write、read 权限.* .* .*表示全部放开。如果想管理默认 vhost把-p /main换成-p /就行。还有一个被问烂了的问题用官方 Docker 镜像的guest账号为什么在另一台机器上登录不了因为 RabbitMQ 出于安全考虑默认限制guest只能从 localhost 访问。解决办法就是别用 guest 跑业务创建一个专属账号并赋权而不是试图去解开 guest 的 localhost 限制。3. HAProxy 安装与核心配置3.1 安装方式与版本选择HAProxy 的安装没有太多讲究Debian/Ubuntu 直接用 aptCentOS 用 yum版本尽量选 2.4 以上因为我下面会用到的tcp-check和 stats socket 相关能力在旧版本上行为有差异。我使用的是 HAProxy 2.6 LTS。如果你不想在宿主机上装Docker 跑 HAProxy 也完全可以。但要注意HAProxy 容器需要映射出 5672 和 15672 两个端口健康检查要能访问到 RabbitMQ 的管理端口网络模式建议用 host 模式或者自定义 bridge 网络避免端口映射带来的复杂排查。3.2 一份能直接用的配置AMQP 走 TCP管理台走 HTTPHAProxy 的配置核心是四个段落global、defaults、frontend、backend。下面是我实际在用的精简版配置加了注释可以直接抄作业global log /dev/log local0 maxconn 8192 user haproxy group haproxy daemon defaults log global mode tcp option tcplog option dontlognull retries 3 timeout connect 5s timeout client 180s timeout server 180s timeout check 3s frontend amqp_frontend bind *:5672 mode tcp default_backend rabbitmq_amqp backend rabbitmq_amqp mode tcp balance roundrobin option tcpka option tcp-check tcp-check connect port 5672 server rabbit1 10.0.0.11:5672 check inter 5s fall 3 rise 2 server rabbit2 10.0.0.12:5672 check inter 5s fall 3 rise 2 server rabbit3 10.0.0.13:5672 check inter 5s fall 3 rise 2 frontend management_frontend bind *:15672 mode http option httplog default_backend rabbitmq_management backend rabbitmq_management mode http balance leastconn option httpchk GET /api/health/checks/alarms http-check expect status 200 server rabbit1 10.0.0.11:15672 check port 15672 inter 5s fall 3 rise 2 server rabbit2 10.0.0.12:15672 check port 15672 inter 5s fall 3 rise 2 server rabbit3 10.0.0.13:15672 check port 15672 inter 5s fall 3 rise 2这里面有几个关键点必须解释。第一AMQP 的 frontend 和 backend 都显式写了mode tcp这不是多余的——defaults 里虽然也是 tcp但显式声明能让配置更清晰避免后面有人改成 http 产生连锁反应。第二管理界面单独开了一个 frontend走mode http因为它本身是 HTTP 协议可以顺手做七层转发。第三AMQP 后端的健康检查用的是tcp-check connect port 5672本质上就是测试端口能否建连不做协议层内容检查因为 AMQP 握手是有状态的过程不适合用简单的协议交互做健康检查。3.3 健康检查到底检查什么这是整个负载均衡配置里最值得琢磨的地方。健康检查的目的是确定“这台节点还能不能接新连接”但“能接新连接”和“服务正常”之间是有差距的。如果只用 TCP 端口探测会出现一种情况RabbitMQ 进程还在、端口还监听但集群已经脑裂或者 Erlang 虚拟机卡死新连接建上去了客户端发消息却得不到响应。这就是我为什么在管理界面的健康检查里用了option httpchk GET /api/health/checks/alarms。这个 API 是 RabbitMQ 官方提供的健康检查接口返回 200 表示没有告警返回 503 表示节点无法完成基础操作。把它作为 http 模式的健康检查比单纯探测端口要可靠得多。那 AMQP 为什么不也用 HTTP 检查因为 AMQP 后端和 HTTP 后端是两条流量路径如果你非要在 AMQP 的 backend 里用option httpchkHAProxy 会把健康检查的 HTTP 请求发到 5672 端口AMQP 协议不认识 HTTP 请求检查永远失败。灵活的写法是让健康检查指向管理端口但流量转发仍然走 AMQP 端口即server rabbit1 10.0.0.11:5672 check port 15672 inter 5s fall 3 rise 2这行的含义是业务流量打到 5672健康检查连 15672。这样 AMQP 流量走四层健康检查用管理接口的 HTTP 结果两不误。我实际测试下来这个方案最稳推荐你也这么配。3.4 心跳、超时与 TCP 参数的坑RabbitMQ 的客户端库默认有心跳机制Java 客户端默认心跳 60 秒Python 的 pika 默认心跳也是 60 秒。这个心跳不是 HAProxy 管的是客户端和 RabbitMQ 之间协商的。但 HAProxy 作为中间的透明代理如果它的timeout client或timeout server小于客户端的心跳间隔就可能在两端还没来得及交换心跳时先把空闲连接掐断。这是长连接方案里最容易踩的坑没有之一。具体表现是客户端日志里频繁出现连接被重置然后重连再被重置看起来像是网络抖动实际上是 HAProxy 在杀空闲连接。解决方法是把timeout client和timeout server都设成大于最长心跳间隔的数值。上面配置里我写了 180 秒如果你的客户端把心跳调到 30 秒那 180 秒也是安全的但反过来如果客户端心跳是 5 秒而你的 timeout 是 10 秒两边就会打架处理不好连接就断断续续。另外我在 AMQP backend 里加了option tcpka这是开启 HAProxy 到后端节点的 TCP keepalive。长连接经过云环境的 NAT 网关时如果长时间没有数据包NAT 表项可能被回收导致连接假活。TCP keepalive 加上后可以让中间设备知道这条连接还活着。这是一个小参数但线上断连事故排查到最终往往就是这种小参数的锅。4. 联调验证客户端真的被平均分发了吗4.1 从客户端视角验证配置写完重载 HAProxyhaproxy -c -f /etc/haproxy/haproxy.cfg先检查语法然后systemctl reload haproxy接下来就要验证流量是不是真的被均匀分发到三个节点了。第一步用客户端直连 HAProxy 地址建若干条连接。我用 Python 的 pika 写了个小脚本循环创建 30 条连接每条连接保持存活然后去每个 RabbitMQ 节点上看连接数rabbitmqctl list_connections name peer_host三台节点各应显示约 10 条连接。如果发现某个节点上连接数为 0先别看 HAProxy 配置直接在浏览器里打开 HAProxy 的 stats 页面确认那台节点的状态是不是 UP。我遇到过的最傻的情况是配置检查通过了、reload 成功了但 backend 里把 IP 写错了HAProxy 日志里全是连接失败stats 页面显示 DOWN客户端当然也连不过去。第二步用一个实际的收发消息流程做端到端验证。生产端往队列发 10 万条消息消费端从队列拉取确认无丢失、无重复、延迟正常。这一步的目的不是压测而是确认 AMQP 流量经过 HAProxy 之后的协议完整性——因为 HAProxy 的 TCP 转发在极端情况下可能出现半包、粘包问题虽然概率极低但端到端验证能把这些隐患提前暴露出来。4.2 节点宕机演练配置验证完之后必须做一次真实的宕机演练否则“高可用”三个字就只能停留在 PPT 上。我在非业务高峰期把 rabbit1 这个节点的 rabbitmq-server 直接停掉然后观察三个层面的表现这个过程很值得记录第一层是 HAProxy 的 stats 页面。默认每隔 5 秒做一次健康检查fall 3 次判定 DOWN。实测从节点停止到 HAProxy 标红大概 15 秒左右。这个窗口期内新建的连接会尝试发给 rabbit1然后失败。HAProxy 对失败连接会重试defaults 里的retries 3重试到 healthy 节点之后客户端是无感知的。第二层是已有连接。之前已经建好的、连到 rabbit1 的连接会全部断开这不是 HAProxy 决定的是 TCP 连接对端进程关闭导致的。客户端库需要自动重连。Java 客户端默认开AutomaticRecoveryEnabled会自动重新建立连接pika 则需要设置connection_attempts和retry_delay否则断线后不会自动重连。这一步提醒我负载均衡只是解决了“新连接发给谁”的问题客户端自身的重连能力才是故障切换的最后一道保障。第三层是 quorum queue 的可用性。三节点集群少一个节点quorum 仍在3 节点多数是 2队列可以继续读写。我在演练过程中持续生产、消费确认没有出现消息不可用的报错。4.3 负载均衡的效果观察演练结束后把 rabbit1 加回来然后观察一段时间内的连接分布和负载情况。HAProxy 的 stats 页面是观察入口。除了连接数还可以看Bout流出字节数和Sessions会话数这些指标能反映流量是否真的有打散。我在 RabbitMQ 侧也用rabbitmqctl list_connections核对了各节点连接数同步用rabbitmq-diagnostics runtime_memory_use看了内存占用确认三台节点的连接数和内存曲线基本一致没有出现一台被打满另外两台吃灰的情况。这里要特别提醒AMQP 的负载均衡粒度是“连接”不是“消息”。如果某一个客户端把几百条 channel 全部复用在同一条连接上那无论 HAProxy 怎么轮询它的流量都只会落在同一台节点。遇到这种情况不要怪负载均衡没用要么让客户端多建几条连接要么接受“按连接均衡”的现实。5. 踩坑实录与排查速查表5.1 Docker 部署后 admin 账号建不了虚拟主机这是我在实际操作中替同事排查过最多次的问题。现象是管理界面能打开用 admin 账号登录成功但点击新建 virtual host 时提示没有权限或者干脆没有新建按钮。根因就是我在 2.4 节说的那套权限模型。很多人建了用户打了 administrator 标签就以为万事大吉。administrator 标签只代表这个用户能访问管理界面的全局设置不代表它对任何 virtual host 有任何 configure 权限。创建 virtual host 本身是管理操作但后续要在 vhost 里声明交换机、队列就必须有该 vhost 上的 configure 权限。处理方式也简单三步走确认 vhost 已创建确认用户已授予该 vhost 的权限然后用正常用户去连。如果还不行检查是不是自建用户根本没有分配到默认 vhost/上。这个坑的经典说法就是“管理界面能打开但用 admin 用户不能创建虚拟主机”看到这个描述十有八九是权限没配全。5.2 管理界面能打开但连不上服务器另一个热搜高频问题是docker 部署 RabbitMQ 之后管理 UI 能打开但页面提示无法连接到服务器节点列表是空的。这个问题在我排查过的案例里最常见的原因是容器重启后 hostname 变了而 RabbitMQ 节点名是绑定 hostname 的旧节点名还留在集群元数据里新起来的节点没法认领通信就断了。尤其用 docker-compose 不指定 hostname 时每次up可能拿到不同的容器名称节点名跟着变集群就崩了。解决办法部署时显式指定容器 hostname绑定固定的 volume 保存 rabbitmq 数据和配置启动命令里设置RABBITMQ_NODENAME。比如docker run -d \ --name rabbit1 \ --hostname rabbit1 \ -e RABBITMQ_NODENAMErabbitrabbit1 \ -v rabbit1_data:/var/lib/rabbitmq \ -p 5672:5672 -p 15672:15672 \ rabbitmq:3.13-management5.3 心跳超时导致消费者反复断线这个我在 3.4 节已经详细讲过但值得再单独记录一次因为我在这次联调里真实遇到过一次。现象是消费者运行 55 秒左右就报连接被关闭重连后 55 秒再报极其规律。排查路径先看 RabbitMQ 服务端日志发现连接是被服务端判定心跳超时关闭的再看 HAProxy 日志发现是 HAProxy 主动 reset 了 idle 连接。两边一对照就明白了客户端心跳协商为 60 秒但 HAProxy 的timeout client/server默认只有 50 秒早期配置当连接空闲超过 50 秒HAProxy 先动手了。服务端客户端都还没死中间代理把连接拆了。这个事故的教训是长连接负载均衡的 timeout 配置不能凭感觉要和应用层的心跳参数对齐。我现在保持一个原则HAProxy 的超时时间至少是客户端心跳的 2 倍以上宁长勿短。5.4 HAProxy 后端地址写错导致的“假健康”最后一次坑比较隐蔽我把某个后端的 IP 写成了已回收的旧机器 IP结果 HAProxy 健康检查居然显示 UP。原因是我在 backend 里对这台机器只配了check用的端口探测而那台已回收机器上有另一个进程恰好占着 5672 端口于是 TCP 检查误判为健康。这让我反思了健康检查策略只探测端口不可靠必须探测业务特征。对 RabbitMQ 来说用/api/health/checks/alarms这个 HTTP 接口做检查是最好的选择它不仅能确认端口活着还能确认 RabbitMQ 自身没有告警。如果你因为某些原因不能用 HTTP 检查也至少要保证后端 IP 是从rabbitmqctl cluster_status里实时确认过的节点而不是记忆里的地址。5.5 常见问题速查表我把这次遇到和网友高频遇到的问题整理成一张表方便以后排查时直接对照现象根因处理办法admin 用户无法创建虚拟主机用户缺 vhost 的 configure 权限rabbitmqctl set_permissions -p vhost user .* .* .*guest 登录提示权限不足guest 默认仅限 localhost创建专用账号并赋权管理界面能开但节点列表为空容器 hostname 变化导致节点名错乱固定 hostname设置 RABBITMQ_NODENAME消费者每 60 秒规律断线HAProxy timeout 小于心跳间隔调大 timeout client/server开启 tcpka客户端连接失败但 stats 显示全 UP健康检查只探测端口端口被其他进程占用改用 HTTP API 健康检查三节点挂一台后 quorum 队列不可写存活节点不足多数保证节点数 ≥ 3多数存活HAProxy 语法检查通过但服务不监听bind 端口被占用ss -lntp检查端口占用或换监听端口最后再分享一点这套搭完之后我个人的体会是RabbitMQ 集群的搭建本身不难真正花时间的是入口层的细节——超时参数、健康检查粒度、客户端重连策略。负载均衡不是一个“挂上去就行”的组件它是在替你把故障提前挡在外面但前提是配置得足够贴近实际协议的行为。还有一个实用建议在 HAProxy 节点上开一个 cron 定期抓取 stats 页面的连接数指标或者直接接入 Prometheus 拉 HAProxy exporter。别等到业务方反馈“我们队列消费变慢了”才去看连接分布连接数倾斜往往在故障发生前几小时就有苗头。分布式系统里大多数灾难都不是突发的而是慢慢积累了隐患靠监控提前发现比事后救火重要得多。