KoderKoder.ai
价格企业教育投资人
登录开始使用

产品

价格企业投资人

资源

联系我们支持教育博客

法律信息

隐私政策使用条款安全可接受使用政策举报滥用

社交

LinkedInTwitter
Koder.ai
语言

© 2026 Koder.ai 保留所有权利。

首页›博客›Joe Armstrong 与 Erlang:让它崩溃以换取可靠平台
2025年7月09日·1 分钟

Joe Armstrong 与 Erlang:让它崩溃以换取可靠平台

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

Joe Armstrong 与 Erlang:让它崩溃以换取可靠平台

本文涉及的内容(及其为何仍然重要)

Joe Armstrong 不只是 Erlang 的共同创作者——他还是这套思想最清晰、最有说服力的讲解者。通过演讲、论文以及务实的观点,他普及了一个简单的理念:如果你想让软件保持在线,就要为失败而设计,而不是假装可以避免失败。

本文是对 Erlang 思维方式的导览,以及解释在构建可靠实时平台时这些思想为何依然重要——比如聊天系统、呼叫路由、实时通知、多玩家协调,以及那些在部分组件失常时仍需快速且一致响应的基础设施。

把“实时”讲清楚

“实时”并不总是指“微秒”或“硬性截止”。在许多产品中它意味着:

  • 用户能感知到的快速响应(没有莫名其妙的停顿)
  • 在高负载下行为可预测(可能变慢,但不应失控)
  • 在部分故障时服务仍能继续(一个坏组件不该拖垮全部)

Erlang 为电信系统而生,这类系统对这些期望是强制性的——也正是这种压力塑造了它最有影响力的理念。

我们将聚焦的三大支柱

不从语法入手,而是关注那些让 Erlang 著名并持续出现在现代系统设计里的概念:

  1. 并发为默认:用许多小的、相互隔离的工作者构建系统,而不是少数巨大的组件。
  2. 把容错当作设计目标:假设会有错误、超时和崩溃——并规划下一步该怎么做。
  3. “let it crash”:不要为每行代码做过度防御;快速失败并通过结构(而非临场补救)干净地恢复。

在此过程中,我们会把这些理念和 actor 模型与消息传递联系起来,用通俗的方式解释监督树和 OTP,并展示为什么 BEAM VM 让整套方法变得可行。

即使你不用 Erlang(甚至永远不会用),Armstrong 的框架仍为你提供了一份强有力的清单,帮助构建在现实环境混乱时仍能保持响应和可用的系统。

Joe Armstrong 的动机:构建能保持运行的系统

电信交换机和呼叫路由平台不能像许多网站那样“停机维护”。它们被期望全天候处理通话、计费事件和信令流量——通常对可用性和响应时间有严格要求。

Erlang 在 1980 年代末期源于爱立信,意在用软件而非专用硬件来满足这些现实。Joe Armstrong 和同事们并非单纯追求优雅;他们试图构建运营人员在持续负载、部分故障和复杂现实条件下也能信赖的系统。

“可靠”在实践中意味着什么

思维方式的关键转变在于:可靠性并不等于“从不失败”。在大型长期运行的系统中,总会出现故障:某个进程遇到意外输入、某个节点重启、网络链路抖动或某个依赖停顿。

因此目标变成:

  • 在部分失常时继续为用户提供服务
  • 快速检测故障
  • 自动恢复,尽量减少人工干预
  • 隔离故障,避免一个 bug 带垮全部

正是这种心态使得监督树和“let it crash”这样的想法显得合理:你把失败视为常态事件,而非灾难性的例外。

少些神话,多些问题解决

将故事讲成某位远见者的单点突破很诱人。更有用的观点更简单:电信的约束迫使人们在权衡上做出不同选择。Erlang 优先考虑并发、隔离和恢复,因为这些是保持服务在不断变化的世界中运行所需的实用工具。

这种以问题为中心的框架也是为何 Erlang 的教训今天仍然通用——任何地方只要可用性和快速恢复比完美预防更重要,它们都能派上用场。

并发为默认:许多小工作者

Erlang 的核心思想之一是,“同时做很多事”并不是事后加上的特性——而是你构建系统的常态。

轻量进程,通俗解释

在 Erlang 中,工作被拆成许多微小的“进程”。把它们想象成小工作者,各自负责一项任务:处理一个电话、跟踪一个聊天会话、监控一个设备、重试一次付款或监听一个队列。

它们是轻量的,意味着你可以在不需要巨大硬件的情况下拥有大量此类进程。与其让一个笨重的工作者尝试处理一切,不如用一群专注的小工作者——它们能快速启动、快速停止并快速被替换。

为什么“大一统程序”的故障传播方式不同

许多系统被设计成一个大程序,内部包含很多紧耦合的部分。当这类系统遇到严重错误、内存问题或阻塞操作时,故障会向外扩散——就像跳闸导致整栋楼停电一样。

Erlang 倾向相反的做法:隔离职责。如果一个小工作者表现异常,你可以丢弃并替换它,而不影响不相关的工作。

把消息传递当作“传便条”

这些工作者如何协作?它们不会去触碰彼此的内部状态,而是发送消息——更像传便条而不是共用一块混乱的白板。

一个工作者可以说:“这里有一个新请求”、“这个用户断线了”或者“5 秒后再试一次”。接收方读取便条并决定如何处理。

关键好处是控制力:因为工作者是隔离的并通过消息通信,故障不太可能蔓延到整个系统。

消息传递与 actor 模型(无术语版)

理解 Erlang 的“actor 模型”的简单方式是把系统想象成由许多小而独立的工作者组成。

Actors:只靠发送消息沟通的小工作者

一个 actor 是一个自包含单元,拥有私有状态和一个邮箱。它做三件基本的事:

  • 从邮箱里逐条接收消息
  • 更新自身的内部状态
  • 向其他 actor 发送消息

仅此而已。没有隐藏的共享变量,也不能“伸手去改别人的内存”。如果一个 actor 需要另一个的东西,就通过发送消息来请求。

避免共享状态能消除的那类 bug

当多个线程共享同一数据时,会出现竞态条件:两者几乎同时修改同一值,结果取决于时序。这类 bug 往往是间歇性的、难以复现的。

通过消息传递,每个 actor 拥有自己的数据,其他 actor 无法直接修改它。这并不能消除所有错误,但能显著减少由于同时访问同一状态引发的问题。

回压,像咖啡店排队一样解释

消息并非“免费到达”。如果某个 actor 接收消息的速度超过处理速度,它的邮箱(队列)就会增长。这就是回压:系统在间接告诉你“这里过载了”。

在实践中,你要监控邮箱大小并设定限额:丢弃负载、批量处理、抽样,或把工作推给更多工作者,而不是让队列无限增长。

一个具体例子:聊天通知

想象一个聊天应用。每个用户可以有一个负责发送通知的 actor。当用户离线时,消息仍在到达——邮箱增长。设计良好的系统可能会限制队列长度、丢弃非关键通知,或切换到摘要模式,而不是让一个慢用户拖慢整个服务。

“Let It Crash” 详解:快速失败,更快恢复

“Let it crash” 并不是纵容糟糕工程的口号,而是一种可靠性策略:当组件进入糟糕或意外状态时,它应当快速并公开地停止,而不是勉强维持。

它真正的含义

与其在一个进程内为每个可能的边界情况写无数防御代码,Erlang 鼓励把每个工作者做小且专注。如果该工作者遇到真正无法处理的情况(损坏的状态、被破坏的假设、意外输入),它就退出。系统的另一部分负责把它带回运行状态。

这把问题从“我们如何防止失败?”转向“当失败发生时如何干净地恢复?”

权衡:更少的防御检查、更清晰的逻辑

到处进行防御性编程会把简单流程变成条件、重试和部分状态的迷宫。“Let it crash” 用一些进程内的复杂度换取:

  • 更简单、更易读的代码路径
  • 更快地发现被破坏的假设
  • 集中且一致的恢复(因为恢复是被中央管理的)

核心观点是恢复应该是可预测且可复现的,而不是在每个函数里即兴处理。

适用场景与不适用场景

它最适合那些可恢复且可隔离的失败:临时网络问题、恶意请求、卡住的工作者、第三方超时。

当崩溃可能导致不可逆伤害时,它就不合适,例如:

  • 在没有持久化真理源的情况下造成数据丢失
  • 在安全关键的操作中“再试一次”不可接受

快速重启与已知良好状态

只有在重启既快速又安全时,崩溃才有用。实践中这意味着以已知良好状态重启工作者——通常通过重新加载配置、从持久化存储重建内存缓存,并在不掩盖破坏性状态的前提下恢复工作。

监督树:有意为失败而设计

发布可分享的演示
将项目放到自定义域名上,与团队分享稳定演示。
设置域名

Erlang 的“let it crash” 之所以可行,是因为崩溃不会被随意丢弃。关键模式是 监督树:一个层级结构,监督者像经理一样,工作者做实际工作(处理呼叫、跟踪会话、消费队列等)。当工作者异常时,经理注意到并重启它。

负责重启工作的管理者

监督者不会尝试在原地“修复”出问题的工作者。相反,它应用一个简单、一致的规则:如果工作者死掉,就启动一个新的。这样恢复路径是可预测的,减少了散落在代码各处的随意错误处理。

同样重要的是,监督者也能决定何时不重启——如果某个东西频繁崩溃,可能说明有更深层的问题,反复重启可能会让情况更糟。

重启策略(高层)

监督不是一刀切的。常见策略包括:

  • 一对一(one-for-one):仅重启失败的工作者。适合相互独立的任务。
  • 组重启:某个工作者失败时,一组相关进程一起重启。适合紧耦合、必须保持同步的组件。

依赖关系:需要你认真思考的部分

良好的监督设计始于依赖关系图:哪些组件依赖哪些,什么叫“干净重启”。

如果一个会话处理器依赖于缓存进程,单独重启处理器可能会让它连接到一个坏的状态。把它们放在适当的监督者下(或一起重启)可以把混乱的故障模式变成一致且可复现的恢复行为。

OTP:用于可靠服务的可重用构件

如果 Erlang 是语言,OTP(Open Telecom Platform)就是把“let it crash” 变成可在生产中长期运行工具箱的那一套构件。

OTP:一套经过验证的模式工具箱

OTP 不是单一库,而是一组约定与现成组件(称为 behaviours),用于解决构建服务时那些乏味但关键的问题:

  • gen_server:用于长期运行、维护状态并逐条处理请求的工作者
  • supervisor:用于根据明确定义的规则自动重启失败的工作者
  • application:用于定义整个服务如何启动、停止以及如何打包发布

这不是“魔法”。它们是有明确定义回调的模板,让你的代码接入一个已知的形状,而不是每个项目都发明一个新框架。

为什么标准模式胜过自定义框架

团队常常自建后台工作者、临时监控钩子和一次性的重启逻辑。表面上能用——直到用不了为止。OTP 通过推动大家朝同一套词汇和生命周期靠拢来降低这种风险。当新工程师加入时,他们不需要先学你家的定制框架;可以依赖在 Erlang 生态中被广泛理解的共享模式。

OTP 如何在日常架构中发挥作用

OTP 促使你按 进程角色 和 职责 思考:什么是工作者、什么是协调者、谁应该重启谁、什么不应自动重启。

它还鼓励良好习惯:清晰命名、显式启动顺序、可预测的关闭和内建的监控信号。结果是设计来持续运行的软件——能从故障中恢复、随着时间演化并在不需要频繁人工干预的情况下继续工作。

BEAM VM:让模型可行的运行时

让可靠性成为习惯
构建小型服务、隔离故障,并以 Koder.ai 为工作台快速迭代。
开始项目

Erlang 的大思想——微小进程、消息传递和“let it crash”——如果没有 BEAM 虚拟机(VM),在生产中使用会困难得多。BEAM 是让这些模式显得自然而非脆弱的运行时。

调度:公平性胜过“一个大线程”

BEAM 设计用于运行大量轻量进程。它不依赖少数操作系统线程并指望应用表现良好,而是由 VM 自己对 Erlang 进程进行调度。

实际好处是负载下的响应性:工作被切成小片段并公平轮转,这样就没有单个繁忙工作者长期主导系统。这与由许多独立任务组成的服务非常契合——每个任务做一点就让出时间。

隔离与“按进程”垃圾回收

每个 Erlang 进程有自己的堆和自己的垃圾回收。这是个关键细节:清理一个进程的内存不需要暂停整个程序。

同样重要的是,进程是隔离的。如果一个崩溃,它不会破坏其他进程的内存,VM 本身也保持存活。这种隔离是监督树可行的基础:故障被局部化,然后通过重启失败部分来处理,而不是把一切都摧毁重来。

分布式:多个节点,一个系统

BEAM 也以直接的方式支持分布式:你可以运行多个 Erlang 节点(独立的 VM 实例)并让它们通过发送消息互通。如果你理解了“进程通过消息通信”,分布就是同一理念的延伸——只是一些进程恰好运行在另一台节点上。

BEAM 并不是在承诺原始速度,而是在让并发、故障隔离和恢复成为默认,这样可靠性故事变得可实践而非空谈。

无需停机的升级(谨慎使用的热代码替换)

Erlang 最被谈论的技巧之一是热代码替换:在运行系统上更新部分代码以尽量减少停机(在运行时与工具支持下)。实用承诺不是“永远不重启”,而是“在不把短暂问题变成长期故障的情况下部署修复”。

“热代码”到底意味着什么

在 Erlang/OTP 中,运行时可以同时保留一个模块的两个版本。已有进程可以继续用旧版完成工作,而新调用可以使用新版。这让你可以在不把所有人踢下线的情况下修补 bug、推出功能或调整行为。

做好了,这直接支持可靠性目标:更少的整体重启、更短的维护窗口以及当生产环境出问题时更快的恢复。

不容忽视的限制

并非所有变更都能安全地在线替换。需要额外小心(或直接重启)的更改示例包括:

  • 状态结构变化(进程期望的数据格式与新代码不同)
  • 必须在服务间保持一致的协议或消息格式变更
  • 需要时间或协调的模式迁移

Erlang 提供了受控迁移的机制,但仍需设计升级路径。

思维方式:把升级和回滚当常规操作

当升级与回滚被视为常规操作而非罕见应急时,热升级最有效。这意味着从一开始就规划版本兼容性和明确的“撤销”路径。实践中,团队会把在线升级技术与分阶段发布、健康检查和基于监督的恢复相结合。

即便你从未使用 Erlang,教训仍然适用:把“安全变更”作为一等需求来设计系统,而不是事后补的事项。

Erlang 思想在实时平台中的优势

实时平台并非关于绝对精确的时序,而是关于在不断出错的情况下保持响应:网络抖动、依赖变慢、用户流量激增。Joe Armstrong 所倡导的 Erlang 设计适配了这种现实,因为它假定会失败并把并发视为常态而非例外。

常见的“实时”用例

你会在以下场景看到 Erlang 风格思想的闪光:

  • 消息与聊天:百万级小会话,每个有自己的状态和重试逻辑。
  • 实时通信:语音/视频信令、在线状态更新和会话协调。
  • 物联网协调:大批设备不定时上线、下线并重新出现。
  • 支付工作流:多步骤流程,某些步骤可能缓慢或不可用,需要补偿操作。

“软实时”通常意味着什么

大多数产品不需要“每个操作在 10 ms 内完成”这类硬性保证。它们需要 软实时:典型请求的延迟持续较低、部分故障时快速恢复、高可用性让用户很少感知到事故。

故障是常态:为它而设计

真实系统会遇到:

  • 连接丢失(移动网络、Wi‑Fi 切换)
  • 下游超时(依赖服务变慢)
  • 部分故障(某个区域或依赖降级)

Erlang 的模型鼓励把每个活动(用户会话、设备、付款尝试)隔离开来,这样故障不会蔓延。与其构建一个试图“什么都处理”的大组件,不如把系统拆成小单元:每个工作者只做一件事、通过消息通信、若崩溃则干净重启。

这种从“防止每次失败”到“快速隔离并恢复”的转变,往往是让实时平台在压力下仍显稳定的关键。

常见误解与现实限制

构建并赚取积分
通过创建关于 Koder.ai 的内容或使用推荐链接邀请他人来赚取积分。
赚取积分

Erlang 的声誉可能听起来像一种承诺:系统永远在线,因为它们会不断重启。现实更务实也更有用。“Let it crash” 是构建可靠服务的工具,而不是忽视棘手问题的借口。

重启不是创可贴

常见错误是把监督当成掩盖深层 bug 的手段。如果某个进程在启动后立即崩溃,监督者可能不断重启它,最终导致 崩溃循环——消耗 CPU、刷满日志,并可能引发比原 bug 更大的故障。

良好系统会加入退避、重启强度限制以及明确的“放弃并升级”行为。重启应该恢复健康,而不是掩盖不变的错误假设。

状态是难点

重启进程通常容易;恢复正确状态并不容易。如果状态仅存在于内存中,你必须明确重启后什么才算“正确”:

  • 是否应该从持久化存储重建?
  • 能否安全地重放事件(幂等性)?
  • 在途工作或部分应用的更新如何处理?

容错并不能替代细致的数据设计,它只是迫使你对此更明确。

仍然需要可观测性

崩溃有用的前提是你能早期看到并理解它们。这意味着要投入到 日志、指标与追踪 中——不仅仅是“它重启了,所以没事”。你需要在用户感知到问题之前注意到增加的重启率、不断增长的队列和变慢的依赖。

现实的运行限制依旧存在

即便有 BEAM 的优势,系统也会以普通方式失败:

  • 内存增长:内存泄漏、缓存或大堆导致
  • 邮箱积压:生产者速度超过消费者(延迟激增与超时)
  • 依赖故障:数据库、第三方 API、DNS 出问题时,仅重启代码不能解决根因

Erlang 的模型帮助你隔离并恢复故障,但不能消除故障本身。

今天如何应用这些教训(即便不用 Erlang)

Erlang 最大的馈赠不是语法,而是一套构建在部件不可避免出错时仍能运行的习惯。你几乎在任何技术栈中都能应用这些习惯。

把理念转成具体行动

先把失败边界明确化。把系统拆成可以独立失败的组件,并确保每个组件有清晰契约(输入、输出,以及什么叫“出问题”)。

然后自动化恢复,而不是试图防止每个错误:

  • 隔离组件:把有风险的工作放在独立进程/容器/线程中,避免一个崩溃污染全局。
  • 定义边界:超时、带退避的重试、熔断器和隔舱以阻止级联故障。
  • 使恢复变成常态:健康检查、自动重启和安全默认值,让系统快速回到已知良好状态。

把这些习惯变成“真实”的一种方法是把它们内建到工具与生命周期中,而不是只写在代码里。例如,当团队使用 Koder.ai 通过 chat 协作编写 web、后端或移动应用时,工作流天然会推动明确规划(Planning Mode)、可重复部署以及带快照和回滚的安全迭代——这些概念与 Erlang 推广的运营思维一致:假定变化与失败会发生,并让恢复变得平淡无奇。

在 Erlang 之外的起点

可以用你已有的工具近似实现“监督”模式:

  • 监督者:systemd、Kubernetes Deployments 或进程管理器(失败自动重启、就绪探针)
  • 进程隔离:对 CPU 密集或不受信任的任务使用独立服务
  • 消息传递:使用队列/流(RabbitMQ、SQS、Kafka)来解耦生产者与消费者、平滑突发负载

简短决策清单

在复制这些模式之前,先决定你真正需要什么:

  • 预期的失败模式:过载、部分故障、慢依赖、坏输入、内存泄漏
  • 延迟需求:你需要实时响应,还是允许最终一致处理?
  • 恢复目标:快速重启、优雅降级还是人工介入?
  • 团队技能与工具链:谁负责值班、可观测性与事故响应?

如果你想要实操性的下一步,请在 /blog 中查看更多指南,或在 /docs 中浏览实现细节(若评估工具也可看 /pricing)。

常见问题

为什么 Joe Armstrong 的 Erlang 思维今天仍然相关?

Erlang 推广了一种务实的可靠性思维:假定部分会失败,并设计好失败之后发生的事情。

与其试图阻止每一次崩溃,它更强调 故障隔离、快速检测 和 自动恢复,这正适用于聊天、呼叫路由、通知和协调服务等实时平台。

文中所说的“实时”用通俗说法是什么意思?

在本文语境下,“实时”通常指 软实时(soft real-time):

  • 响应感觉快速且一致
  • 在负载下行为保持可预测
  • 在部分故障时系统仍能继续工作

它更关心避免停顿、螺旋式恶化和级联故障,而不是微秒级的硬性时限。

Erlang 风格设计中的“并发为默认”是什么意思?

“并发为默认”意味着以许多小、相互隔离的工作单元来构建系统,而不是一两个大而紧耦合的组件。

每个工作单元负责很窄的职责(一个会话、一个设备、一条重试循环),这使扩展和故障隔离更容易实现。

什么是 Erlang 的“轻量进程”,为什么重要?

轻量进程是可以大量创建的小型独立工作者。

实际好处包括:

  • 可以为每个“事物”(用户/会话/设备)建一个进程
  • 故障局限于单个工作者
  • 重启工作比重启单体代价低得多
为什么 Erlang 更偏好消息传递而不是共享状态?

消息传递是通过发送消息来协作,而不是共享可变状态。

这能减少一类并发错误(例如竞态条件),因为每个工作者拥有自己的内部状态,其他人只能通过消息间接请求更改。

在 actor/消息系统中,什么是回压,应如何处理?

当一个工作者接收消息的速度超过处理速度,其邮箱会增长,这就是回压(back-pressure)。

常用的处理方法有:

  • 监控邮箱/队列大小
  • 设定上限(丢弃、抽样或限制)
  • 将负载分散到更多工作者
  • 优雅降级(例如对非关键通知切换为摘要模式)
“let it crash” 实际意味着什么(又不意味着什么)?

“Let it crash” 的意思是:当工作者进入无效或意外状态时,应快速失败而不是拖着苟延残喘。

恢复由结构化的机制(监督)负责,从而带来更简单的代码路径和可预测的恢复——前提是重启既安全又快速。

什么是监督树,为什么它对容错很关键?

监督树是一种层级结构,监督者监视工作者并按规则重启它们。

通过集中式的恢复策略,你可以:

  • 决定失败时应该重启什么
  • 通过限制/退避避免无休止的崩溃循环
  • 在组件必须同步时一起重启相关组
什么是 OTP,如何帮助构建可靠服务?

OTP 是一组标准模式(behaviours)和约定,使得 Erlang 系统能长期可运行且易于运维。

常见构件包括:

  • gen_server:用于有状态且按序处理请求的长期工作者
  • supervisor:用于按规则自动重启失败的工作者
  • application:用于定义服务如何启动、停止以及如何打包发布

优点是使用共享、被广泛理解的生命周期而不是各自为政的框架。

如果我不使用 Erlang,如何应用 Erlang 的经验?

即便不使用 Erlang,也可以把这些原则应用到其他栈:

  • 隔离风险工作(独立进程/容器/服务)
  • 增加超时、带退避的重试、熔断器和隔舱(bulkheads)
  • 自动化恢复(健康检查 + 失败重启)
  • 使用消息队列/流(RabbitMQ、SQS、Kafka)解耦组件

更多内容请参见文中提到的 /blog 的相关指南和 /docs 中的实现细节。

目录
本文涉及的内容(及其为何仍然重要)Joe Armstrong 的动机:构建能保持运行的系统并发为默认:许多小工作者消息传递与 actor 模型(无术语版)“Let It Crash” 详解:快速失败,更快恢复监督树:有意为失败而设计OTP:用于可靠服务的可重用构件BEAM VM:让模型可行的运行时无需停机的升级(谨慎使用的热代码替换)Erlang 思想在实时平台中的优势常见误解与现实限制今天如何应用这些教训(即便不用 Erlang)常见问题
分享
Koder.ai
使用 Koder 构建您自己的应用 立即!

了解 Koder 强大功能的最佳方式是亲自体验。

免费开始预约演示