ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第32章:RabbitMQ OTP 监督树、Boot Steps 与启动生命周期源码

第32章:RabbitMQ OTP 监督树、Boot Steps 与启动生命周期源码 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上做破坏性实验错误插件、错误 confrabbit1/2 保持支付多数派。生产禁止在营业节点上kill核心监督者「看它会不会自己起来」——那是把one_for_all当玩具。测试同学要的契约是插件 MFA 返回{error, _}时节点不得对外 servingVHost 消息存储挂了默认不自杀整个 OS 节点vhost_restart_strategy默认continue。运维要能区分is_running与is_servingdrain 之后进程还在门已经关上。还有一类隐蔽事故发生在「看起来已经起来」之后定义导入、插件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且intensity0子进程是后来动态挂上去的策略的意思是「这棵核心树不走宽松的多次重启」和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 stepVHost 监督挂了会不会把支付 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_strategycontinue映射为transient单 VHost 崩了不连坐 OS 节点若运维改成stop_nodepermanent则 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 statuseval打supervisor:which_children(rabbit_sup)。生产破坏性实验只在预发。小胖实验grep 启动日志画时序which_childrenrabbit3 插坏插件看失败对照 drain 后 is_serving。收工。3. 项目实战3.1 环境准备项值集群第 31 章三节点破坏只对rabbit3工具docker logs、rabbitmq-diagnostics、rabbitmqctl eval目录promo-mq/ch32/对照文件deps/rabbit/src/rabbit.erlboot step 声明与start/2、rabbit_boot_steps.erl、rabbit_sup.erl有 Erlang 源码树的读者在 IDE 打开上述文件只有 Docker 的读者用日志对照本章引用的函数名。3.2 步骤一从日志画出启动时序步骤目标把Running boot step与「开门」分成两段证明 networking 不在 boot step 图的末尾假门面里。dockerlogs rabbit121|grep-ERunning boot step|Ready to start client connection listeners|Server startup complete|Prelaunch对照源码顺序简化不是每一步都打印在同一级别rabbitmq_prelaunch # 阶段 1conf / 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]),okrun_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 rabbitmqctlevalsupervisor:which_children(rabbit_sup).dockerexecrabbit1 rabbitmqctlevalrabbit_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,{#{strategyone_for_all,intensity0,period1},[]}}.%% rabbit_vhost_sup_sup.erl%% simple_one_for_oneRestartStrategy 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 rabbit321|tail-n80dockerexecrabbit1 rabbitmqctl cluster_status python promo-mq/ch31/publish_order.py --order-id P-CH32-1运行结果rabbit3 日志在 boot step 附近 exit / 无法readyrabbit1/2 仍 Confirmcluster_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 rabbitmqctlevalrabbit:is_running().dockerexecrabbit3 rabbitmqctlevalrabbit:is_serving().dockerexecrabbit3 rabbitmq-diagnostics-qpingdockerexecrabbit3 rabbitmq-upgrade revivedockerexecrabbit3 rabbitmqctlevalrabbit:is_serving().源码%% rabbit.erlis_serving()-is_running()andalsonotrabbit_maintenance:is_being_drained_local_read(node()).运行结果drain 后is_running()仍 trueis_serving()falserevive 后两者 true。坑LB 健康检查若只用 Erlang ping会把 draining 节点继续当后端。健康检查必须认 serving/监听。3.6 步骤五关键 boot step 对照表贴 Wiki步骤名职责失败时你看到feature_flagsflag 注册表升不了级、混版加不进databaserabbit_db:init/0Khepri节点起不来recovery恢复交换机/队列/绑定拓扑不齐仍可能开门前就 exitempty_db_check默认用户/VHost处女节点才插入pre_flight准备与同伴、客户端通信仍未绑 5672notify_clusterrabbit_node_monitor:notify_node_up同伴不知道你来了virtual_host_reconciliation并行组网时补齐 VHost 进程定义导入早于成群时的坑postlaunchrabbit_networking:bootTCP/TLS日志 Ready to start listeners完整列表以rabbit.erl里-rabbit_boot_step与各插件模块为准插件还会注册自己的 stepexchange type、quorum、stream coordinator 等。3.7 测试验证编号操作期望TC-CH32-01grep boot step 与 listeners 行listeners 日志晚于大部分 boot stepTC-CH32-02which_children(rabbit_sup)含 vhost_sup_sup非空TC-CH32-03rabbit3 坏 auth conf 重启3 不 serving1 上支付 Confirm 仍成功TC-CH32-04恢复 conf 后 3 加入members 恢复支付队列 members3TC-CH32-05drain 后 eval is_servingfalserevive 后 trueTC-CH32-06vhost_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 stepfeature_flags是注册表初始化两处都要看第 26 章。ENABLED_PLUGINS只影响处女enabled_plugins文件第 2 章。4.x 元数据默认 Khepridatabase步失败不要再去调 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 steprequires写成networking那个已 skipped 的名字它实际会在开门前还是开门后执行对照enables/requires与 postlaunch 顺序预测下一章帧处理如何依赖「通道监督已在」rabbit_sup是one_for_all且 intensity 0为什么动态start_child的大量工人挂掉时节点常常仍活着提示看这些 child 的 restart typetransient/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、未证明未 drainis_running已参与集群可能正在维护is_serving未 drain 且应用在跑仍建议再查 listenerslisteners含 AMQP门已开不证明 quorum 成员数日志Server startup complete本节点宣称 ready对照集群 peers 是否承认破坏性实验结束后必须删除bad-auth.conf、rabbit3ping成功、is_serving为 true、支付队列members3。否则第 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 实战修炼与源码剖析
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进