第 05 站展开探险地图
会重启的监督树
先弄清谁依赖谁,再决定一个伙伴倒下时该重启哪些进程。
Registry、worker supervisor 与 dispatcher 有依赖时,怎样选择重启策略,让故障恢复充分又不过度?
一个独立 child 退出,整个子系统都被重启
监督树把彼此独立的 child 放进 one_for_all。一个 worker 退出后,Registry 和 dispatcher 也被重启;换成错误顺序后,依赖者又保留了过期状态。
- child 的依赖关系、启动顺序与监督树结构
- 每次注入退出前后的 PID
- 重启强度,以及恢复后的状态和外部副作用检查
它要解决什么
“Let it crash” 不是放任错误。无法继续的进程交给监督者重启;密码错误、库存不足等预期情况仍要正常返回。
你会做到
能从“谁依赖谁、谁应一起恢复”画出监督树
能用自己的话比较
one_for_one、one_for_all和rest_for_one能根据进程正常或异常退出时的期待,选择 permanent、transient 或 temporary
- 会写一个简单的 GenServer 或 gen_server
- 知道 link 会传递退出信号,也知道进程可能正常或异常结束
先看做什么,再看怎么写
rest_for_one 用 child 顺序表达依赖;前面的故障会重启后面的依赖者。
# child 的顺序会影响 rest_for_one 的恢复范围
children = [
# Registry 先启动,后面的组件会使用它
{Registry, keys: :unique, name: Jobs.Registry},
# 动态监督者负责照看临时 worker
{DynamicSupervisor,
strategy: :one_for_one,
name: Jobs.Workers},
Jobs.Dispatcher
]
# 从同一个根监督者启动整组 child
Supervisor.start_link(
children,
strategy: :rest_for_one,
name: Jobs.Supervisor
)为什么这样写
为什么让监督者重启,而不是到处 try/catch?
局部进程无法继续时,监督者能按依赖关系恢复一个已知初始状态。
预期业务错误仍应正常返回;重启也不会恢复已丢数据或撤回外部副作用。
从 Java、Python、JavaScript 看过来
Java
- 熟悉的起点
- 捕获异常、容器重启或线程池替换 worker。
- BEAM 在意什么
- 监督树把进程依赖与局部恢复写进应用内部。
- 别带错直觉
- 不是所有错误都靠 crash;预期结果仍走返回值。
Python
- 熟悉的起点
- try/except、task group 与外部进程管理器。
- BEAM 在意什么
- link 与退出信号让监督关系成为运行时协议。
- 别带错直觉
- 重启不会自动补回内存状态。
JavaScript
- 熟悉的起点
- catch、worker exit 与进程管理器。
- BEAM 在意什么
- 监督者按 child spec 和策略决定恢复范围。
- 别带错直觉
- 重启不能撤销已经发出的 HTTP 请求。
让 child 逐个退出
记录三个 PID,再分别让 dispatcher 与 registry 异常退出。比较哪些 PID 改变。
- 01
启动监督树,用
which_children记录所有 child 的 PID - 02
让最后一个 child 异常退出,重新查看 PID
- 03
再让第一个 child 异常退出,比较这次有多少 PID 改变
# 列出监督者的 child 名称、PID、类型和模块
Supervisor.which_children(Jobs.Supervisor)- 最后一个 child 出错时,通常只有它自己重新启动
- 第一个 child 出错时,
rest_for_one会让它和后面的依赖者都重新启动
改成 one_for_all,再让独立的最后一个 child 退出。观察是否发生多余重启。
restart strategy 和 child 顺序会一起决定哪些进程重新开始。
新 PID 只说明进程重启。业务状态、磁盘数据和外部操作仍要检查。
这段代码里的关键词
故障域
故障影响范围。有些组件一起恢复,有些彼此隔离;监督树用结构表达这种关系。
restart intensity
一段时间内允许的最大重启次数。超过上限,监督者退出,把问题交给上一级。
Application
OTP 中可启动、停止、配置和声明依赖的组件。根监督树通常由 application callback 启动。
这里用了哪些设计模式
Supervision Tree
把进程所有权、依赖和恢复边界写进层级结构。
Bulkhead
把互不依赖的工作拆进不同故障域,避免一处拖倒全部。
Restart Strategy
按依赖关系选择 one_for_one、rest_for_one 或 one_for_all。
哪种伙伴关系更适合使用 rest_for_one?
画一棵监督树
画出任务队列的监督树。标出每个 child 的重启类型,并说明恢复范围。
提示 1先迈一步
先找出谁保管长期状态,再画它与其他进程的依赖。
提示 2再缩小一点
短暂执行的任务和长期基础设施,通常不该使用完全相同的 child spec。
提示 3离答案很近了
别忘了写下超过 restart intensity 后,问题会交给谁。
提示 4从终点往回想
先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。
每一条父子关系都能说出恢复理由
重启类型与正常、异常退出时的期待相符
图中明确指出至少一个不该靠崩溃或自动重试处理的业务错误
记住三句话
- 1
监督树首先在回答:谁依赖谁,出错时影响应该停在哪里。
- 2
“Let it crash” 不是忽略可以预料的错误,而是把无法继续的局部问题交给监督者。
- 3
进程重新启动只是恢复的一步;数据、外部操作和幂等仍要认真设计。