一、什么是内存泄漏?为什么Erlang也会遇到?
内存泄漏就像你家水龙头没关紧,水一直流却用不上。在Erlang中,虽然虚拟机有垃圾回收机制,但某些情况下资源仍会像漏水的桶一样持续增长。比如进程长期持有不再需要的数据、ETS表忘记删除、二进制大对象未及时释放等。
举个实际例子:
%% 技术栈:Erlang/OTP 25
%% 有问题的代码:进程字典滥用导致泄漏
leaky_function() ->
put(heavy_data, lists:duplicate(1000000, 42)), % 存储1MB数据到进程字典
do_some_work(),
% 忘记执行erase(heavy_data)
ok.
这个案例中,每次调用函数都会在进程字典留下"数据垃圾",就像在房间里不断堆放快递盒却不清理。
二、如何发现内存泄漏?
2.1 观察系统指标
使用Erlang自带的工具就像检查汽车的仪表盘:
%% 查看内存概况
erlang:memory().
%% 输出示例:
%% [{total,2097463104},
%% {processes,1084212224},
%% {ets,32883328}]
如果processes或ets部分内存持续增长,就像油箱表一直往右偏,就要警惕了。
2.2 使用recon工具
这个第三方工具就像给虚拟机做X光检查:
%% 安装后使用:
recon:proc_count(memory, 5). % 显示内存前5的进程
recon:bin_leak(5). % 检查二进制泄漏
2.3 实战诊断示例
假设我们发现一个ETS表异常:
%% 技术栈:Erlang/OTP 25
%% 泄漏场景:不断增长的ETS表
init() ->
ets:new(cache, [public, ordered_set]), % 创建ETS表
ok.
handle_request(Data) ->
ets:insert(cache, {now(), Data}), % 持续插入但不清理
ok.
通过recon:ets_count()可以看到这个表的大小像吹气球一样膨胀。
三、常见泄漏场景及修复方案
3.1 进程堆积
就像餐厅里不断来客人却没人离开:
%% 问题代码:进程不退出
start_worker() ->
spawn(fun() ->
receive _ -> ok end % 阻塞但永不结束
end).
fix_worker() ->
spawn(fun() ->
receive
stop -> ok;
Msg -> handle(Msg)
after 30000 -> exit(normal) % 30秒超时自动退出
end
end).
3.2 二进制大对象
处理图片时容易踩坑:
%% 危险操作:二进制未及时释放
process_image(Data) ->
Bin = read_large_file(Data), % 读取10MB图片
Thumbnail = resize(Bin),
% 忘记binary:copy/1或主动释放
{ok, Thumbnail}.
%% 正确做法
safe_process_image(Data) ->
Bin = read_large_file(Data),
try resize(Bin) of
Thumbnail -> {ok, Thumbnail}
after
erlang:binary_to_list(Bin) % 强制释放内存
end.
3.3 定时器泄漏
就像忘记关掉的闹钟:
%% 错误示例:重复创建定时器
schedule() ->
erlang:send_after(1000, self(), tick),
ok.
%% 修复方案:取消旧定时器
schedule_fixed() ->
case get(timer_ref) of
undefined -> ok;
OldRef -> erlang:cancel_timer(OldRef)
end,
Ref = erlang:send_after(1000, self(), tick),
put(timer_ref, Ref),
ok.
四、高级调试技巧
4.1 崩溃dump分析
当BEAM虚拟机崩溃时,会生成dump文件。使用erl_crash.dump分析工具:
# 在Linux终端执行
strings erl_crash.dump | grep -A 10 '=proc:' | less
这就像查看飞机的黑匣子,能找到内存暴涨前的最后状态。
4.2 压力测试中的内存监控
用TSUNG工具模拟负载时,可以这样观察:
%% 在测试节点上执行
observer:start(),
%% 切换到Memory标签页观察曲线
4.3 自定义内存监控
编写自己的监控模块:
-module(mem_monitor).
-export([start/0]).
start() ->
spawn(fun() ->
loop(erlang:memory())
end).
loop(PrevMem) ->
Current = erlang:memory(),
case Current > PrevMem of
true -> alert_memory_jump(PrevMem, Current);
false -> ok
end,
timer:sleep(5000),
loop(Current).
五、预防胜于治疗
5.1 代码审查要点
- 检查所有
spawn调用是否有退出条件 - 确认每个
ets:new都有对应的ets:delete - 大二进制处理使用引用计数
- 定时器必须有取消机制
5.2 监控体系搭建
建议在生产环境部署:
- Prometheus + Grafana监控内存曲线
- 设置
erlang:system_monitor阈值 - 定期执行
recon_alloc:settings/1检查内存分配器
5.3 内存优化技巧
%% 使用binary:referenced_byte_size/1
check_bin_leak(Bin) ->
case binary:referenced_byte_size(Bin) > 1024*1024 of
true -> warning;
false -> ok
end.
%% 及时压缩大消息
compress_large_term(Term) ->
case erts_debug:size(Term) > 10000 of
true -> {compressed, term_to_binary(Term)};
false -> Term
end.
六、真实案例剖析
某电商平台在促销时出现的内存泄漏:
%% 问题代码:购物车进程泄漏
handle_call({add, Item}, _From, State) ->
NewItems = [Item | State#state.items],
{reply, ok, State#state{items=NewItems}}.
%% 诊断发现:购物车进程平均持有200MB商品数据
%% 修复方案:定期清理或分页存储
handle_call({add, Item}, _From, State) ->
NewItems = case length(State#state.items) > 100 of
true -> lists:sublist([Item | State#state.items], 100);
false -> [Item | State#state.items]
end,
{reply, ok, State#state{items=NewItems}}.
七、工具链推荐
- recon:内存分析瑞士军刀
- etop:类似Linux top的Erlang版
- observer_cli:终端版的observer
- memsup:OTP自带的内存监控
- fprof:性能分析时顺带检查内存
安装示例:
%% 在rebar.config中添加
{deps, [
{recon, "2.5.1"}
]}.
八、总结与最佳实践
内存泄漏就像慢性病,初期不易察觉但危害巨大。我们的防治策略是:
- 预防阶段:代码审查时重点关注资源生命周期
- 检测阶段:部署多层级监控(进程/ETS/二进制)
- 诊断阶段:使用recon+observer组合拳
- 修复阶段:优先解决持续增长型泄漏
- 优化阶段:对高频操作实施内存限制
记住三个黄金法则:
- 每个
spawn都要有退出路径 - 每个
ets:new都要配对ets:delete - 大二进制要像对待打开的文件一样及时关闭
最后送大家一个检查清单:
%% 内存安全快速自查表
checklist() ->
[
{process_count, length(processes()) < 10000},
{ets_memory, ets:info(system_memory) < 100000000},
{binary_memory, binary:memory() < 50000000}
].
评论