第32章:RabbitMQ OTP 监督树、Boot Steps 与启动生命周期源码
2026/9/13 1:49:18 网站建设 项目流程

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_runningis_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_allintensity=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_supone_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_suprabbit_eventrabbit_node_monitor这类「允许死而复生」的工人套了一层one_for_one,别和rabbit_supone_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+evalsupervisor:which_children(rabbit_sup)。生产破坏性实验只在预发。

小胖:实验:grep 启动日志画时序;which_children;rabbit3 插坏插件看失败;对照 drain 后 is_serving。收工。


3. 项目实战

3.1 环境准备

集群第 31 章三节点;破坏只对rabbit3
工具docker logsrabbitmq-diagnosticsrabbitmqctl eval
目录promo-mq/ch32/
对照文件deps/rabbit/src/rabbit.erl(boot step 声明与start/2)、rabbit_boot_steps.erlrabbit_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_suprabbit_registry、各类*_supcheck()返回有问题的 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/0
dockerrestart 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_flagsflag 注册表升不了级、混版加不进
databaserabbit_db:init/0Khepri节点起不来
recovery恢复交换机/队列/绑定拓扑不齐仍可能开门前就 exit
empty_db_check默认用户/VHost处女节点才插入
pre_flight准备与同伴、客户端通信仍未绑 5672
notify_clusterrabbit_node_monitor:notify_node_up同伴不知道你来了
virtual_host_reconciliation并行组网时补齐 VHost 进程定义导入早于成群时的坑
(postlaunch)rabbit_networking:bootTCP/TLS日志 Ready to start listeners

完整列表以rabbit.erl-rabbit_boot_step与各插件模块为准;插件还会注册自己的 step(exchange type、quorum、stream coordinator 等)。

3.7 测试验证

编号操作期望
TC-CH32-01grep boot step 与 listeners 行listeners 日志晚于大部分 boot step
TC-CH32-02which_children(rabbit_sup)含 vhost_sup_sup,非空
TC-CH32-03rabbit3 坏 auth conf 重启3 不 serving;1 上支付 Confirm 仍成功
TC-CH32-04恢复 conf 后 3 加入members 恢复;支付队列 members=3
TC-CH32-05drain 后 eval is_servingfalse;revive 后 true
TC-CH32-06vhost_sup_sup:check()[]

测试编写禁止把「VM 进程存在」断言成健康;必须is_servinglisteners含 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 stepfeature_flags是注册表初始化,两处都要看(第 26 章)。
  • ENABLED_PLUGINS只影响处女enabled_plugins文件(第 2 章)。
  • 4.x 元数据默认 Khepri,database步失败不要再去调 Mnesia 分区策略。
  • 安全:eval等于在节点上执行 Erlang,权限等同管理员,审计要记录。
  • 实验室坏 conf 只允许打在非支付 Leader 节点,恢复前不得开始第 31 章混沌。K8s 探活与is_serving对齐,避免 recovery 阶段被杀进重启风暴。

常见踩坑(生产)

  1. 滚动后一台「活着」但 LB 还打上去,全部 Confirm 超时。根因:健康检查用 ping 而非 serving/listeners;节点停在 core_started 或 drain。
  2. 某个 VHost 消息存储循环崩溃,有人把vhost_restart_strategy改成 stop_node,整个支付节点跟着退出。根因:把租户隔离开关当成「更严格就更好」。
  3. 自定义插件 boot step 与kernel_ready形成环或 MFA 未导出,节点起不来,却去查磁盘水位。根因:没搜Running boot step最后一步。

思考题

  1. 若自定义 Exchange 插件的 boot steprequires写成networking(那个已 skipped 的名字),它实际会在开门前还是开门后执行?对照enables/requires与 postlaunch 顺序预测,下一章帧处理如何依赖「通道监督已在」?
  2. rabbit_supone_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 psVM 在未证明 rabbit 应用
rabbitmq-diagnostics pingErlang 发行可达未证明 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 实战修炼与源码剖析

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询