我有一个elixir / OTP应用程序,由于内存不足问题导致生产崩溃。导致崩溃的功能在专用进程中每6小时调用一次。运行需要几分钟(~30),看起来像这样:
def entry_point do
get_jobs_to_scrape()
|> Task.async_stream(&scrape/1)
|> Stream.map(&persist/1)
|> Stream.run()
end
在我的本地计算机上,当函数运行时,我看到大型二进制文件内存消耗不断增长:
请注意,当我在运行该函数的进程上手动触发垃圾收集时,内存消耗会显着下降,因此对于无法使用GC的几个不同进程来说,它绝对不是问题,但只有一个不会出现问题。 t GC正确。此外,重要的是要说过程 每隔几分钟才能管理GC,但有时候这还不够。生产服务器只有1GB内存,并且在GC启动之前就崩溃了。
尝试解决我遇到的问题Erlang in Anger(参见第66-67页)。一个建议是将所有大型二进制文件操作放在一次性过程中。 scrape
函数的返回值是包含大二进制文件的映射。因此,它们在Task.async_stream
"工人"之间共享。以及运行该功能的过程。因此,理论上,我可以将persist
与scrape
放在Task.async_stream
内。我不想这样做,并通过这个过程保持对persist
的调用同步。
另一个建议是定期致电:erlang.garbage_collect
。看起来它解决了这个问题,但感觉太过于hacky。作者也不建议这样做。这是我目前的解决方案:
def entry_point do
my_pid = self()
Task.async(fn -> periodically_gc(my_pid) end)
# The rest of the function as before...
end
defp periodically_gc(pid) do
Process.sleep(30_000)
if Process.alive?(pid) do
:erlang.garbage_collect(pid)
periodically_gc(pid)
end
end
结果内存负载:
我不太明白书中的其他建议是如何解决这个问题的。
在这种情况下你会推荐什么?保持hacky解决方案或有更好的选择。
答案 0 :(得分:6)
erlang虚拟机具有垃圾收集机制,默认情况下,该机制针对短期数据进行了优化。一个短暂的进程可能根本不会被垃圾收集,直到它死亡,并且大多数垃圾收集运行只检查新添加的项目。在完全扫描完成之前,不会再次检查在GC运行中幸存的项目。
我建议你尝试调整 fullsweep_after 标志。它可以通过:erlang.system_flag(:fullsweep_after, value)
全局设置,也可以使用:erlang.spawn_opt/4
为您的特定流程设置。
来自文档:
Erlang运行时系统使用分代垃圾收集方案,使用"旧堆"对于至少存在一个垃圾收集的数据。当旧堆上没有空间时,就会完成一次完整的垃圾收集。
选项fullsweep_after可以在强制完全扫描之前指定世代集合的最大数量,即使旧堆上有空间也是如此。将数字设置为零会禁用常规收集算法,即在每个垃圾收集中复制所有实时数据。
更改fullsweep_after可能有用的几种情况:
- 如果不再使用的二进制文件将尽快丢弃。 (将Number设置为零。)
- 一个主要拥有短期数据的进程很少或永远不会被填满,也就是说,旧堆主要包含垃圾。要确保偶尔进行完全扫描,请将Number设置为合适的值,例如10或20。
- 在RAM数量有限且没有虚拟内存的嵌入式系统中,您可能希望通过将Number设置为零来保留内存。 (可以全局设置该值,请参阅erlang:system_flag / 2.)
默认值为65535(除非您已通过环境变量ERL_FULLSWEEP_AFTER
更改了它),因此任何较低的值都会使垃圾收集更具侵略性。
这是关于这个主题的好读物:https://www.erlang-solutions.com/blog/erlang-19-0-garbage-collector.html