一、Erlang虚拟机内存泄漏的典型症状
内存泄漏就像家里漏水的水龙头,刚开始可能只是滴水,但时间久了就会水漫金山。在Erlang系统中,最常见的症状就是beam.smp进程的内存占用曲线像坐了火箭一样往上窜。通过观察erlang:memory()的返回值,你会发现total值持续增长,而processes_used和binary_used往往是最主要的"罪魁祸首"。
举个实际案例:我们有个消息推送服务,运行一周后内存从2GB涨到了16GB。用recon_alloc:memory/1查看时,发现binary_allocators占用了85%的内存。这就像你的衣柜里塞满了从来不穿的衣服,系统内存被各种不再使用的二进制数据占得满满当当。
二、内存泄漏的定位工具箱
工欲善其事,必先利其器。Erlang生态里有几个神器值得常备:
- recon库:相当于内存检测的瑞士军刀
% 查看内存占用前100的进程
recon:proc_count(memory, 100).
% 分析二进制内存分布
recon:bin_leak(5). % 检查前5个可疑的二进制泄漏点
- etop:实时监控工具
% 启动类似top的监控界面
etop:start([{interval,5}, {sort, memory}]).
- erlang:process_info/2:当怀疑某个进程时
% 获取进程详细信息
erlang:process_info(Pid, [memory, messages, garbage_collection]).
最近我们还发现一个隐藏技巧:在Elixir环境下可以用:recon_lib模块的:node_stats_print/4函数,它能生成内存变化趋势图,比纯数字直观多了。
三、常见泄漏场景与修复方案
3.1 二进制数据泄漏
这是最常见的坑。Erlang的二进制分为heap binary和refc binary两种,后者容易引发泄漏。比如下面这个HTTP客户端代码就有问题:
download_large_file(Url) ->
{ok, {{_, 200, _}, _, Body}} = httpc:request(Url),
process_file(Body). % 如果Body很大且不立即处理就会泄漏
修复方案是使用二进制匹配及时释放:
download_large_file_fixed(Url) ->
{ok, {{_, 200, _}, _, Body}} = httpc:request(Url),
<<Chunk:1024/binary, Rest/binary>> = Body, % 分块处理
process_chunk(Chunk),
handle_remaining(Rest).
3.2 ETS表失控
ETS表就像共享内存,忘记清理就会泄漏。见过最夸张的案例是一个会话表设置了public属性却没人清理:
init() ->
ets:new(session_tab, [public, named_table, {write_concurrency,true}]).
store_session(UserId, Data) ->
ets:insert(session_tab, {UserId, Data}).
% 但缺少delete操作!
解决方案是加TTL或定期清理:
start_cleaner() ->
spawn_link(fun() ->
timer:sleep(5 * 60 * 1000),
clean_expired_sessions()
end).
3.3 进程字典滥用
进程字典(process dictionary)是全局变量,用不好就会翻车:
handle_call(Request, _From, State) ->
put(request_count, get(request_count) + 1), % 危险操作!
{reply, ok, State}.
应该改用状态显式传递:
handle_call(Request, _From, #{count := Count} = State) ->
NewState = State#{count := Count + 1},
{reply, ok, NewState}.
四、高级调试技巧
当常规方法失效时,可以祭出这些大招:
- 核心转储分析:
erl -env ERL_CRASH_DUMP_SECONDS 10 # 崩溃时自动生成dump
然后用crashdump_viewer工具分析:
crashdump_viewer:start().
- 二进制引用追踪:
% 在recon中启用高级追踪
recon_trace:calls({erlang, binary_to_term, '_'}, 10).
- 压力测试复现:
spawn_workers(0) -> ok;
spawn_workers(N) ->
spawn(fun() -> stress_test() end),
spawn_workers(N-1).
最近处理的一个分布式缓存泄漏问题,就是通过同时开启recon和Wireshark抓包,发现是TCP连接没有正确关闭导致的二进制堆积。
五、防患于未然的实践建议
根据我们团队的血泪教训,总结出这些最佳实践:
监控三件套:
- 使用observer_cli实时监控
- 配置Prometheus+Gragana看板
- 关键节点部署内存阈值告警
代码审查重点:
% 这些代码要重点检查 - binary_to_term/1不带安全限制 - 大消息直接跨进程传递 - 没有匹配的<<_/binary>>操作测试阶段检查:
# 在CI中加入内存检查 dialyzer --plt ./plt -r apps/*/src运维阶段保障:
% 热补丁加载示例 update_code() -> {module, _} = code:load_file(memory_fix), ok.
记住,内存泄漏就像牙疼,早发现早治疗。我们去年有个服务因为没及时处理ETS泄漏,最后不得不半夜紧急扩容,那场面简直像消防队救火。
六、不同场景下的应对策略
根据系统类型不同,处理策略也要灵活调整:
- Web服务:重点关注Cowboy连接池和JSON解析
- 数据库驱动:检查连接池和查询结果集处理
- 消息队列:监控进程邮箱堆积情况
- 实时计算:注意流处理中的中间状态
比如用Phoenix开发时,可以这样检查WebSocket内存:
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug Plug.Parsers,
parsers: [:json],
pass: ["*/*"],
json_decoder: Phoenix.json_library(),
length: 10_000_000 # 限制最大解析大小
end
七、总结与思考
处理Erlang内存泄漏就像侦探破案,需要:
- 监控系统提供线索(observer、recon)
- 重现现场收集证据(压力测试)
- 分析证据锁定嫌犯(ets、binary、process)
- 制定方案解决问题(代码优化、架构调整)
最后送大家一个检查清单:
- [ ] 所有binary都有明确的生命周期
- [ ] ETS表有自动清理机制
- [ ] 进程邮箱定期清空
- [ ] 关键操作都有内存限制
- [ ] 监控系统覆盖内存指标
记住,没有银弹能解决所有内存问题,但养成良好的编码习惯,能让你少走很多弯路。就像老司机开车,既要知道怎么修车,更要懂得如何避免故障。
评论