前言
对服务端程序来说, 内存泄漏是经常会面临的问题. 使用 erlang 的情况下, 不用程序员手动管理内存. 如果不写 c driver, 一般的内存问题还是很容易定位的. 这篇 blog 对常见的内存泄漏类型, 排查手段做个小结.
内存泄漏类型
process 泄漏
如果没有 etop
iex(xxxx@xxxx.)1> :erlang.system_info(:process_count)
5369
可以通过 process_count 来获取 erlang vm 中已分配的 process 数量. 若 process 数量和业务实际需要不吻合, 则需要排查.
消息堆积
iex(xxxx@xxxx.)5> spawn fn -> :etop.start([sort: :msg_q]) end
#PID<0.6255.1>
========================================================================================
'xxxx@xxxx.' 03:04:46
Load: cpu 0 Memory: total 147234 binary 2839
procs 5371 processes 59008 code 42641
runq 0 atom 1722 ets 8239
Pid Name or Initial Func Time Reds Memory MsgQ Current Function
----------------------------------------------------------------------------------------
<7796.0.0> init '-' 339058 29540 0 init:loop/1
<7796.1.0> erts_code_purger '-' 479850 285160 0 erts_code_purger:wai
<7796.2.0> erts_literal_area_co '-' 337591 2688 0 erts_literal_area_co
<7796.3.0> erts_dirty_process_s '-' 37924 2688 0 erts_dirty_process_s
一般消息堆积都会伴随着 memory 增长, 不管是 sort by msg_q 或 memory, 都很容易发现问题.
如果没有 etop
iex(xxxx@xxxx.)8> Enum.map(:erlang.processes(), fn proc -> {:erlang.process_info(proc, :message_queue_len), proc} end) |> Enum.sort(fn({{_, a}, _}, {{_, b}, _}) -> a > b end) |> List.first
{{:message_queue_len, 0}, #PID<0.32638.0>}
ets 表泄漏
找出占用最多内存的 ets 表
iex(7)> :ets.all() |> Enum.map(fn ets_name -> {:ets.info(ets_name, :memory), ets_name} end) |> Enum.sort(fn a, b -> a > b end)
[{18002942, :test},
{41940, #Reference<0.3983585142.1897791489.87703>},
...
]
整体内存分析
:erlang.memory 可以一眼看出是否 ets 表存在泄漏
值得注意的是, 大于 64bit 的 binary, 会在:erlang.memory 的 binary 项体现. 不会计入 ets 项中.
65bit
iex(1)> :ets.new(:test, [:public, :named_table])
:test
iex(2)> :erlang.memory
[
total: 23688632,
processes: 4940400,
processes_used: 4939456,
system: 18748232,
atom: 463465,
atom_used: 442288,
binary: 27872,
code: 8462310,
ets: 589664
]
iex(3)> for num <- 1..1000000 do
...(3)> :ets.insert(:test, {num, :crypto.strong_rand_bytes(65)})
...(3)> end
[true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, ...]
iex(4)> :erlang.memory
[
total: 284511736,
processes: 33626760,
processes_used: 33625816,
system: 250884976,
atom: 463465,
atom_used: 446381,
binary: 112090520,
code: 8553627,
ets: 120619384
]
64bit
iex(1)> :ets.new(:test, [:public, :named_table])
:test
iex(2)> :erlang.memory
[
total: 23569856,
processes: 4778728,
processes_used: 4777784,
system: 18791128,
atom: 463465,
atom_used: 442288,
binary: 70736,
code: 8462310,
ets: 589680
]
iex(3)> for num <- 1..1000000 do
...(3)> :ets.insert(:test, {num, :crypto.strong_rand_bytes(64)})
...(3)> end
[true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, ...]
iex(4)> :erlang.memory
[
total: 204325192,
processes: 33373520,
processes_used: 33372576,
system: 170951672,
atom: 463465,
atom_used: 447944,
binary: 39168,
code: 8653586,
ets: 152623976
]
数据过大
首先, 应该能估算出业务大致的内存占用. 可以通过 process_info, 找出可疑的进程.
iex(11)> :erlang.process_info(:ets.info(:test, :owner), :memory)
{:memory, 28693220}
通过:sys.get_state 可以发现一些逻辑错误造成的, list/map 无限增长的 bug.
iex(xxxxx@xxxxx.)13> :sys.get_state(:erlang.list_to_pid('<0.2362.0>'))
{:state, {:local, :prometheus_sup}, :one_for_one, {[], %{}}, :undefined, 5, 1,
[], 0, :prometheus_sup, []}
内存估算
https://github.com/okeuday/erlang_term
http://erlang.org/doc/efficiency_guide/advanced.html#id68923