1. 项目背景
值守夜真正吓人的往往不是 Raft,而是节点起不来或起了一半:发布窗口滚动升级后,一台ping通了,5672 却拒绝;另一台日志停在Running boot step database;还有人把插件 enable 失败理解成「集群脑裂」。推广中台需要一张启动时序图贴在 Runbook 背面:哪一段还没打开 AMQP 门,哪一段失败必须整节点退出,哪一层崩溃不该拖死整机。
现状痛点可以画成:
docker start rabbit3 │ ▼ ping 成功? ← 有人把 ICMP/Erlang 发行口当成营业 │ ├─ 日志没有 Server startup complete ├─ listeners 没有 5672 ├─ is_running 真但 is_serving 假(drain 没 revive) └─ 某个 boot step {error, Reason} 整节点 exit │ ▼ 有人以为「进程还在就能切流量」 有人重启整机「清邪」把另外两台 majority 一起抖没有监督树地图,排障会在三个世界迷路:OS 里 Erlang VM 活着、rabbit应用还在 boot、客户端已经打到 5672。能 grep 到Running boot step只证明日志在打;本章要把这句话背后的有向无环图、监督策略、VHost 级隔离钉死。
业务约束:实验室用三节点;本章只在rabbit3上做破坏性实验(错误插件、错误 conf),rabbit1/2 保持支付多数派。生产禁止在营业节点上kill核心监督者「看它会不会自己起来」——那是把one_for_all当玩具。
测试同学要的契约是:插件 MFA 返回{error, _}时节点不得对外 serving;VHost 消息存储挂了默认不自杀整个 OS 节点(vhost_restart_strategy默认continue)。运维要能区分is_running与is_serving:drain 之后进程还在,门已经关上。
还有一类隐蔽事故发生在「看起来已经起来」之后:定义导入、插件ensure_all_started、监听器绑定都挤在 postlaunch 里。有人写脚本ping成功立刻声明 quorum,撞上core_started但 5672 尚未rabbit_networking:boot(),报错被当成网络策略问题。另一拨人在 K8s 里用 TCP 探活 5672,节点仍在跑 boot steprecovery时探活失败被杀,形成重启风暴。把启动生命周期画成可验收的阶段,才能把探针、声明脚本、流量切入点对齐到ready 且 serving,而不是对齐到进程 PID。
2. 项目设计
小胖把小区物业「开业检查」流程图摊开:消防、电梯、收银系统,缺一不可开门。
小胖:这不就是开机自检吗?Erlang 不是号称让进程随便死吗?那监督树不就是物业保安,死一个拉起来就行。Boot step 听着像安装向导的下一步,失败了跳过不就完了?为啥还要画有向图,食堂炒菜哪有这么多步骤。
大师:进程随便死,指的是业务 Channel 这种叶子;大楼钢梁(rabbit_sup)死法完全不同。rabbit_sup:init/1写的是one_for_all且intensity=0:子进程是后来动态挂上去的,策略的意思是「这棵核心树不走宽松的多次重启」,和rabbit_restartable_sup那种one_for_one, 10, 10不是同一档物业。Boot step 更不能跳过:rabbit_boot_steps:run_step/2里 MFA 一旦{error, Reason}就exit——宁可整节点不要,也不要没有数据库的 5672。有向图是requires/enables搭出来的,有环启动时直接{invalid_boot_step_dependency, ...}。
技术映射:叶子可死可重启;boot step 失败 = 拒绝营业;
one_for_all的核心监督 ≠ Channel 的崩溃隔离。
小白:Prelaunch 和 boot step 谁先谁后?我在rabbit.erl里看到run_prelaunch_second_phase,那第一阶段在哪?networking这个 boot step 注释说 skipped,那 5672 到底谁开的?插件是 OTP application 还是 boot step?VHost 监督挂了会不会把支付 quorum 一起带走?is_running为真时客户端连上了会怎样?
大师:第一阶段在rabbitmq_prelaunch应用:读环境、解码 conf、Feature Flag 磁盘对账、日志——此时 AMQP 端口还没开(第 2、3 章说过)。rabbit应用start/2里跑第二阶段:enabled_plugins 文件、flag 注册表、日志再确认、集群/Khepri 准备。然后rabbit_plugins:setup()加载插件,rabbit_boot_steps:run_boot_steps([rabbit | Plugins])按拓扑序执行,状态打到core_started。真正绑端口在postlaunch:插件ensure_all_started、定义导入之后,rabbit_networking:boot()才起 TCP/TLS。源码写明'networking' boot step skipped and moved to end of startup,对应 issue #2405:先让插件就绪再开门,避免半开协议。插件既是 OTP app,又用-rabbit_boot_step往图里插点,所以「enable 插件」常常等于「多跑若干步」。VHost 是rabbit_vhost_sup_sup下的simple_one_for_one:每个 VHost 一棵rabbit_vhost_sup(one_for_all,管消息存储和队列监督)。默认vhost_restart_strategy=continue映射为transient,单 VHost 崩了不连坐 OS 节点;若运维改成stop_node(permanent)则 VHost 挂会拖死节点——支付 quorum 的 Raft 在队列进程里,但节点一死多数派照样抖。is_running只表示rabbit应用在跑(已参与集群);is_serving还要求未 drain。drain 后 ping 可能仍真,5672 却不该再被 LB 打到。
小胖:那我记口令:先 prelaunch 不开门 → 再 boot step 搭内脏 → 最后 networking 开门。失败就整死,不要半开。VHost 是分店,分店着火默认总店还在。
大师:对。再补两句给排障。第一,日志关键字Running boot step带步骤名和 app 名,卡在哪一步就去该模块的 MFA。第二,rabbit_restartable_sup给rabbit_event、rabbit_node_monitor这类「允许死而复生」的工人套了一层one_for_one,别和rabbit_sup的one_for_all搞混——你在 observer 里看到rabbit_event_sup就是这层包装。
技术映射:
core_started≠ 开门;ready才是log_broker_started之后;is_serving= running ∧ ¬drain。
小白:故意让插件失败,用哪种失败?卸载 management 会不会让本章实验误伤第 31 章验收?boot step 重复注册会怎样?code:ensure_loaded检查失败的日志长什么样?Windows 开发机没有 observer 怎么办?
大师:实验室在rabbit3放一个错误的advanced.config或 enable 一个依赖缺失的插件,看节点退出且 1、2 仍可 Confirm;不要在 rabbit1 上拆 management。重复 step 名exit({duplicate_boot_step, StepName});MFA 未导出exit({boot_functions_not_exported, MissingFns})。没有 observer 就用日志 +rabbitmq-diagnostics status+eval打supervisor:which_children(rabbit_sup)。生产破坏性实验只在预发。
小胖:实验:grep 启动日志画时序;which_children;rabbit3 插坏插件看失败;对照 drain 后 is_serving。收工。
3. 项目实战
3.1 环境准备
| 项 | 值 |
|---|---|
| 集群 | 第 31 章三节点;破坏只对rabbit3 |
| 工具 | docker logs、rabbitmq-diagnostics、rabbitmqctl eval |
| 目录 | promo-mq/ch32/ |
| 对照文件 | deps/rabbit/src/rabbit.erl(boot step 声明与start/2)、rabbit_boot_steps.erl、rabbit_sup.erl |
有 Erlang 源码树的读者在 IDE 打开上述文件;只有 Docker 的读者用日志对照本章引用的函数名。
3.2 步骤一:从日志画出启动时序
步骤目标:把Running boot step与「开门」分成两段,证明 networking 不在 boot step 图的末尾假门面里。
dockerlogs rabbit12>&1|grep-E"Running boot step|Ready to start client connection listeners|Server startup complete|Prelaunch"对照源码顺序(简化,不是每一步都打印在同一级别):
rabbitmq_prelaunch # 阶段 1:conf / flags / 日志 rabbit:start/2 run_prelaunch_second_phase rabbit_plugins:setup rabbit_boot_steps:run_boot_steps # 日志:Running boot step X defined by app Y pre_boot → feature_flags → rabbit_registry → database → ... recovery → routing_ready → pre_flight → notify_cluster networking step:只打 debug「skipped」 rabbit_boot_state:set(core_started) run_postlaunch_phase(独立进程) application:ensure_all_started(Plugin) rabbit_definitions:maybe_load_definitions rabbit_networking:boot() # 日志:Ready to start client connection listeners log_broker_started rabbit_boot_state:set(ready)rabbit_boot_steps.erl每一步:
%% 要点(阅读用,不要贴进生产热补丁)?LOG_INFO("Running boot step ~ts defined by app ~ts",[Step,App]),ok=run_step(Attrs,mfa)%% run_step 内:apply(M,F,A) 返回 {error, Reason} -> exit({error, Reason})运行结果:先大量 boot step,再出现Ready to start client connection listeners,最后Server startup complete。
坑:用ping成功当「已开门」——ping走 Erlang 发行,不证明 5672。请再跑rabbitmq-diagnostics listeners。
坑:日志级别若高于 info,可能看不到部分 debug;把log.console.level = debug仅打在 rabbit3 的实验 conf,实验结束改回 info。
3.3 步骤二:看清两棵监督树
步骤目标:区分节点级rabbit_sup与 VHost 级rabbit_vhost_sup。
dockerexecrabbit1 rabbitmqctleval"supervisor:which_children(rabbit_sup)."dockerexecrabbit1 rabbitmqctleval"rabbit_vhost_sup_sup:check()."阅读对照:
%% rabbit_sup.erlinit([])->{ok,{{one_for_all,0,1},[]}}.% 空树,孩子动态 start_child%% rabbit_restartable_sup.erl —— 包一层可重启工人init([{Mod,_F,_A}=Fun,Delay])->{ok,{{one_for_one,10,10},[{Mod,Fun,...,worker,[Mod]}]}}.%% rabbit_vhost_sup.erl —— 每个 VHost 一棵,管 store 与队列监督init([_VHost])->{ok,{#{strategy=>one_for_all,intensity=>0,period=>1},[]}}.%% rabbit_vhost_sup_sup.erl%% simple_one_for_one;RestartStrategy = vhost_restart_strategy()%% continue -> transient(默认);stop_node -> permanent运行结果:which_children能看到rabbit_vhost_sup_sup、rabbit_registry、各类*_sup。check()返回有问题的 VHost 列表,健康时应为[]。
坑:在运行节点killrabbit_sup子进程「做实验」可能触发one_for_all级联,实验室只读树,破坏性用插件失败,不杀监督者。
坑:vhost_restart_strategy在 schema 里还有persistent枚举,代码路径认stop_node/permanent/continue/transient。改这项前读rabbit_vhost_sup_sup:vhost_restart_strategy/0,不要抄错词。
3.4 步骤三:故意让插件/boot step 失败
步骤目标:证明失败 = 节点不 serving,而不是少一个菜单仍营业。
在仅 rabbit3的 conf 里指向一个不存在的 auth 后端插件(第 3、25 章相关检查auth_backend_plugins_check会在 boot 早期炸),或 enable 未安装的插件名:
# 错误示范:实验室一次性dockerexecrabbit3 rabbitmq-pluginsenablerabbitmq_does_not_exist# 更干净:复制 ch32/bad.conf 挂进 conf.d 后重启 rabbit3# promo-mq/ch32/bad-auth.conf (只挂 rabbit3,用完删除) auth_backends.1 = extra_oauth # 对应 boot step auth_backend_plugins_check: # rabbit_access_control:ensure_auth_backends_are_enabled/0dockerrestart rabbit3dockerlogs rabbit32>&1|tail-n80dockerexecrabbit1 rabbitmqctl cluster_status python promo-mq/ch31/publish_order.py --order-id P-CH32-1运行结果:rabbit3 日志在 boot step 附近 exit / 无法ready;rabbit1/2 仍 Confirm;cluster_status显示 rabbit3 不在 running 或不可达。
坑:坏 conf 留在 volume 里,以后第 31 章演练会「无故起不来」。实验结束必须删bad-auth.conf并成功重启 rabbit3,等到 members 恢复。
坑:三台同挂坏 conf = 自杀集群。只允许 rabbit3。
3.5 步骤四:is_runningvsis_serving
步骤目标:把 drain 状态对上rabbit:is_serving/0源码。
dockerexecrabbit3 rabbitmq-upgrade draindockerexecrabbit3 rabbitmqctleval"rabbit:is_running()."dockerexecrabbit3 rabbitmqctleval"rabbit:is_serving()."dockerexecrabbit3 rabbitmq-diagnostics-qpingdockerexecrabbit3 rabbitmq-upgrade revivedockerexecrabbit3 rabbitmqctleval"rabbit:is_serving()."源码:
%% rabbit.erlis_serving()->is_running()andalsonotrabbit_maintenance:is_being_drained_local_read(node()).运行结果:drain 后is_running()仍 true,is_serving()false;revive 后两者 true。
坑:LB 健康检查若只用 Erlang ping,会把 draining 节点继续当后端。健康检查必须认 serving/监听。
3.6 步骤五:关键 boot step 对照表(贴 Wiki)
| 步骤名 | 职责 | 失败时你看到 |
|---|---|---|
feature_flags | flag 注册表 | 升不了级、混版加不进 |
database | rabbit_db:init/0Khepri | 节点起不来 |
recovery | 恢复交换机/队列/绑定 | 拓扑不齐仍可能开门前就 exit |
empty_db_check | 默认用户/VHost | 处女节点才插入 |
pre_flight | 准备与同伴、客户端通信 | 仍未绑 5672 |
notify_cluster | rabbit_node_monitor:notify_node_up | 同伴不知道你来了 |
virtual_host_reconciliation | 并行组网时补齐 VHost 进程 | 定义导入早于成群时的坑 |
(postlaunch)rabbit_networking:boot | TCP/TLS | 日志 Ready to start listeners |
完整列表以rabbit.erl里-rabbit_boot_step与各插件模块为准;插件还会注册自己的 step(exchange type、quorum、stream coordinator 等)。
3.7 测试验证
| 编号 | 操作 | 期望 |
|---|---|---|
| TC-CH32-01 | grep boot step 与 listeners 行 | listeners 日志晚于大部分 boot step |
| TC-CH32-02 | which_children(rabbit_sup) | 含 vhost_sup_sup,非空 |
| TC-CH32-03 | rabbit3 坏 auth conf 重启 | 3 不 serving;1 上支付 Confirm 仍成功 |
| TC-CH32-04 | 恢复 conf 后 3 加入 | members 恢复;支付队列 members=3 |
| TC-CH32-05 | drain 后 eval is_serving | false;revive 后 true |
| TC-CH32-06 | vhost_sup_sup:check() | [] |
测试编写禁止把「VM 进程存在」断言成健康;必须is_serving或listeners含 AMQP。
4. 项目总结
优点与缺点
| 机制 | 优点 | 缺点 |
|---|---|---|
| Boot step DAG | 依赖显式、失败即停、插件可插点 | 图复杂,新人易被 networking 假步骤骗 |
one_for_all核心树 | 内脏不一致时不装傻 | 误杀核心子进程杀伤大 |
restartable_sup | 统计/监控类工人可复活 | 与核心树策略混读会误判 |
| VHost 分树 | 租户存储隔离 | stop_node配错会把租户故障升级成节点故障 |
| 监听器后置 | 避免半开协议 | core_started期间连 5672 会失败,脚本要等 ready |
对比「纯 OTP application:start 链」:boot step 能跨插件声明依赖;代价是多一套图论与 attribute 扫描(rabbit_misc:rabbitmq_related_module_attributes)。
适用场景
- 升级后节点起不来,要对着日志定责到 MFA。
- 评审「要不要把某插件做成 boot step」。
- 健康检查设计(running vs serving)。
- 理解为何定义导入、插件、监听器顺序被刻意后移。
不适用:用杀rabbit_sup当混沌(杀错层);用本章替代第 17 章组网;以为监督树能代替 quorum 副本。
升级窗口把「启动时序」写进值班口令:先看最后一条Running boot step,再看有没有Ready to start client connection listeners,最后才看应用 Confirm。脚本侧用await_startup/ 循环is_serving,禁止sleep 5碰运气。插件评审要求作者写明新增 boot step 的requires/enables,避免和 skipped 的networking名字绑死。
注意事项
- 生产改
vhost_restart_strategy前要演练「VHost 存储故障是否允许节点自杀」。 - Feature Flag 在 prelaunch 就对账,boot step
feature_flags是注册表初始化,两处都要看(第 26 章)。 ENABLED_PLUGINS只影响处女enabled_plugins文件(第 2 章)。- 4.x 元数据默认 Khepri,
database步失败不要再去调 Mnesia 分区策略。 - 安全:
eval等于在节点上执行 Erlang,权限等同管理员,审计要记录。 - 实验室坏 conf 只允许打在非支付 Leader 节点,恢复前不得开始第 31 章混沌。K8s 探活与
is_serving对齐,避免 recovery 阶段被杀进重启风暴。
常见踩坑(生产)
- 滚动后一台「活着」但 LB 还打上去,全部 Confirm 超时。根因:健康检查用 ping 而非 serving/listeners;节点停在 core_started 或 drain。
- 某个 VHost 消息存储循环崩溃,有人把
vhost_restart_strategy改成 stop_node,整个支付节点跟着退出。根因:把租户隔离开关当成「更严格就更好」。 - 自定义插件 boot step 与
kernel_ready形成环或 MFA 未导出,节点起不来,却去查磁盘水位。根因:没搜Running boot step最后一步。
思考题
- 若自定义 Exchange 插件的 boot step
requires写成networking(那个已 skipped 的名字),它实际会在开门前还是开门后执行?对照enables/requires与 postlaunch 顺序预测,下一章帧处理如何依赖「通道监督已在」? rabbit_sup是one_for_all且 intensity 0,为什么动态start_child的大量工人挂掉时节点常常仍活着?提示:看这些 child 的 restart type(transient/permanent)以及它们是否挂在restartable_sup下。
附录 A:完整清单与探针口径
promo-mq/ch32/ bad-auth.conf # 仅挂 rabbit3,用完删除 grep-boot.sh # 抽取 Running boot step / listeners / startup complete eval-tree.sh # which_children / is_running / is_serving checklist.md探针建议(写进第 26、29 章 Runbook 交叉引用):
| 探针 | 能证明 | 不能证明 |
|---|---|---|
OS 进程 /docker ps | VM 在 | 未证明 rabbit 应用 |
rabbitmq-diagnostics ping | Erlang 发行可达 | 未证明 5672、未证明未 drain |
is_running | 已参与集群 | 可能正在维护 |
is_serving | 未 drain 且应用在跑 | 仍建议再查 listeners |
listeners含 AMQP | 门已开 | 不证明 quorum 成员数 |
日志Server startup complete | 本节点宣称 ready | 对照集群 peers 是否承认 |
破坏性实验结束后必须:删除bad-auth.conf、rabbit3ping成功、is_serving为 true、支付队列members=3。否则第 31 章演练会把「启动失败」误判成「杀 Leader 失败」。
延伸阅读与资源
LangChain从入门到进阶实战之旅
SQLAlchemy 2.0从入门到进阶的实战之旅
Dify 从入门到进阶:LLM 应用平台实战修炼
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
MongoDB 实战进阶与内核修炼
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析