Erlang/OTP 生产案例与性能调优

通过 WhatsApp、RabbitMQ 等经典案例深入理解 Erlang 在生产环境的应用,掌握 BEAM 虚拟机调优、内存管理和故障排查实战技巧。

Erlang 的理论优势需要通过生产环境的检验才能转化为可信赖的工程判断。从 WhatsApp 单节点支撑数百万并发连接,到 RabbitMQ 在复杂企业网络中的可靠消息投递,Erlang 已在电信、即时通讯、金融科技和物联网等领域积累了丰富的实战案例。本文将深入分析这些经典场景,提炼 Erlang/OTP 系统在生产环境中的调优策略、故障排查方法和性能优化技巧。

一、经典案例分析

1.1 WhatsApp:极简架构的极致效率

WhatsApp 是 Erlang 最著名的成功案例。在被 Facebook 收购时,WhatsApp 仅用数十台服务器就服务了 4 亿活跃用户,每台服务器承载 超过 200 万并发 WebSocket 连接

架构特点

维度设计选择效果
进程模型每个连接一个 Erlang 进程进程隔离保证单用户崩溃不影响全局
存储Mnesia 存储在线状态,Riak 存储消息亚毫秒级状态查询
部署精简功能,单节点高负载减少节点间通信开销
容错FreeBSD 操作系统 + Erlang 监督树年宕机时间仅数分钟

WhatsApp 的核心启发在于:单一技术栈的深度优化往往优于复杂技术栈的拼凑。WhatsApp 没有使用 Redis、Kafka 或微服务网格,而是将所有组件(消息队列、状态存储、连接管理)都实现在 Erlang 内部,避免了跨进程/跨网络通信的开销。

1.2 RabbitMQ:AMQP 代理的可靠性工程

RabbitMQ 是部署最广泛的开源消息代理,其核心用 Erlang 编写。RabbitMQ 将 Erlang 的分布式能力和监督树推广到了消息队列领域:

% RabbitMQ 队列进程树的简化示意
% 每个队列由一组协作进程组成:
-
  amqqueue_process        % 队列主进程
  ├── amqqueue_slave      % 镜像副本( HA 模式)
  ├── amqqueue_msg_store  % 消息存储
  └── amqqueue_index      % 索引管理

RabbitMQ 的设计亮点是队列进程的细粒度监督:消息投递失败不会导致整个代理崩溃,而是触发单个队列的重试或死信路由。Erlang 的轻量级进程使得这种细粒度容错在百万消息/秒的吞吐下仍然可行。

1.3 Ericsson AXD 301:电信级交换机

Erlang 的诞生地爱立信,将 OTP 用于构建 AXD 301 ATM 交换机。该系统的核心指标验证了 Erlang 的设计理念:

  • 可用性:99.9999999%(九个 9),年宕机时间仅 31 毫秒
  • 热更新:在不中断服务的情况下更新数百万行代码
  • 并发:单节点管理数百万并发通话状态

这些指标不是营销数字,而是电信行业的硬性合规要求。Erlang 的「let it crash」哲学和监督树容错机制正是为满足此类需求而设计。

二、BEAM 虚拟机调优

2.1 调度器调优

% 查看调度器状态
> erlang:system_info(schedulers_online).
8

> erlang:statistics(runtime).
{2534,2534}  % {TotalRunTime_ms, SinceLastCall_ms}

> erlang:statistics(garbage_collection).
{1234,2345678,0}  % {GCCount, WordsReclaimed, 0}

调度器绑定策略:

# 线程绑定策略
erl +sbt db    # 数据库绑定(减少缓存竞争)
erl +sbt ns    # 无共享处理器(线程迁移自由)

# CPU 拓扑感知
erl +sct L0-3c0-3p0N0:L4-7c0-3p1N1
# NUMA 节点 0 使用逻辑 CPU 0-3,NUMA 节点 1 使用逻辑 CPU 4-7

2.2 内存管理调优

Erlang 使用分代垃圾回收,每个进程拥有独立的堆空间。关键内存参数:

# 设置进程初始堆大小(默认 ~233 words)
erl +hmbs 4194304   # 最小二进制虚拟堆 4MB

# 强制更大的初始堆,减少 GC 频率(适合长生命周期进程)
erl +hms 1048576    # 最小堆大小 1MB

# 启用 off-heap message passing(减少大消息复制)
erl +hmqd off_heap

2.3 系统限制调整

# 文件描述符限制(ulimit -n)必须大于最大连接数
ulimit -n 1000000

# Erlang 进程限制
erl +P 2000000      # 最大 200 万进程

# ETS 表限制(默认 1400)
erl +e 10000        # 最大 1 万个 ETS 表

# Atom 表限制(防止 atom 表溢出攻击)
erl +t 1000000      # 最大 100 万 atoms

三、性能监控与诊断

3.1 Observer 工具

Observer 是 Erlang 内置的图形化监控工具,可以查看进程、ETS 表、Mnesia、应用程序和负载分布:

> observer:start().  % 启动 GUI

关键监控指标

指标健康值告警阈值
CPU 使用率< 70%> 90%
内存使用率< 80%> 95%
进程数< 50% 上限> 80% 上限
消息队列最大长度< 100> 1000
GC 频率< 10 次/秒> 50 次/秒

3.2 命令行诊断

% 查找消息队列堆积的进程
> [P || P <- processes(), 
        {message_queue_len, L} <- [process_info(P, message_queue_len)], 
        L > 1000].

% 查看进程当前活动
> process_info(Pid, current_function).
{current_function,{some_module,some_function,2}}

% 查看进程历史调用栈
> process_info(Pid, backtrace).

% 系统内存分布
> erlang:memory().
[
 {total, 123456789},
 {processes, 45678912},
 {processes_used, 41234567},
 {system, 77777777},
 {atom, 1048576},
 {atom_used, 987654},
 {binary, 12345678},
 {code, 23456789},
 {ets, 3456789}
]

% erts_alloc 内存分配器统计
> instrument:allocations().

3.3 Recon 库:生产环境诊断

Recon 是 Erlang 生态中最实用的生产诊断库:

% 查看 CPU 占用最高的 N 个进程
> recon:proc_count(reductions, 5).
[
 {<0.123.0>,1234567,[{current_function,{mod,fun,2}},{name,my_worker}]},
 ...
]

% 查看内存占用最高的进程
> recon:proc_count(memory, 10).

% 查看 binary 引用泄漏
> recon:bin_leak(5).

% 安全地获取进程状态(避免阻塞调用进程)
> recon:get_state(Pid).

% 查看节点的 TCP/UDP 端口占用
> recon:tcp().
> recon:udp().

% 进程调度器统计
> recon:scheduler_usage(1000).  % 采样 1 秒

四、常见故障排查

4.1 内存泄漏诊断

Erlang 内存泄漏通常不是传统意义上的「内存无法释放」,而是以下模式:

% 1. 消息队列堆积(进程消费速度 < 生产速度)
% 诊断:
> [{Pid, Len} || Pid <- processes(),
                 {message_queue_len, Len} <- [process_info(Pid, message_queue_len)],
                 Len > 1000].
% 修复:增加消费者、优化处理逻辑或增加背压

% 2. ETS 表未清理
% 诊断:
> length(ets:all()).
% 修复:使用 ets:delete/1,或设置 heir 进程

% 3. Binary 引用泄漏(sub-binary 持有大 binary 引用)
% 诊断:
> recon:bin_leak(10).
% 修复:使用 binary:copy/1 切断引用链

% 4. Atom 表溢出
% 诊断:
> erlang:memory(atom).
% 修复:避免 list_to_atom/1,使用 list_to_existing_atom/1

4.2 性能瓶颈定位

% CPU 瓶颈
> c:bt(Pid).  % 查看进程阻塞原因

% IO 瓶颈
> file:advise(File, Offset, Length, Advice).

% Mnesia 死锁
> mnesia:system_info(db_nodes).
> mnesia:info().

% 网络分区诊断
> net_adm:ping(Node).
> nodes(connected).

4.3 日志与追踪

% 使用 Logger(OTP 21+)
logger:error("Connection failed: ~p", [Reason]).

% 动态开启进程追踪
> recon_trace:calls({module, function, 2}, 10).
% 追踪 module:function/2 的前 10 次调用

% 使用 redbug(eheap 安全的追踪)
> redbug:start("mymodule:myfunction->return", 
               [{msgs, 100}, {time, 5000}]).

五、部署最佳实践

5.1 Release 构建

% rebar3 构建 Release
$ rebar3 release

% 生产模式运行
_build/default/rel/myapp/bin/myapp start
_build/default/rel/myapp/bin/myapp status
_build/default/rel/myapp/bin/myapp stop

% 远程控制台
_build/default/rel/myapp/bin/myapp remote_console

5.2 容器化注意事项

FROM erlang:26-alpine

WORKDIR /app
COPY _build/prod/rel/myapp .

# Erlang 需要 epmd 进行节点发现
EXPOSE 4369
# 分布式 Erlang 端口范围
EXPOSE 9100-9150

# BEAM 在容器中需要正确的 hostname 解析
ENV ERL_INETRC=/app/inetrc

CMD ["bin/myapp", "foreground"]
# .inetrc
{host, {10,0,1,1}, ["erlang-node-1"]}.
{host, {10,0,1,2}, ["erlang-node-2"]}.

5.3 热更新的正确姿势

-module(hot_upgrade).
-export([upgrade/0]).

upgrade() ->
    % 1. 生成 appup 文件
    % 2. 构建 upgrade relup
    % 3. 在线安装
    {ok, _Vsn} = release_handler:install_release("1.1.0"),
    release_handler:make_permanent("1.1.0").

OTP 的热更新不是魔法,它要求:

  • 函数签名保持不变,或提供转换函数
  • 状态数据结构变更时使用 code_change/3 回调
  • 数据库 Schema 变更需单独处理(Mnesia 支持表结构变更)
  • 避免在关键路径上更新(先灰度,后全量)

六、总结

Erlang/OTP 的生产价值不仅在于语言特性,更在于经过数十年电信级场景锤炼的工程方法论。监督树设计模式让容错从编码规范变为运行时保证,BEAM 虚拟机的进程隔离让系统在面对未知故障时表现出惊人的韧性。

从 WhatsApp 到 RabbitMQ,成功的 Erlang 系统都遵循相似的原则:最小化外部依赖、最大化进程隔离、拥抱失败而非防御失败。在微服务架构日益复杂的今天,Erlang 的「小系统大抽象」哲学反而显得愈发珍贵——它提醒我们,可靠性的本质不是增加更多组件,而是让每个组件都具备失败自愈的能力。

对于希望引入 Erlang 的团队,建议从非关键路径的边缘服务开始(如实时通知、状态推送、后台任务调度),在积累运维经验后再逐步扩展核心链路。Observer 工具和 Recon 库应成为日常运维的标准装备,它们提供的进程级洞察能力是其他运行时难以比拟的。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「erlang」更多文章

  1. Erlang 分布式编程:节点互联与集群部署
  2. Elixir 入门与 Phoenix Web 框架实战
  3. Erlang OTP 框架:构建工业级并发应用