第 05 站展开探险地图
00装好工具前置准备01起跑线前置准备02进程与信箱R1Elixir 数据流水线可选复习R2读懂 Erlang可选复习03消息与超时04OTP 消息章法05会重启的监督树06给并发设上限07BEAM 节点失联X1两种语言对暗号双语扩展X2双语搭档双语扩展08可靠任务调度器
首页/BEAM 主线/第 05 站
05
OTP进阶探索ElixirErlangOTP

会重启的监督树

先弄清谁依赖谁,再决定一个伙伴倒下时该重启哪些进程。

4小关卡
约 6 小时 · 建议分 4 次可以分几次
QUESTION · 本站只追这一问

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 顺序表达依赖;前面的故障会重启后面的依赖者。

当前代码
Elixir
# 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 请求。
LAB
动手

让 child 逐个退出

约 15–25 分钟

记录三个 PID,再分别让 dispatcher 与 registry 异常退出。比较哪些 PID 改变。

  1. 01

    启动监督树,用 which_children 记录所有 child 的 PID

  2. 02

    让最后一个 child 异常退出,重新查看 PID

  3. 03

    再让第一个 child 异常退出,比较这次有多少 PID 改变

复制到终端,按回车
# 列出监督者的 child 名称、PID、类型和模块
Supervisor.which_children(Jobs.Supervisor)
你会看到
  • 最后一个 child 出错时,通常只有它自己重新启动
  • 第一个 child 出错时,rest_for_one 会让它和后面的依赖者都重新启动
故意弄坏

改成 one_for_all,再让独立的最后一个 child 退出。观察是否发生多余重启。

这次能看清

restart strategy 和 child 顺序会一起决定哪些进程重新开始。

这次还不能说明

新 PID 只说明进程重启。业务状态、磁盘数据和外部操作仍要检查。

先认词

这段代码里的关键词

01

故障域

故障影响范围。有些组件一起恢复,有些彼此隔离;监督树用结构表达这种关系。

02

restart intensity

一段时间内允许的最大重启次数。超过上限,监督者退出,把问题交给上一级。

03

Application

OTP 中可启动、停止、配置和声明依赖的组件。根监督树通常由 application callback 启动。

从代码里认出章法

这里用了哪些设计模式

01

Supervision Tree

把进程所有权、依赖和恢复边界写进层级结构。

02

Bulkhead

把互不依赖的工作拆进不同故障域,避免一处拖倒全部。

03

Restart Strategy

按依赖关系选择 one_for_one、rest_for_one 或 one_for_all。

想一想

哪种伙伴关系更适合使用 rest_for_one?

轮到你

画一棵监督树

画出任务队列的监督树。标出每个 child 的重启类型,并说明恢复范围。

提示 1先迈一步

先找出谁保管长期状态,再画它与其他进程的依赖。

提示 2再缩小一点

短暂执行的任务和长期基础设施,通常不该使用完全相同的 child spec。

提示 3离答案很近了

别忘了写下超过 restart intensity 后,问题会交给谁。

提示 4从终点往回想

先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。

过关条件
  • 每一条父子关系都能说出恢复理由

  • 重启类型与正常、异常退出时的期待相符

  • 图中明确指出至少一个不该靠崩溃或自动重试处理的业务错误

带走

记住三句话

  1. 1

    监督树首先在回答:谁依赖谁,出错时影响应该停在哪里。

  2. 2

    “Let it crash” 不是忽略可以预料的错误,而是把无法继续的局部问题交给监督者。

  3. 3

    进程重新启动只是恢复的一步;数据、外部操作和幂等仍要认真设计。

再读一点

去看原版资料

OTP Design PrinciplesElixir Supervisor
本站结束实验做过,答案也想过,就把这一站收好。