返回 HackerTubeYouTube2026年11月13日视频时长:42:02

Erlang 与 Elixir 的灵魂:构建持续提供服务的系统

The Soul of Erlang and Elixir • Sasa Juric • GOTO 2019

GCGOTO Conferences

完整字幕

中文
查看原视频
本篇目录

运行时是共同的线索

查看原文 00:14

软件系统需要在个别请求失败或运行缓慢时继续为用户服务。系统的其他部分如何继续工作?运维人员又如何在系统运行期间诊断问题?在这场 2019 年的演讲中,Saša Jurić 认为,BEAM 的轻量进程、调度和运行时内省让这些事情成为日常系统设计的一部分。他将演讲献给 Erlang 的主要发明者 Joe Armstrong,纪念他的遗产。

示例使用 Elixir,但 Jurić 讨论的是共同的 Erlang 虚拟机 BEAM。他有意把语言简化看作访问运行时的接口,并不是说 Erlang 与 Elixir 完全相同。他先介绍并发机制,再通过现场演示说明其用途,最后提出关于系统架构的前瞻性观点。

独立进程通过消息协作

查看原文 02:54

BEAM 轻量进程是一个顺序执行上下文,不是操作系统进程或线程。它执行普通的函数调用、分支和循环;多个这样的进程共同运行才形成并发。例如,spawn 可以从一个函数启动新进程,同时调用者继续执行。

每个进程都有独立的执行流、栈和堆,并分别进行垃圾回收。进程通过向指定接收方发送消息来协作;接收方的消息邮箱大致类似队列。Jurić 将这种模型比作相互独立、通过请求通信的微服务,但他的本地示例使用运行时支持的消息传递,无需 HTTP 和网络。

一个 BEAM 实例作为一个操作系统进程运行,可以容纳许多轻量进程。调度器线程负责执行它们。Jurić 的教学模型是假想一个队列:调度器取出进程,运行一小段时间,再转向下一个。实际调度器比这个示意复杂得多。他介绍当时的默认配置是每个可用 CPU 核心一个调度器,并提到运行时约 1.3 亿个进程的上限;这些是他在 2019 年的说法,不是对应用容量的保证。

查看原图

合成负载确定演示条件

查看原文 07:37

Jurić 的示例系统作为一个后台 BEAM 实例运行。他启动 10,000 个合成任务,每个任务对应一个进程:先运行一小段 CPU 密集循环,向一个已知进程报告成功,休眠一秒,然后重复。其他进程负责收集并展示报告。

仪表盘显示每秒约 10,000 次成功,调度器利用率约为 20%。任务并不同步,浏览器频繁绘制图表也会影响测量。虽然这台机器通常会使用四个调度器,他故意将其配置为一个,以便看清相互竞争的工作带来的影响。这些数字仅描述该合成演示,并非通用吞吐量基准。

独立计算进程限制异常的影响

查看原文 11:00

面向用户的示例计算从 1 到 n 的正整数之和:当 n = 3 时,答案是 6。每个浏览器连接都有自己的进程;请求到达时,连接进程启动一个短暂的计算进程。后者把答案发回后终止,再由连接进程转发给浏览器。连接进程随用户出现和消失,计算进程则随请求出现和消失。

查看原图

演示故意让输入 13 触发未处理异常。计算进程终止,但连接仍然存在,并收到终止通知,因此可以报告错误。与此同时,合成任务大致保持原有速率。Jurić 想说明的是:进程边界可以隔离故障,同时让故障可被观察;它不会让失败的请求自动成功。

抢占式调度同时隔离延迟

查看原文 15:40

原始求和实现还使用暴力 CPU 循环。输入九个 9 后,计算在唯一的调度器上运行约十秒,并使利用率达到 100%。但仪表盘仍显示每秒约 10,000 次成功,同一个连接也能在长计算运行期间处理其他输入。

查看原图

Jurić 将其归因于频繁的抢占式调度:运行时在进程之间切换,无需这个计算主动让出执行权。他在演讲中称切换间隔低于一毫秒。让各项活动更公平地推进,需要以单项活动的最高效率为代价。因此,他认为拆分进程的两个主要理由是隔离故障和隔离延迟。他从用户能否获得有用服务来定义可用性,同时承认在缺陷修复前仍有一部分服务不可用。

在线检查找到并终止失控计算

查看原文 18:42

负数输入暴露出更危险的缺陷:计算陷入无限 CPU 密集循环。其他任务最初仍能继续,但反复提交错误请求可能积累大量失控工作,最终让系统忙碌却无法产出有用结果。隔离争取了时间,却不能代替修复。

Jurić 扮演运维人员,收到 CPU 告警后发现日志没有提供有用的新记录。他连接到运行中系统的远程 IEx 交互式 shell,无需重启。他列出进程、检查属性,并比较每个进程在采样间隔内的归约计数变化。他将归约计数描述为与进程启动以来完成的工作量大致成比例;通过差值可以排列繁忙进程。他为演示编写的辅助函数展示过去一秒 CPU 消耗最高的进程,再次采样确认同一个进程仍排在首位。

查看原图

即使进程不响应消息,运行时也可以检查它。Jurić 获取当前堆栈跟踪,又用另一个演示辅助函数追踪调用,最多持续一秒或记录 50 次调用,以先发生者为准。随后,他向该进程发送 kill 退出信号。调度器利用率下降,连接进程报告错误,其他演示任务继续运行。他将其描述为运行时层面的取消能力,也可用于超时或用户请求;演示说明的是这个 BEAM 进程的行为,并不能概括所有外部工作。

查看原图

在输入边界修复并加载变更

查看原文 26:13

堆栈跟踪定位了函数和源代码行;调用轨迹显示递归不断重复,一个参数递增,而目标值始终为负数。Jurić 由此判断,系统深处的计算代码假设输入是正整数。业务领域允许这个假设,但系统边界应在输入到达计算代码之前拒绝无效值。

查看原图

演示中,他启用缺失的校验,移除针对 13 的特殊失败,并将暴力求和替换为 n × (n + 1) / 2。测试通过后,一个他明确称为取巧的部署任务重新编译并加载两个模块,无需重启运行中的系统。他不刷新页面,展示负数和零被拒绝、普通输入和 13 被接受、大数求和迅速完成。仪表盘上的合成负载仍在运行。这展示了在线修复,并不是完整的生产部署流程。

集群进程可以迁移,但分布式机制有实际限制

查看原文 29:51

多个 BEAM 实例可以连接成集群节点。Jurić 介绍了如何枚举节点、观察加入和离开、在节点上启动进程,以及沿用大部分面向进程的模型跨节点收发消息。节点是运行时实例;演示可以在同一台机器上运行多个节点,因此观察到的节点故障本身并不是物理机器故障测试。

演示中的命名服务就是进程。他启动两个节点,将服务命名为 foo 和 bar;停止节点 2 后,bar 在节点 1 上重新出现并从那里响应。这说明特定演示中的恢复行为,不能证明一般情况下的分布式一致性或可用性。

Jurić 明确表示,BEAM 分布式机制在运行时和库的层面都有严重问题,尽管他认为可以在不根本改变接口的情况下修复实现。他没有在演讲中详细展开这些问题。将演示用于生产决策时,必须保留这一前提。

Swarm 提供集群范围的命名层

查看原文 33:28

本地服务代码不需要知道分布式机制。演示引入 Swarm:一个更高层的库,负责在集群中启动和查找命名进程。它自己的进程会观察节点成员变化,并管理名称与进程之间的关联。一个调用启动命名服务,另一个按名称取得进程标识,再交给本地服务接口使用;目标进程可以在本地,也可以在远端。

查看原图

Jurić 将这种集成描述为三行代码,并强调编排逻辑运行在系统内部,而不是另一个需要单独管理的服务。这种简洁依赖演示使用的库及其运行时行为,并不能消除他刚才提到的分布式限制。

技术统一降低协调成本,也有代价

查看原文 36:28

Jurić 对比了自己参与过的两个真实系统。系统 A 组合多种技术和外部产品,满足 Web 服务、长请求、共享状态、持久化、后台工作和恢复等需求。系统 B 在一个项目和运行时内使用 Erlang 满足自身需求。幻灯片列出的是各自采用的组件示例,并非两个系统之间的受控性能测试。

查看原图

他认为,每台机器使用一门语言、一个项目和一个操作系统进程,可以减少开发环境、测试、CI、部署、指标、日志和调试之间的协调工作。他尤其看重降低新团队成员的学习负担,使他们能够参与系统中更多部分。通用编程语言也能以代码的灵活性表达系统行为,减少对多种独立配置格式的依赖。

这是一项权衡,并不是要求消除所有专业产品。Jurić 承认,示例中的内置方案缺少成熟产品提供的一些功能和性能,尽管它们满足了当时团队的需要。当整个系统和团队从中受益时,统一性才有价值;他明确说,统一本身不是目标。

简单起步的架构仍是愿景

查看原文 39:58

Jurić 最后提出一个他明确承认尚未完全实现的设想:创建一个 Elixir 项目,引入少量依赖,就能构建中小型面向 Web 的系统,并在同一系统中集成分布式支持和带复制的 SQL 持久化。他希望这样既能快速起步,又能在未来面对更大规模、更复杂的需求时避免从头重写。

这是他的前瞻性判断,并不保证五分钟配置就能得到可用于生产的分布式持久化,也不保证每个系统都能无需重写地扩展。演示给出的实际经验更具体:明确建模独立活动,观察它们的行为,并让系统在调查和修复故障期间继续提供有用服务。