Elixir进程内存占用异常求助:ETS缓存少量邮编仍占高内存
你的问题核心其实和ETS本身无关,而是处理大文件时的内存管理方式导致的——不管你插入多少数据到ETS,进程内存都被前期加载和处理全量CSV数据的临时变量占满了,具体原因和优化方案如下:
为什么进程内存占用居高不下?
一次性加载整个大文件
File.read!()会把95MB的CSV文件完整读进当前进程的内存里,生成一个巨大的字符串。哪怕你后续只过滤出20万条记录,这个原始的大字符串和解析后生成的180万条全量记录列表,都会暂时驻留在进程堆内存中(尤其是在iex环境中,函数返回的大列表会被自动绑定到_变量,不会立即被GC回收)。ETS的内存不归属进程
ETS是Erlang/OTP提供的系统级存储,它的内存占用是独立于创建它的进程的——你插多少数据到ETS,都不会体现在PostcodeCache进程的内存统计里,进程内存只统计自身栈堆上的临时数据。Enum.map的额外内存开销
你用Enum.map(&:ets.insert_new(:cache, &1))会生成一个包含180万个布尔值的列表(每个insert_new的返回结果),这个列表本身也会占用不少内存,进一步加剧进程内存压力。
优化方案
1. 改用流式处理,避免一次性加载全文件
把File.read!()换成File.stream!(),逐行读取和解析CSV,这样每次只在内存中保留一行数据,处理完就丢弃,不会积累全量数据:
defmodule PostcodeCache do use GenServer def cache_postcodes do "path_to_postcode.csv" # 流式读取文件,默认按行分割 |> File.stream!() # 逐行解析(需要把原来的function_to_parse改成处理单一行的逻辑) |> Stream.map(&parse_single_line/1) |> Stream.filter(&function_to_filter/1) |> Stream.map(&function_to_format/1) # 用Enum.each代替Enum.map,只执行插入操作,不保留结果列表 |> Enum.each(&:ets.insert_new(:cache, &1)) end # 示例:单一行解析逻辑(根据你的CSV格式调整) defp parse_single_line(line) do line |> String.trim() |> String.split(",") # 转换成你需要的结构,比如{postcode, location} |> List.to_tuple() end end
2. 手动触发GC(仅用于调试验证)
如果在iex环境中运行,函数执行完后,返回的大列表可能还被_变量持有,可以手动触发垃圾回收来释放内存:
# 触发当前进程的GC :erlang.garbage_collect(self())
3. 验证ETS的实际内存占用
你可以用以下命令查看ETS表的真实内存消耗(单位是Erlang word,乘以系统的word大小(通常是8字节)就是实际字节数):
:ets.info(:cache, :memory)
总结
你的进程内存占用高是因为一次性加载和处理全量CSV数据导致的临时内存开销,和ETS的存储无关。改用流式处理后,进程内存应该会降到合理水平,同时ETS会正常缓存你需要的20万条记录。
内容的提问来源于stack exchange,提问作者RobStallion

