探讨 Joe Armstrong 如何塑造 Erlang 的并发、监督与“let it crash”心态——这些思想仍被用来构建可靠的实时服务。

Joe Armstrong 不只是 Erlang 的共同创作者——他还是这套思想最清晰、最有说服力的讲解者。通过演讲、论文以及务实的观点,他普及了一个简单的理念:如果你想让软件保持在线,就要为失败而设计,而不是假装可以避免失败。
本文是对 Erlang 思维方式的导览,以及解释在构建可靠实时平台时这些思想为何依然重要——比如聊天系统、呼叫路由、实时通知、多玩家协调,以及那些在部分组件失常时仍需快速且一致响应的基础设施。
“实时”并不总是指“微秒”或“硬性截止”。在许多产品中它意味着:
Erlang 为电信系统而生,这类系统对这些期望是强制性的——也正是这种压力塑造了它最有影响力的理念。
不从语法入手,而是关注那些让 Erlang 著名并持续出现在现代系统设计里的概念:
在此过程中,我们会把这些理念和 actor 模型与消息传递联系起来,用通俗的方式解释监督树和 OTP,并展示为什么 BEAM VM 让整套方法变得可行。
即使你不用 Erlang(甚至永远不会用),Armstrong 的框架仍为你提供了一份强有力的清单,帮助构建在现实环境混乱时仍能保持响应和可用的系统。
电信交换机和呼叫路由平台不能像许多网站那样“停机维护”。它们被期望全天候处理通话、计费事件和信令流量——通常对可用性和响应时间有严格要求。
Erlang 在 1980 年代末期源于爱立信,意在用软件而非专用硬件来满足这些现实。Joe Armstrong 和同事们并非单纯追求优雅;他们试图构建运营人员在持续负载、部分故障和复杂现实条件下也能信赖的系统。
思维方式的关键转变在于:可靠性并不等于“从不失败”。在大型长期运行的系统中,总会出现故障:某个进程遇到意外输入、某个节点重启、网络链路抖动或某个依赖停顿。
因此目标变成:
正是这种心态使得监督树和“let it crash”这样的想法显得合理:你把失败视为常态事件,而非灾难性的例外。
将故事讲成某位远见者的单点突破很诱人。更有用的观点更简单:电信的约束迫使人们在权衡上做出不同选择。Erlang 优先考虑并发、隔离和恢复,因为这些是保持服务在不断变化的世界中运行所需的实用工具。
这种以问题为中心的框架也是为何 Erlang 的教训今天仍然通用——任何地方只要可用性和快速恢复比完美预防更重要,它们都能派上用场。
Erlang 的核心思想之一是,“同时做很多事”并不是事后加上的特性——而是你构建系统的常态。
在 Erlang 中,工作被拆成许多微小的“进程”。把它们想象成小工作者,各自负责一项任务:处理一个电话、跟踪一个聊天会话、监控一个设备、重试一次付款或监听一个队列。
它们是轻量的,意味着你可以在不需要巨大硬件的情况下拥有大量此类进程。与其让一个笨重的工作者尝试处理一切,不如用一群专注的小工作者——它们能快速启动、快速停止并快速被替换。
许多系统被设计成一个大程序,内部包含很多紧耦合的部分。当这类系统遇到严重错误、内存问题或阻塞操作时,故障会向外扩散——就像跳闸导致整栋楼停电一样。
Erlang 倾向相反的做法:隔离职责。如果一个小工作者表现异常,你可以丢弃并替换它,而不影响不相关的工作。
这些工作者如何协作?它们不会去触碰彼此的内部状态,而是发送消息——更像传便条而不是共用一块混乱的白板。
一个工作者可以说:“这里有一个新请求”、“这个用户断线了”或者“5 秒后再试一次”。接收方读取便条并决定如何处理。
关键好处是控制力:因为工作者是隔离的并通过消息通信,故障不太可能蔓延到整个系统。
理解 Erlang 的“actor 模型”的简单方式是把系统想象成由许多小而独立的工作者组成。
一个 actor 是一个自包含单元,拥有私有状态和一个邮箱。它做三件基本的事:
仅此而已。没有隐藏的共享变量,也不能“伸手去改别人的内存”。如果一个 actor 需要另一个的东西,就通过发送消息来请求。
当多个线程共享同一数据时,会出现竞态条件:两者几乎同时修改同一值,结果取决于时序。这类 bug 往往是间歇性的、难以复现的。
通过消息传递,每个 actor 拥有自己的数据,其他 actor 无法直接修改它。这并不能消除所有错误,但能显著减少由于同时访问同一状态引发的问题。
消息并非“免费到达”。如果某个 actor 接收消息的速度超过处理速度,它的邮箱(队列)就会增长。这就是回压:系统在间接告诉你“这里过载了”。
在实践中,你要监控邮箱大小并设定限额:丢弃负载、批量处理、抽样,或把工作推给更多工作者,而不是让队列无限增长。
想象一个聊天应用。每个用户可以有一个负责发送通知的 actor。当用户离线时,消息仍在到达——邮箱增长。设计良好的系统可能会限制队列长度、丢弃非关键通知,或切换到摘要模式,而不是让一个慢用户拖慢整个服务。
“Let it crash” 并不是纵容糟糕工程的口号,而是一种可靠性策略:当组件进入糟糕或意外状态时,它应当快速并公开地停止,而不是勉强维持。
与其在一个进程内为每个可能的边界情况写无数防御代码,Erlang 鼓励把每个工作者做小且专注。如果该工作者遇到真正无法处理的情况(损坏的状态、被破坏的假设、意外输入),它就退出。系统的另一部分负责把它带回运行状态。
这把问题从“我们如何防止失败?”转向“当失败发生时如何干净地恢复?”
到处进行防御性编程会把简单流程变成条件、重试和部分状态的迷宫。“Let it crash” 用一些进程内的复杂度换取:
核心观点是恢复应该是可预测且可复现的,而不是在每个函数里即兴处理。
它最适合那些可恢复且可隔离的失败:临时网络问题、恶意请求、卡住的工作者、第三方超时。
当崩溃可能导致不可逆伤害时,它就不合适,例如:
只有在重启既快速又安全时,崩溃才有用。实践中这意味着以已知良好状态重启工作者——通常通过重新加载配置、从持久化存储重建内存缓存,并在不掩盖破坏性状态的前提下恢复工作。
Erlang 的“let it crash” 之所以可行,是因为崩溃不会被随意丢弃。关键模式是 监督树:一个层级结构,监督者像经理一样,工作者做实际工作(处理呼叫、跟踪会话、消费队列等)。当工作者异常时,经理注意到并重启它。
监督者不会尝试在原地“修复”出问题的工作者。相反,它应用一个简单、一致的规则:如果工作者死掉,就启动一个新的。这样恢复路径是可预测的,减少了散落在代码各处的随意错误处理。
同样重要的是,监督者也能决定何时不重启——如果某个东西频繁崩溃,可能说明有更深层的问题,反复重启可能会让情况更糟。
监督不是一刀切的。常见策略包括:
良好的监督设计始于依赖关系图:哪些组件依赖哪些,什么叫“干净重启”。
如果一个会话处理器依赖于缓存进程,单独重启处理器可能会让它连接到一个坏的状态。把它们放在适当的监督者下(或一起重启)可以把混乱的故障模式变成一致且可复现的恢复行为。
如果 Erlang 是语言,OTP(Open Telecom Platform)就是把“let it crash” 变成可在生产中长期运行工具箱的那一套构件。
OTP 不是单一库,而是一组约定与现成组件(称为 behaviours),用于解决构建服务时那些乏味但关键的问题:
gen_server:用于长期运行、维护状态并逐条处理请求的工作者supervisor:用于根据明确定义的规则自动重启失败的工作者application:用于定义整个服务如何启动、停止以及如何打包发布这不是“魔法”。它们是有明确定义回调的模板,让你的代码接入一个已知的形状,而不是每个项目都发明一个新框架。
团队常常自建后台工作者、临时监控钩子和一次性的重启逻辑。表面上能用——直到用不了为止。OTP 通过推动大家朝同一套词汇和生命周期靠拢来降低这种风险。当新工程师加入时,他们不需要先学你家的定制框架;可以依赖在 Erlang 生态中被广泛理解的共享模式。
OTP 促使你按 进程角色 和 职责 思考:什么是工作者、什么是协调者、谁应该重启谁、什么不应自动重启。
它还鼓励良好习惯:清晰命名、显式启动顺序、可预测的关闭和内建的监控信号。结果是设计来持续运行的软件——能从故障中恢复、随着时间演化并在不需要频繁人工干预的情况下继续工作。
Erlang 的大思想——微小进程、消息传递和“let it crash”——如果没有 BEAM 虚拟机(VM),在生产中使用会困难得多。BEAM 是让这些模式显得自然而非脆弱的运行时。
BEAM 设计用于运行大量轻量进程。它不依赖少数操作系统线程并指望应用表现良好,而是由 VM 自己对 Erlang 进程进行调度。
实际好处是负载下的响应性:工作被切成小片段并公平轮转,这样就没有单个繁忙工作者长期主导系统。这与由许多独立任务组成的服务非常契合——每个任务做一点就让出时间。
每个 Erlang 进程有自己的堆和自己的垃圾回收。这是个关键细节:清理一个进程的内存不需要暂停整个程序。
同样重要的是,进程是隔离的。如果一个崩溃,它不会破坏其他进程的内存,VM 本身也保持存活。这种隔离是监督树可行的基础:故障被局部化,然后通过重启失败部分来处理,而不是把一切都摧毁重来。
BEAM 也以直接的方式支持分布式:你可以运行多个 Erlang 节点(独立的 VM 实例)并让它们通过发送消息互通。如果你理解了“进程通过消息通信”,分布就是同一理念的延伸——只是一些进程恰好运行在另一台节点上。
BEAM 并不是在承诺原始速度,而是在让并发、故障隔离和恢复成为默认,这样可靠性故事变得可实践而非空谈。
Erlang 最被谈论的技巧之一是热代码替换:在运行系统上更新部分代码以尽量减少停机(在运行时与工具支持下)。实用承诺不是“永远不重启”,而是“在不把短暂问题变成长期故障的情况下部署修复”。
在 Erlang/OTP 中,运行时可以同时保留一个模块的两个版本。已有进程可以继续用旧版完成工作,而新调用可以使用新版。这让你可以在不把所有人踢下线的情况下修补 bug、推出功能或调整行为。
做好了,这直接支持可靠性目标:更少的整体重启、更短的维护窗口以及当生产环境出问题时更快的恢复。
并非所有变更都能安全地在线替换。需要额外小心(或直接重启)的更改示例包括:
Erlang 提供了受控迁移的机制,但仍需设计升级路径。
当升级与回滚被视为常规操作而非罕见应急时,热升级最有效。这意味着从一开始就规划版本兼容性和明确的“撤销”路径。实践中,团队会把在线升级技术与分阶段发布、健康检查和基于监督的恢复相结合。
即便你从未使用 Erlang,教训仍然适用:把“安全变更”作为一等需求来设计系统,而不是事后补的事项。
实时平台并非关于绝对精确的时序,而是关于在不断出错的情况下保持响应:网络抖动、依赖变慢、用户流量激增。Joe Armstrong 所倡导的 Erlang 设计适配了这种现实,因为它假定会失败并把并发视为常态而非例外。
你会在以下场景看到 Erlang 风格思想的闪光:
大多数产品不需要“每个操作在 10 ms 内完成”这类硬性保证。它们需要 软实时:典型请求的延迟持续较低、部分故障时快速恢复、高可用性让用户很少感知到事故。
真实系统会遇到:
Erlang 的模型鼓励把每个活动(用户会话、设备、付款尝试)隔离开来,这样故障不会蔓延。与其构建一个试图“什么都处理”的大组件,不如把系统拆成小单元:每个工作者只做一件事、通过消息通信、若崩溃则干净重启。
这种从“防止每次失败”到“快速隔离并恢复”的转变,往往是让实时平台在压力下仍显稳定的关键。
Erlang 的声誉可能听起来像一种承诺:系统永远在线,因为它们会不断重启。现实更务实也更有用。“Let it crash” 是构建可靠服务的工具,而不是忽视棘手问题的借口。
常见错误是把监督当成掩盖深层 bug 的手段。如果某个进程在启动后立即崩溃,监督者可能不断重启它,最终导致 崩溃循环——消耗 CPU、刷满日志,并可能引发比原 bug 更大的故障。
良好系统会加入退避、重启强度限制以及明确的“放弃并升级”行为。重启应该恢复健康,而不是掩盖不变的错误假设。
重启进程通常容易;恢复正确状态并不容易。如果状态仅存在于内存中,你必须明确重启后什么才算“正确”:
容错并不能替代细致的数据设计,它只是迫使你对此更明确。
崩溃有用的前提是你能早期看到并理解它们。这意味着要投入到 日志、指标与追踪 中——不仅仅是“它重启了,所以没事”。你需要在用户感知到问题之前注意到增加的重启率、不断增长的队列和变慢的依赖。
即便有 BEAM 的优势,系统也会以普通方式失败:
Erlang 的模型帮助你隔离并恢复故障,但不能消除故障本身。
Erlang 最大的馈赠不是语法,而是一套构建在部件不可避免出错时仍能运行的习惯。你几乎在任何技术栈中都能应用这些习惯。
先把失败边界明确化。把系统拆成可以独立失败的组件,并确保每个组件有清晰契约(输入、输出,以及什么叫“出问题”)。
然后自动化恢复,而不是试图防止每个错误:
把这些习惯变成“真实”的一种方法是把它们内建到工具与生命周期中,而不是只写在代码里。例如,当团队使用 Koder.ai 通过 chat 协作编写 web、后端或移动应用时,工作流天然会推动明确规划(Planning Mode)、可重复部署以及带快照和回滚的安全迭代——这些概念与 Erlang 推广的运营思维一致:假定变化与失败会发生,并让恢复变得平淡无奇。
可以用你已有的工具近似实现“监督”模式:
在复制这些模式之前,先决定你真正需要什么:
如果你想要实操性的下一步,请在 /blog 中查看更多指南,或在 /docs 中浏览实现细节(若评估工具也可看 /pricing)。
Erlang 推广了一种务实的可靠性思维:假定部分会失败,并设计好失败之后发生的事情。
与其试图阻止每一次崩溃,它更强调 故障隔离、快速检测 和 自动恢复,这正适用于聊天、呼叫路由、通知和协调服务等实时平台。
在本文语境下,“实时”通常指 软实时(soft real-time):
它更关心避免停顿、螺旋式恶化和级联故障,而不是微秒级的硬性时限。
“并发为默认”意味着以许多小、相互隔离的工作单元来构建系统,而不是一两个大而紧耦合的组件。
每个工作单元负责很窄的职责(一个会话、一个设备、一条重试循环),这使扩展和故障隔离更容易实现。
轻量进程是可以大量创建的小型独立工作者。
实际好处包括:
消息传递是通过发送消息来协作,而不是共享可变状态。
这能减少一类并发错误(例如竞态条件),因为每个工作者拥有自己的内部状态,其他人只能通过消息间接请求更改。
当一个工作者接收消息的速度超过处理速度,其邮箱会增长,这就是回压(back-pressure)。
常用的处理方法有:
“Let it crash” 的意思是:当工作者进入无效或意外状态时,应快速失败而不是拖着苟延残喘。
恢复由结构化的机制(监督)负责,从而带来更简单的代码路径和可预测的恢复——前提是重启既安全又快速。
监督树是一种层级结构,监督者监视工作者并按规则重启它们。
通过集中式的恢复策略,你可以:
OTP 是一组标准模式(behaviours)和约定,使得 Erlang 系统能长期可运行且易于运维。
常见构件包括:
gen_server:用于有状态且按序处理请求的长期工作者supervisor:用于按规则自动重启失败的工作者application:用于定义服务如何启动、停止以及如何打包发布优点是使用共享、被广泛理解的生命周期而不是各自为政的框架。
即便不使用 Erlang,也可以把这些原则应用到其他栈:
更多内容请参见文中提到的 /blog 的相关指南和 /docs 中的实现细节。