引言
Erlang 的并发能力不是「看起来像并发」——是 BEAM 虚拟机的多调度器在物理核上并行运行百万级进程。理解调度器如何轮转、削减、迁移进程,是写出可预测高性能 Erlang 的前提。本文深入 BEAM:调度模型、ETS、内存与 GC、以及性能监控手段。
前置:/erlang-concurrency-actors/(进程与消息)、/erlang-otp-framework/(进程骨架)、/erlang-ets-caching/(ETS 数据)。
目录
- 1. BEAM 虚拟机架构
- 2. 进程与调度器模型
- 3. 调度策略:公平与抢占
- 4. 进程削减与迁移
- 5. ETS 与进程协作
- 6. 内存管理与垃圾回收
- 7. 性能监控与调优
- 8. 与 OS 线程的映射
- 9. 实战:高并发进程设计
- 10. 速查表与一句话记忆
- 延伸阅读
1. BEAM 虚拟机架构
1.1 层次结构
Erlang/Elixir 源码
↓ 编译
BEAM 字节码(.beam)
↓ 解释执行
BEAM 虚拟机(寄存器机 + 模拟器)
↓ 映射
OS 线程(每核一个调度器)
1.2 BEAM 特点
✓ 寄存器机(x 寄存器 + 栈)
✓ 指令精简,每指令多栈帧
✓ 一次分配一页栈(进程很小)
✓ 进程间无共享内存(消息传递)
记忆:BEAM = 寄存器机虚拟机——源码编译成字节码、模拟器解释执行、每个调度器映射一个 OS 线程;进程小、消息传递无共享内存。
2. 进程与调度器模型
2.1 进程 vs 调度器
OS 线程(调度器):每核一个,真正并行执行
Erlang 进程:轻量用户态进程,由调度器轮转
一个 Erlang 进程很小(初始 ~300 字),百万级无压力
2.2 查看调度器与进程
erlang:system_info(schedulers). % 调度器数量(默认 = 核数)
erlang:system_info(process_count). % 当前进程数
erlang:processes(). % 所有进程列表
记忆:调度器映射 OS 线程(默认每核一个)、Erlang 进程是用户态轻量进程(初始几百字节)——百万进程靠多调度器并行轮转。
3. 调度策略:公平与抢占
3.1 抢占式调度
BEAM 采用抢占式:进程运行一段时间(reduction 计数)后被抢占,让其他进程执行——不是靠操作系统的 time slice。
reduction 计数:每个进程累计执行的指令量
达到阈值 → 进程被移出运行队列,换下一个
3.2 为什么重要
✓ 单个进程不阻塞系统(除非 dirty NIF)
✓ 公平:所有进程都有 CPU
✓ 实时性:响应进程不被饿死
记忆:BEAM 抢占式调度——进程按 reduction(指令量)计数、到阈值就让出 CPU;任何进程都不能独占系统,保证公平与实时响应。
4. 进程削减与迁移
4.1 运行队列结构
每个调度器有自己的运行队列(优先队列):
运行队列:max → normal → low(优先级)
⇅ 负载均衡:调度器空闲时 steal 别的队列进程(migration)
4.2 进程迁移
当某个调度器很忙、另一个空闲时,BEAM 会迁移进程(migrate)保持负载均衡。这带来 Cache 亲和性问题——绑核的 NIF/ETS 要注意。
% 绑定进程到调度器(默认由 BEAM 分配)
process_flag(scheduler, 3). % 绑定到调度器 3
erlang:system_flag(scheduler_bind_type, spread).
记忆:调度器有独立运行队列(max/normal/low),负载不均衡时 BEAM 迁移进程(migration)——追求负载均衡;对绑核优化的 NIF/ETS 可用 process_flag 绑定调度器。
5. ETS 与进程协作
5.1 ETS 无锁读
ETS 表允许无锁并发读(读多写少最优),写会锁对应桶:
Tab = ets:new(mytab, [set, public, {write_concurrency, true}]).
% 并发读:所有调度器可并行
Val = ets:lookup(Tab, Key).
5.2 写并发配置
% 高频写场景开写并发(分段锁)
ets:new(tab, [set, public, {write_concurrency, true}, {read_concurrency, true}])
5.3 ETS 与进程最佳实践
✓ 热点只读数据 → ETS + 读并发(或 :persistent_term)
✓ 跨进程共享状态 → ETS(避免 GenServer 瓶颈)
✓ 写热点 → 分表分桶
记忆:ETS 无锁并发读、写锁桶——读多写少用 read_concurrency、写热点用 write_concurrency 分段锁;跨进程共享状态用 ETS 而非单个 GenServer 瓶颈。
6. 内存管理与垃圾回收
6.1 进程私有堆
每个进程有自己的堆(无全局共享内存),GC 是分代式(年轻代快速回收):
进程私有堆 → 年轻代(快速 GC)→ 老年代(major GC)
进程用完即回收(堆整个释放)
6.2 COW:写时复制
BEAM 的大二进制(> 64 字节)用引用计数 + 写时复制——复制进程共享同一二进制,只有改写才拷贝:
Big = binary:copy(<<1:8000000>>), % 8MB 二进制
send_to_other_process(Big), % 只传引用,不拷贝
6.3 GC 观测
erlang:memory(total). % 总内存
erlang:system_info(processes_memory).
记忆:BEAM 无全局共享内存——进程私有堆 + 分代 GC(年轻代快回收);大二进制引用计数 + 写时复制(跨进程只传引用)——这是 Erlang 高内存效能的基石。
7. 性能监控与调优
7.1 关键指标
erlang:statistics(scheduler_wall_time). % 每个调度器的忙碌率
erlang:statistics(total_run_queue_lengths). % 运行队列长度(系统负载)
erlang:statistics(reductions). % reduction 总数(吞吐)
erlang:system_info(context_switches). % 上下文切换次数
7.2 调优方向
| 症状 | 对策 |
|---|---|
| 调度器全忙 | 加核 / 减计算(优化代码) |
| 运行队列暴涨 | 提高并发度(更多进程) |
| 上下文切换过高 | 减少同步调用、批量消息 |
| 单进程瓶颈 | 拆多进程并行 |
7.3 观察工具
observer(图形界面:进程/内存/负载):erlang.trace_pattern(函数调用追踪)fprof/eprof(CPU 性能分析)
记忆:性能监控四指标——scheduler_wall_time(忙碌率)、run_queue(负载)、reductions(吞吐)、context_switches(切换);observer/fprof 观察进程与热点函数。
8. 与 OS 线程的映射
8.1 调度器绑核
BEAM 默认调度器数 = 可用核数,可配置绑定策略:
# VM 启动参数
+SDcpu 4 # 4 个 CPU 调度器
+S 16 # 16 个调度器线程
+sbwt none # 关闭调度器忙等待(省电)
8.2 脏调度器
CPU 密集/IO 阻塞的 NIF 任务用独立脏调度器,不占普通调度器:
+SDdirty 4 # 4 个脏调度器
% dirty NIF 自动路由到脏调度器
erlang:system_flag(dirty_cpu_schedulers_online, 4).
记忆:调度器 = OS 线程,默认每核一个(+SDcpu 控制)、脏调度器(+SDdirty)承接 NIF 长任务不占普通调度器——CPU 核是 Erlang 并行的天花板。
9. 实战:高并发进程设计
9.1 模式:一连接一进程
% 每个客户端连接 = 独立进程(gen_server/spawn_link)
% 天然隔离:一个崩溃不影响其他
spawn_monitor(fun() -> handle_connection(Conn) end).
9.2 消息批量处理
% 用 select_receive 批量收消息,降低上下文切换
receive_batch(N) when N > 0 ->
receive
{msg, M} -> process(M), receive_batch(N - 1)
after 0 -> done
end.
9.3 吞吐 vs 延迟
✓ 吞吐优先:尽量多进程并行 + 批量消息
✓ 延迟优先:减少每消息处理路径 + 实时优先级进程
记忆:高并发设计三招——一连接一进程(隔离)、批量收消息(降切换)、按吞吐/延迟权衡优先级——让 BEAM 调度器满载并行。
10. 速查表与一句话记忆
| 环节 | 关键点 |
|---|---|
| VM | 寄存器机 + 模拟器 |
| 调度器 | 每核一个 OS 线程 |
| 抢占 | reduction 计数让出 CPU |
| 迁移 | 负载均衡 steal 进程 |
| ETS | 无锁读 + 写桶锁 |
| GC | 私有堆 + 分代 + COW |
| 监控 | scheduler_wall_time |
| 绑核 | +SDcpu / process_flag |
一句话记忆:BEAM 运行时 = 寄存器机虚拟机,源码编译成字节码由模拟器执行;并发本质是每核一个调度器(OS 线程)并行轮转百万级轻量进程;抢占式调度靠 reduction 计数(到阈值让出 CPU,保证公平实时),负载不均衡时进程跨调度器迁移;ETS 提供无锁并发读(写锁桶、write_concurrency 分段锁),跨进程共享状态用 ETS 避免 GenServer 瓶颈;内存是进程私有堆 + 分代 GC + 大二进制引用计数写时复制(无全局共享内存);监控看 scheduler_wall_time/run_queue/reductions,绑定用 +SDcpu 与 process_flag,NIF 长任务走脏调度器——理解调度模型才能写出可预测的高性能 Erlang。"
延伸阅读
- /erlang-concurrency-actors/ — 进程与消息模型
- /erlang-otp-framework/ — OTP 进程骨架
- /erlang-ets-caching/ — ETS 高速数据
- /erlang-ports-nif/ — NIF 与调度器交互
- /erlang-hot-code-upgrade/ — 运行时热升级
- [[os]] — 操作系统线程与调度
- [[cpp]] — C/C++ 性能视角
- Erlang 调度器文档
- BEAM 虚拟机深度文章
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。