上周又被拉去救火,一个跑了半年的消息中间件集群在业务高峰前突然大面积发布阻塞,排查到最后既不是代码问题也不是网络问题,而是当初部署时内存水位线用了默认值,容器内存上限调大之后水位线没跟着调整,节点被自己的内存告警卡住了。这种事在托管 Kubernetes 平台上部署 RabbitMQ 太常见了——YAML 能跑起来不难,难的是跑得稳、扩得动、出问题能查。这篇就把我在某云容器引擎(后文简称 CCE)上部署 RabbitMQ 中间件的完整过程摊开讲一遍,从方案选型、参数换算、YAML 编排,到监控接入、压测验证和一堆踩过的坑。适合已经在用 Kubernetes、准备把消息队列搬上集群的后端和运维同学,也适合刚接手别人留下的中间件集群、想搞清楚每个参数为什么这么配的人。
1. 先把方案定下来:为什么不是随便跑个 Deployment
1.1 单副本容器方案的三个硬伤
很多人第一次在 Kubernetes 上跑 RabbitMQ,很自然地写个 Deployment、replicas 设成 2、挂个 PVC,跑起来看着挺正常。我第一次也这么干过,结果第二周就出事了:两个 Pod 各自是独立节点,互相不认识,队列只在其中一个节点上,客户端连到另一个节点就报 404 找不到队列。这就是第一个硬伤——Deployment 无法保证稳定的网络标识。Pod 重建后 IP 变了、主机名变了,RabbitMQ 的节点名是基于主机名生成的,节点名一变,之前持久化在磁盘里的元数据全部对不上,节点会以全新身份启动,队列定义、用户、策略全丢。
第二个硬伤是存储与身份的绑定关系断开。Deployment 配合 PVC 的写法在滚动更新时会导致新旧 Pod 抢同一个卷,或者新 Pod 挂载了一个空卷,看起来 Pod 是 Running 的,实际里面啥数据都没有。RabbitMQ 的 mnesia 数据、消息索引、定义文件都在 /var/lib/rabbitmq 下,卷挂错位置等于从零开始。
第三个硬伤是没有有序启停的保证。集群模式下节点需要先互相发现再组集群,如果三个副本同时起来,各自都以为自己是第一个节点,会各自以独立模式启动,之后要靠插件去补救,失败率高,而且日志里一堆node not running的报错,排查起来极其痛苦。这三点决定了 RabbitMQ 这类有状态、强身份依赖的中间件,必须用StatefulSet + Headless Service的组合,这是绕不开的基础结构。
1.2 StatefulSet、Operator、Helm Chart 三条路的取舍
确定用 StatefulSet 之后,落地路径其实有三条,我都在不同项目里试过,说一下真实感受。
| 方案 | 上手成本 | 可控性 | 升级维护 | 适合场景 |
|---|---|---|---|---|
| 手写 StatefulSet + 原生插件 | 中等,需要懂 peer discovery 配置 | 最高,每个参数自己定 | 版本升级要手动改镜像和配置 | 定制化需求强、规模不大、团队熟悉 Erlang 生态 |
| 社区 Operator | 低,CRD 写几行就起集群 | 中等,被 Operator 的默认值约束 | Operator 负责升级,但自己也要跟着升级 | 多集群统一管理、希望少写 YAML |
| Helm Chart 模板化 | 低,改 values 即可 | 中等偏高,values 基本能覆盖 | 跟着 Chart 版本走 | 快速搭建、需要标准化交付 |
我最终选了手写 StatefulSet 这条路,不是说 Operator 不好,而是这个场景里我们需要精确控制内存水位线、探针超时时间、preStop 的排空逻辑,还要和内部的日志采集、监控标签体系对齐。Operator 提供的可调项覆盖面不够,改 CRD 之外的配置就得打补丁,后期维护反而更累。如果团队只有一两个人维护、集群数量超过五个,那我建议直接用 Operator,一致性和可维护性的收益更大。
1.3 上线前必须敲定的四个前置决策
动手写 YAML 之前,有四件事必须先定下来,否则后面一定要返工。
存储。消息中间件对磁盘 IO 敏感度极高,机械盘上跑 RabbitMQ 会看到 fsync 耗时飙到几十毫秒,直接拖垮吞吐。生产环境必须选 SSD 类型的 StorageClass,且volumeBindingMode用WaitForFirstConsumer,避免调度到没有对应磁盘的节点上导致 Pod 长期 Pending。回收策略上,StatefulSet 删除后 PVC 默认保留,这个特性要利用好,别顺手写了Retain却不知道删除 PVC 会丢数据。
网络。集群内部通信用 Headless Service 提供稳定 DNS,客户端接入用一个普通 ClusterIP Service 或 LoadBalancer,两者要分开,不要混在一个 Service 上。管理台的 15672 端口绝对不能对外网暴露,这个端口没有做任何限流保护,暴露出去就是送人头。
镜像。不要用latest标签,也不要在 CCE 节点上直接拉公共镜像。正确做法是把镜像同步到同区域的镜像仓库,用固定 tag 加 digest 的方式引用,这样节点拉取走内网,速度快且不受外部网络波动影响。镜像版本要和 Erlang 版本对应好,升级时两者必须一起考虑。
规格。三节点起步,单节点 CPU 请求 2 核、上限 4 核,内存请求 4Gi、上限 8Gi,这是中小规模业务(日均千万级消息)的经验值。不要一上来就设 1 核 2Gi 然后指望靠扩副本扛量,RabbitMQ 单节点的处理能力受限于 Erlang 调度器和队列进程数,内存太小会频繁触发流控,扩副本也救不回来。
2. 关键参数背后的原理,别照抄 YAML
2.1 节点命名与 longname:集群能不能组起来的分水岭
RabbitMQ 的节点名格式是rabbit@主机名,这个主机名在集群内部必须能被其他节点解析到。Kubernetes 里 StatefulSet 的 Pod 主机名形如mq-0.mq-headless.default.svc.cluster.local,带一堆点。Erlang 默认用短名模式(shortname),遇到带点的主机名会截断或者解析失败,于是两个节点死活组不成集群,日志里只留下connection refused之类的模糊提示。
解决办法是显式打开长名模式,在容器环境变量里加RABBITMQ_USE_LONGNAME=true。这一行不加,后面所有集群配置都是白费功夫。配合长名模式,还得确保每个 Pod 的 hostname 稳定可解析,这就是为什么必须用 Headless Service——它给每个 Pod 生成一条pod-name.service-name形式的 DNS 记录,且 Pod 重建后名字不变。
还有一个隐藏的坑:Erlang Cookie 必须所有节点完全一致。这个 Cookie 是节点之间互信的凭证,不一致的话连接会被直接拒绝,报错信息是Connection attempt from disallowed node。生产上不要靠 ConfigMap 挂载一个明文 Cookie,用 Secret 存,并且在所有副本间共享同一个 Secret。Cookie 长度建议 32 位以上随机串,不要用默认的 guest。
2.2 内存水位线怎么算才不触发发布阻塞
RabbitMQ 的内存水位线机制是这样的:当节点内存使用超过设定阈值,会主动阻塞所有生产者连接(注意是阻塞不是断开),逼迫消费端追上来。这个机制本身是保护性的,但如果阈值设错了,保护就变成了自伤。
默认值是相对值 0.4,即"可用内存的 40%"。问题在于,容器里 RabbitMQ 读取内存的方式在旧版本里会读到宿主机内存,容器限额 8Gi 而宿主机 128Gi 的情况下,水位线会算成 51Gi,等于永远触发不了,最后被 kubelet 的 OOM Killer 直接干掉。新版本对 cgroup 限额有支持,但为了保险,我强烈建议显式设置绝对值。
换算逻辑是这样的:容器内存上限设为 8Gi,Erlang 虚拟机自身开销、二进制堆、文件缓存大约要占 25% 到 30%,剩下的才能给消息存储用。保守一点,把水位线设在容器上限的 60% 左右,也就是 4.8Gi,留出约 3Gi 的缓冲。配置写法是在 ConfigMap 里放一份 rabbitmq.conf:
vm_memory_high_watermark.absolute = 4800MB vm_memory_high_watermark_paging_ratio = 0.5 disk_free_limit.absolute = 5GBpaging_ratio设为 0.5 意味着内存用量到水位线一半时就开始把消息换页到磁盘,而不是等到临界点才动作,这样峰值来的时候更平滑。disk_free_limit设为 5GB 是给磁盘留的安全垫,低于这个值同样会阻塞生产者。这两个值都要结合 PVC 容量来定,别设了一个 10GB 的卷却要求磁盘剩余 20GB。
另外提醒一句,队列数量对内存的消耗不能忽略。每个队列在 Erlang 里是一个独立进程加一堆元数据,粗略按 10KB 到 20KB 估算,如果你规划了几万个队列,光元数据就要吃掉几百兆,这部分必须算进水位线预算里。
2.3 探针、优雅下线与脑裂处理的配合
探针配置是最容易出事故的地方,我见过不止一次因为 liveness 探针配得太激进,导致集群在负载高峰期反复重启节点,越重启越乱。
正确的做法是把 liveness 和 readiness 分开设计。liveness 只判断进程是否活着,用rabbitmq-diagnostics -q ping,超时给 30 秒以上,失败阈值给 3 次以上,初始延迟至少 60 秒(RabbitMQ 启动要加载元数据,三节点集群首次启动更慢)。readiness 判断能不能接流量,用rabbitmq-diagnostics -q check_running && rabbitmq-diagnostics -q check_local_alarms,但这个探针要注意,一旦磁盘或内存告警,readiness 会失败,Service 会把该节点摘掉,这是期望行为。
livenessProbe: exec: command: ["rabbitmq-diagnostics", "-q", "ping"] initialDelaySeconds: 90 periodSeconds: 30 timeoutSeconds: 30 failureThreshold: 5 readinessProbe: exec: command: - bash - -c - "rabbitmq-diagnostics -q check_running && rabbitmq-diagnostics -q check_local_alarms" initialDelaySeconds: 30 periodSeconds: 20 timeoutSeconds: 15 failureThreshold: 3优雅下线同样关键。terminationGracePeriodSeconds建议给到 120 秒以上,配合 preStop 钩子执行排空动作,让节点先把连接关掉、把队列主副本迁移走,再退出。如果是仲裁队列,可以调用rabbitmq-upgrade drain或者直接用rabbitmqctl stop_app加await_termination的组合,具体看版本支持情况。preStop 里写个 sleep 然后直接 kill 的做法并不可取,那样等于没有优雅下线,客户端会收到一堆连接重置。
脑裂处理策略在集群参数里设置,cluster_partition_handling三个可选值:ignore、pause_minority、autoheal。三节点集群我建议用pause_minority,少数派节点自动暂停,避免双主写入造成数据不一致;五个以上节点可以考虑autoheal,让多数派存活、少数派重启后重新同步。选哪个取决于你的业务对可用性和一致性的偏好,没有绝对正确答案,但一定要显式设置,别留默认值。
3. 完整实操:从空命名空间到三节点可用集群
3.1 命名空间、存储类与镜像准备的落地细节
第一步建命名空间,顺手把资源配额和网络策略一起打上,避免后面 Pod 乱跑。命令很简单,但标签要打齐,方便成本归集和监控筛选。
kubectl create namespace mq-prod kubectl label namespace mq-prod team=backend env=prod存储这块,先确认集群里有哪些 StorageClass,选 SSD 类型并且支持动态扩容的那个。创建 PVC 模板时容量给 100Gi 起步,消息中间件的磁盘增长是线性的,扩容虽然有 StatefulSet 的 volumeClaimTemplates 支持,但需要手动 patch,最好一次给够。
镜像同步到镜像仓库后,用docker manifest inspect确认多架构支持,然后记录 digest。引用时写成your-registry/mq/rabbitmq:3.12.14-digest这种形式,比纯 tag 稳。CCE 节点拉取时如果遇到证书问题,通常是节点没配好镜像仓库的认证信息,在控制台把仓库密钥绑定到节点池即可,别在 YAML 里写 imagePullSecrets 一个个试。
3.2 ConfigMap 与 Secret:把 Erlang Cookie 和配置固化下来
ConfigMap 放配置文件,Secret 放 Cookie 和默认密码,两者分开管理。ConfigMap 内容如下:
apiVersion: v1 kind: ConfigMap metadata: name: rabbitmq-config namespace: mq-prod data: rabbitmq.conf: | cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s cluster_formation.k8s.host = kubernetes.default.svc.cluster.local cluster_formation.k8s.address_type = hostname cluster_formation.k8s.service_name = mq-headless cluster_formation.k8s.hostname_suffix = .mq-headless.mq-prod.svc.cluster.local cluster_formation.node_cleanup.interval = 30 cluster_formation.node_cleanup.only_log_warning = true cluster_partition_handling = pause_minority queue_master_locator = min-masters vm_memory_high_watermark.absolute = 4800MB vm_memory_high_watermark_paging_ratio = 0.5 disk_free_limit.absolute = 5GB default_user = admin default_pass = CHANGE_ME loopback_users.guest = false management.tcp.port = 15672 prometheus.tcp.port = 15692几点说明。peer_discovery_backend用 k8s 插件,让节点通过查询 API Server 找到同 Service 下的其他 Pod,这是最省事的方式。address_type必须是hostname,配合hostname_suffix拼出完整域名,否则插件返回的 IP 在长名模式下对不上。default_user和default_pass这里只是占位,真实密码走 Secret,用环境变量注入覆盖。
Secret 部分,Erlang Cookie 用命令行生成随机串:
head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 32 kubectl create secret generic rabbitmq-erlang-cookie \ --from-literal=erlang-cookie="$(cat cookie.txt)" \ -n mq-prod注意这里有个细节:Erlang Cookie 文件需要 0400 权限且属主正确,直接用--from-file挂载有时候权限不对导致节点起不来。稳妥做法是用 initContainer 在启动前把 Secret 内容写到/var/lib/rabbitmq/.erlang.cookie并chmod 400,同时把属主改成 999。这个 uid 是官方镜像里 rabbitmq 用户的固定值,别改成别的。
3.3 Headless Service 与 StatefulSet 编排详解
Headless Service 的作用就是给每个 Pod 一个稳定的 DNS 名,配置里clusterIP: None是必须的:
apiVersion: v1 kind: Service metadata: name: mq-headless namespace: mq-prod spec: clusterIP: None publishNotReadyAddresses: true selector: app: rabbitmq ports: - name: amqp port: 5672 - name: management port: 15672 - name: epmd port: 4369 - name: cluster port: 25672publishNotReadyAddresses: true这一行非常关键,它保证 Pod 还没通过 readiness 探针时 DNS 就能解析到。集群组建阶段的节点就是"还没准备好"的状态,如果不加这行,节点互相永远发现不了,形成死锁。
StatefulSet 的核心部分:
apiVersion: apps/v1 kind: StatefulSet metadata: name: rabbitmq namespace: mq-prod spec: serviceName: mq-headless replicas: 3 podManagementPolicy: Parallel selector: matchLabels: app: rabbitmq template: metadata: labels: app: rabbitmq spec: terminationGracePeriodSeconds: 180 securityContext: fsGroup: 999 runAsUser: 999 initContainers: - name: setup-cookie image: your-registry/mq/rabbitmq:3.12.14 command: - bash - -c - "cp /cookie/erlang-cookie /var/lib/rabbitmq/.erlang.cookie && chmod 400 /var/lib/rabbitmq/.erlang.cookie" volumeMounts: - name: cookie mountPath: /cookie - name: data mountPath: /var/lib/rabbitmq containers: - name: rabbitmq image: your-registry/mq/rabbitmq:3.12.14 env: - name: RABBITMQ_USE_LONGNAME value: "true" - name: RABBITMQ_CONFIG_FILE value: /etc/rabbitmq/rabbitmq.conf - name: RABBITMQ_DEFAULT_USER valueFrom: secretKeyRef: name: rabbitmq-admin key: username - name: RABBITMQ_DEFAULT_PASS valueFrom: secretKeyRef: name: rabbitmq-admin key: password ports: - containerPort: 5672 - containerPort: 15672 - containerPort: 4369 - containerPort: 25672 - containerPort: 15692 resources: requests: cpu: "2" memory: 4Gi limits: cpu: "4" memory: 8Gi volumeMounts: - name: config mountPath: /etc/rabbitmq - name: data mountPath: /var/lib/rabbitmq volumes: - name: config configMap: name: rabbitmq-config - name: cookie secret: secretName: rabbitmq-erlang-cookie volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: ssd-standard resources: requests: storage: 100GipodManagementPolicy: Parallel让三个 Pod 同时创建而不是串行等,配合 peer discovery 插件能更快组建集群。如果担心同时启动的竞争问题,改回OrderedReady也可以,但启动时间会拉长到三倍左右,集群规模大的时候体感很明显。
CPU 的 limit 设成 request 的两倍,是因为 RabbitMQ 在消息批量写入时会有短时的 CPU 尖峰,limit 卡太死会被 cgroup 限流,表现为吞吐突然掉一截。但 limit 也不能设太高,否则节点上多个 Pod 同时尖峰会导致节点资源竞争。内存 limit 和 request 的差值则是给 Erlang 堆预留的弹性空间。
3.4 集群组建验证与策略(Policy)下发
Pod 全部 Running 之后,还不能算完事,必须验证集群真的组起来了。进任意一个 Pod:
kubectl exec -it rabbitmq-0 -n mq-prod -- rabbitmqctl cluster_status输出里Running Nodes应该有三个节点全在,partitions为空数组。如果只有一个节点,那说明 peer discovery 没生效,去看日志里cluster_formation相关行,通常是 DNS 解析失败或者 API Server 权限不足。CCE 上 Pod 访问 API Server 走的是默认 ServiceAccount,如果集群开了 RBAC 严格模式,需要给这个 SA 绑定一个能 list endpoints 的 Role,否则插件查不到其他节点。
集群好了之后立刻下发策略,把经典的镜像队列换成仲裁队列,别用老方案。RabbitMQ 3.9 之后经典镜像队列已经被标记为废弃,新版本的队列类型直接选quorum:
kubectl exec -it rabbitmq-0 -n mq-prod -- rabbitmqctl set_policy \ ha-all "^app\." '{"queue-master-locator":"min-masters","ha-mode":"all"}' --apply-to queues上面这条是给存量镜像队列准备的兼容配置,新队列建议在声明时直接指定x-queue-type: quorum,三个节点刚好满足仲裁队列的最小副本要求,性能和一致性都比镜像队列好。如果业务方还在用老的客户端库不支持指定队列类型,那就只能先用策略兜底,后续推动升级。
顺便把死信、TTL 这些通用策略也一起配上,别等到业务方来问才加:
kubectl exec -it rabbitmq-0 -n mq-prod -- rabbitmqctl set_policy \ dlx-policy "^dlx\." '{"dead-letter-exchange":"dlx.exchange"}' --apply-to queues3.5 管理台与 AMQP 端口的对外暴露方式
管理台 15672 只走内网,通过 Ingress 加白名单访问,或者干脆用kubectl port-forward临时看。生产环境我倾向于后者配合堡垒机,省得为管理台单独维护一套认证。
AMQP 的 5672 端口给业务用,如果业务也在同一个集群内,直接给 ClusterIP Service 就行:
apiVersion: v1 kind: Service metadata: name: mq-amqp namespace: mq-prod spec: type: ClusterIP selector: app: rabbitmq ports: - name: amqp port: 5672 targetPort: 5672跨集群或者集群外业务接入,用 LoadBalancer 类型的 Service,配合 CCE 的负载均衡实例。这里要注意健康检查配置,负载均衡的健康检查探针如果打到 5672 上会失败,因为 AMQP 协议不是 HTTP,正确做法是单独开一个健康检查端口或者在负载均衡侧配 TCP 检查。客户端连接串里建议配多个地址做故障转移,不要只写一个入口地址。
4. 监控、压测与容量校验
4.1 Prometheus 指标接入与四条必看曲线
管理插件和 Prometheus 插件都开起来,指标端口 15692。CCE 上一般用 ServiceMonitor 的方式抓取,打上对应的标签:
apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: rabbitmq-monitor namespace: mq-prod labels: release: prometheus spec: selector: matchLabels: app: rabbitmq endpoints: - port: prometheus interval: 30s path: /metrics看板上我固定盯四条线。第一条是rabbitmq_queue_messages_ready加unacked,反映积压趋势,持续上涨说明消费能力跟不上。第二条是rabbitmq_identity_info配合节点数量,确认集群成员没有意外掉线。第三条是rabbitmq_process_resident_memory_bytes对比水位线,提前预警内存告警。第四条是rabbitmq_disk_free_bytes,磁盘见底比内存更致命,因为磁盘告警会直接阻塞所有生产者。
告警阈值上,积压量按业务容忍度设,但内存超过水位线的 70%、磁盘剩余低于 20%、集群节点数少于期望值这三个一定要配成 P1 级告警。另外建议加一条rabbitmq_connections的突增告警,连接数暴涨往往意味着客户端在疯狂重连,背后可能是网络抖动或者认证失败。
4.2 一轮压测的设计、参数与结论
上线前一定要压一轮,别指望估算。我用三节点、每节点 2 核 8Gi 的规格做过一轮测试,压测工具用官方镜像里的 perf-test,直接用 Pod 跑在同一个集群内,避免网络成为瓶颈。
kubectl run perf-test --rm -it --image=your-registry/mq/perf-test:latest \ --restart=Never -n mq-prod -- \ -x 2 -y 2 -z 300 -s 1024 -u perf-q -a参数含义:2 个生产者、2 个消费者、5 分钟、消息体 1KB、队列预声明。跑下来单节点吞吐稳定在 4000 到 5000 条每秒,三节点集群整体接近 12000 条每秒,此时 CPU 使用率在 60% 左右,内存到水位线的 65%,没有触发流控。
再往上加压力,到 8 个生产者的时候出现了内存告警,生产者被阻塞了约 15 秒后恢复。这说明水位线的位置是合理的——它确实拦住了更严重的 OOM,但也提醒我消费端要跟上。压测结论有两个:一是当前规格支撑日均千万级消息没问题,峰值要留 30% 余量;二是必须保证消费端的速度不低于生产端,否则积压迟早触发告警。
压测还有一个容易忽略的点:持久化消息和非持久化消息的差距在五倍以上。测试时如果只跑非持久化消息,得出的吞吐数字毫无参考价值。业务方声明队列时如果没有显式设置 durable 和 delivery_mode,默认是持久化的,压测要对齐真实场景。
5. 问题排查实录与速查表
5.1 启动阶段:Pod 反复重启与 PVC 挂载失败
最常见的一个现象是 Pod 起来几十秒就重启,日志里只有一行Error when reading .erlang.cookie。原因是 Secret 挂载的文件权限是 0644,Erlang 拒绝加载权限过宽的文件。用 initContainer 修正权限就能解决,或者用defaultMode: 0400在卷定义里指定。
第二个高频问题是 PVC 挂载后目录属主是 root,容器以 999 用户运行没写权限,报permission denied。这时候securityContext.fsGroup: 999就是解药,它会让 kubelet 在挂载时自动改属组。如果存储插件不支持 fsGroup,就只能用 initContainer 提前 chown,代价是启动慢几秒。
第三种是 Pod 一直 Pending,kubectl describe看到volume node affinity conflict。这多半是之前某个 Pod 调度到了 A 节点并绑定了本地盘,Pod 重建时被要求调度到 A 节点但资源不够。检查一下 StorageClass 是不是WaitForFirstConsumer,以及节点是否有污点需要容忍。
5.2 集群阶段:节点互相看不见与分区告警
节点组不起来,先按这个顺序查:RABBITMQ_USE_LONGNAME有没有设、Headless Service 的publishNotReadyAddresses有没有开、ServiceAccount 有没有 list endpoints 的权限。这三项查完能解决 90% 的问题。
分区告警(partition)是另一个麻烦事,日志里会看到Mnesia is partitioned或者Node rabbit@xxx is down。原因通常是网络抖动导致节点间心跳超时,或者某个节点被 kubelet 误杀后重建,新旧身份冲突。用pause_minority策略时,少数派节点会自动暂停并停止对外服务,等网络恢复后自动重新加入。手动干预的话可以调用rabbitmqctl forget_cluster_node清理僵尸节点,但这一步要谨慎,一定要先确认那个节点真的不会再回来,否则会丢数据。
5.3 运行阶段:消息堆积、连接暴涨与磁盘告警
消息堆积分两种情况。一种是消费端挂了或变慢,rabbitmq_queue_messages_ready直线上升,这时候先看消费者 Pod 的状态和日志,再看是不是某个队列的 prefetch 设得太小导致吞吐上不去。另一种是生产者发得太快,内存水位线触发流控,表现是业务方报"发消息超时",这时候扩容或者加消费端,光加生产者没用。
连接暴涨通常有两个来源:客户端没有正确复用连接,每次发消息新建一条;或者认证失败后无限重试。前者要推动业务方改成连接池,后者去查rabbitmq_connections维度里的peer_host,找出重连风暴的源头。
磁盘告警的触发比想象中容易,日志文件、消息索引、崩溃转储都会占空间。除了定期清理,还可以把日志输出到 stdout 交给 CCE 的日志服务,容器内不留文件。崩溃转储目录也建议挂在 emptyDir 上,Pod 重建自动清理。
5.4 一张速查表收口
| 现象 | 可能原因 | 排查入口 | 处理方式 |
|---|---|---|---|
| Pod 反复重启,日志报 cookie 权限 | Secret 挂载权限过宽 | ls -l /var/lib/rabbitmq/.erlang.cookie | initContainer 改权限或 defaultMode 0400 |
| Pod Pending,卷亲和冲突 | 本地盘绑定了旧节点 | kubectl describe pod看 Events | 确认 SC 模式,清理旧 PVC 或调整调度 |
| 集群只有单节点 | longname 未开 / DNS 不可达 | rabbitmqctl cluster_status | 加RABBITMQ_USE_LONGNAME=true,检查 Headless |
| 节点间连接被拒 | Erlang Cookie 不一致 | 日志搜disallowed node | 统一 Secret,重启全部节点 |
| 生产者被阻塞 | 内存或磁盘触发水位线 | 看rabbitmq_local_alarm指标 | 调水位线、扩内存、清磁盘、加消费端 |
| 队列找不到(404) | 客户端连到了非主节点且队列未镜像 | 看队列所属节点 | 用仲裁队列,客户端配多地址 |
| 管理台打不开 | 15672 未暴露或 Ingress 规则错 | curl容器内 localhost:15672 | 端口转发临时访问,检查 Service |
| 消费速度上不去 | prefetch 太小或消费者单线程 | 看unacked和消费者数量 | 调大 prefetch,水平扩消费者 |
6. 升级、备份与长期维护的几点补充
6.1 滚动升级的顺序与版本跳跃边界
RabbitMQ 的版本升级不能跳大版本,3.9 直接升 3.13 大概率起不来,官方支持的是逐个小版本递进。具体做法是先升一个次版本,观察一天集群稳定后再升下一个。StatefulSet 的滚动更新策略默认是倒序,从rabbitmq-2开始重启,这个顺序对集群友好,因为主节点通常在前面,最后才动。
升级前一定要做两件事:确认所有队列都是仲裁类型(镜像队列在升级后可能行为变化),以及备份定义文件。升级过程中 readiness 探针会短暂失败,Service 自动摘流,这段时间客户端会重连,建议安排在低峰期。如果升级后发现节点起不来,回滚镜像 tag 就行,StatefulSet 的 revisionHistoryLimit 默认保留 10 个版本,够用。
还有个小细节:Erlang 版本和 RabbitMQ 版本的对应关系在官方文档里有矩阵,升级 RabbitMQ 时如果镜像里的 Erlang 也换了,注意节点间的 Erlang 分布式协议版本兼容性。三节点集群逐台重启时,新旧 Erlang 版本之间通信可能有问题,稳妥做法是先把集群缩到单节点跑新版本,验证没问题后再扩回来,代价是升级期间没有高可用。
6.2 定义与消息的备份策略
定义文件(用户、vhost、队列、策略、绑定关系)用rabbitmqctl export_definitions导出成 JSON,定时任务每天跑一次,存到对象存储里。这一步几乎零成本,但恢复的时候能省下几个小时的手工重建。
消息本身要不要备份,取决于业务对丢消息的容忍度。真要备份的话,用 shovel 插件把消息从生产集群同步到一个单节点的备份实例上,成本不高但能兜底。不过我更倾向于在业务层做补偿,比如生产端记录消息 ID、消费端做幂等,中间件层的备份只能作为最后一道保险,不能当成主要手段。
日常巡检清单我固定成五条:集群节点数是否等于期望值、cluster_status是否有 partition、内存和磁盘是否在安全区间、积压量趋势是否正常、Erlang Cookie 和 Secret 是否被误改。这五条每天早上花三分钟过一遍,能挡住大部分突发故障。
最后说个我自己的体会。这套东西搭起来最花时间的不是写 YAML,而是把每个参数的来龙去脉搞清楚——内存水位线为什么要显式设、探针为什么要分两套、为什么必须用 Headless Service。这些搞明白了,后面遇到任何异常日志都能顺着线索摸到根因;搞不明白,就只能到处搜配置片段,今天抄一个明天抄一个,集群状态越调越乱。另外提醒一句,别在生产集群上直接试新版本,先在测试命名空间里跑一轮压测,把流量模型对齐了再上,这个习惯救过我至少两次。