你在代码里一定写过一堆 try-catch。每个可能出错的地方都小心翼翼地捕获、判断、兜底,生怕哪里抛个异常把程序搞挂。写着写着,业务主流程被一层层错误处理淹没,自己都快看不清了。
但有一群人反其道而行:他们让程序「该崩就崩」,然后专门派一个角色在旁边盯着------谁崩了就把谁重启到一个干净状态。这个角色,就叫 supervisor(监督者)。而这套「让它崩溃」(let it crash)的哲学,支撑起了号称「永不停机」的电信系统。
这篇文章聊聊 supervisor 到底是什么、从哪来、今天藏在你用的哪些工具里,以及怎么用好它。
supervisor 到底是什么
一句话:supervisor 是一个专门负责「管别人生死」的组件------它启动一批下属(进程、任务、连接),盯着它们,谁出事了就按预定规则把谁重启。它自己不干业务活。
打个比方。一家餐厅后厨,厨师负责炒菜(业务逻辑),店长负责盯场:哪个灶台着火了、哪个厨师撂挑子了,店长负责处理------重新安排、把出问题的环节恢复正常。店长不炒菜,厨师也不用操心「整个后厨崩了怎么办」。各司其职。
supervisor 的精髓就在这一句:把「怎么把活干对」和「出错了怎么办」彻底分开。 业务代码只写正常流程,错误恢复交给 supervisor。
它从哪来:一个对「永不停机」的执着
supervisor 这个词在软件里有明确的出处:Erlang 语言和它的 OTP 框架。
上世纪八九十年代,爱立信要用软件驱动电信交换机。电话交换是不能停的------你打电话打到一半,系统不能因为某个 bug 就整个挂掉。于是 Erlang 的设计者(其中的代表人物是 Joe Armstrong)走了一条和主流很不一样的路,后来被总结成一句口号:let it crash,让它崩溃。
这听起来很反直觉:程序出错了,不应该赶紧修吗?怎么还鼓励崩溃?
关键在于对「错误」的理解。软件运行出错,往往是进入了某个意料之外的、脏的状态。这时候有两种选择:
- 在原地小心翼翼地修补这个已经脏了的状态------但你很难穷举所有异常情况,兜底代码本身也可能有 bug,越修越乱;
- 干脆把这个脏状态整个丢掉,重启回一个已知干净的初始状态。
第二种就是 let it crash。就像你的电脑卡死了,重启一下往往比你研究半天更快更靠谱。前提是:得有人负责重启,而且能重启回干净状态。 这个「负责重启的人」,就是 supervisor。
所以 let it crash 不等于「摆烂让程序随便挂」,它是一整套配合:业务进程遇到搞不定的情况就大方地崩掉、保持简单,而 supervisor 在旁边稳稳地把它扶起来。Joe Armstrong 2003 年的博士论文系统阐述了这套思路,标题很直白------《在存在软件错误的情况下构建可靠的分布式系统》。
监督树:让故障传不出去
一个 supervisor 可以管一批 worker,也可以管另一个 supervisor。层层嵌套,就形成了一棵树,叫 supervision tree(监督树)。
你可以把它类比成公司的组织架构。基层员工(worker)干活出了问题,先由他的直属主管(子 supervisor)处理;主管这一摊全乱了,才上报给更高层。
这样设计的最大好处是故障隔离:某个子系统(一棵子树)出事,影响被关在那棵子树里,不会一崩崩全公司。哪一层能兜住,就在哪一层解决,兜不住才往上冒泡。
重启策略:挂了之后,到底重启谁
「谁挂了重启谁」只是最简单的一种。现实中下属之间常有依赖关系,Erlang/OTP 因此给了几种经典策略:
| 策略 | 含义 | 什么时候用 |
|---|---|---|
| one_for_one | 哪个下属挂了,只重启它 | 下属之间互相独立 |
| one_for_all | 一个挂了,全部一起重启 | 下属强耦合、共享状态,一个坏了别的也不可信 |
| rest_for_one | 挂的那个,加上在它之后启动的,一起重启 | 下属有明确的启动先后依赖 |
选哪种,取决于你的下属之间是「各过各的」还是「一荣俱荣、一损俱损」。这一步选错,要么该连带重启的没重启(留下不一致状态),要么不相关的被无辜牵连(影响面变大)。
在 Elixir(Erlang 家族里现在很流行的语言)里,声明一个 supervisor 通常就这么几行:
elixir
children = [
{MyWorker, arg}
]
Supervisor.start_link(children, strategy: :one_for_one)
这几行就表达了:我有这些下属,用 one_for_one 策略管它们。启动、监控、按策略重启,全部由 Supervisor 接管,你一行错误处理代码都不用自己写。声明式,且威力很大。
最容易被忽略的一条:别把自己重启死
这是从「玩具」到「生产可用」的关键一步。
想象一个 worker,一启动就崩,崩了被重启,重启又立刻崩......这就成了崩溃-重启死循环(crash loop)。它比「干脆不重启」还糟:CPU 被空转吃满,日志被刷爆,问题却一点没解决。
所以一个合格的 supervisor 必须有一道闸门:一段时间内最多重启 N 次,超过就别再傻重启了------supervisor 自己也停下来,把问题上报给更上层,或者直接告警让人介入。通常还会配合指数退避(每次重启的间隔越来越长,别一秒钟试八百遍)。
一句话:无脑重启不叫容错,带上限、带退避、超限就升级的重启才叫容错。
今天,它其实无处不在
你可能一行 Erlang 都没写过,但 supervisor 的思想早就渗进了你天天在用的东西里:
- Elixir 直接继承了 OTP,Supervisor 和 DynamicSupervisor 是日常主力;
- Akka 把它带进了 JVM 的 actor 世界(Akka 后来改成商业许可,社区因此有了开源分支 Apache Pekko);
- supervisord 是 Python 生态里管进程的老牌工具,名字直接就叫 supervisor;
- systemd 里给服务配的 Restart=on-failure,本质就是操作系统级别的 supervisor;
- Kubernetes 虽然不叫这个名字,但 Pod 的 restartPolicy 加上控制器不停地把实际状态拉回期望状态,用的是同一种精神。
换句话说,只要你部署过服务、配过自动重启,你其实已经在用 supervisor 的思想了,只是没意识到它有这么一段家谱。
想用好它,记住这几条
即使你用的不是 Erlang,下面这几条也通用:
- 分开职责:supervisor 只管生死,别往里塞业务;反过来,worker 也别默默把异常吞掉,那等于骗过了监督者。
- 让它崩:遇到不该发生的状态,尽早失败,别带病硬撑到更深的地方才炸。
- 重启要回到干净状态:重启逻辑必须能干净初始化,绝不复用可能已经脏了的旧状态,否则重启也白搭。
- 挑对重启策略:下属互相独立就各自重启;强耦合、共享状态就一起重启。
- 一定要有重启上限加退避:这是防 crash loop 的命门,生产环境必备。
- 让它可观测:把重启次数、失败原因暴露出来。否则某个东西在后台「一直偷偷重启」,你可能很久都发现不了。
写在最后
supervisor 这个词今天被用得很泛,但它的灵魂始终只有一个:把「把活干对」和「出错了怎么办」分开,然后坦然接受失败------让它崩,再稳稳地扶起来。
下次当你又想给一段代码套上第五层 try-catch 时,也许可以换个思路:与其让它带病运行,不如让它干脆利落地崩掉,交给一个可靠的 supervisor 把它重启回正轨。有时候,能优雅地失败并恢复,比假装永不失败要可靠得多。
参考与数据源
- Joe Armstrong, Making reliable distributed systems in the presence of software errors (2003):erlang.org/download/ar...
- Erlang/OTP 监督者设计原则:www.erlang.org/doc/design_...
- Elixir Supervisor 文档:hexdocs.pm/elixir/Supe...
- Apache Pekko(Akka 的开源分支)容错文档:pekko.apache.org/docs/pekko/...
- supervisord 官网:supervisord.org/
- systemd.service(Restart= 选项):www.freedesktop.org/software/sy...
- Kubernetes Pod 生命周期与 restartPolicy:kubernetes.io/docs/concep...