我有一个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的进程肯定不会出现问题,但只有一个进程无法正常运行GC。另外,很重要的一点是,每隔几分钟的过程管理GC,但有时这是不够的。生产服务器只有1GB内存,并且在GC启动之前崩溃。
试图解决我遇到的问题Erlang in Anger(请参阅第66-67页)。一个建议是将所有大型的二进制操作放入一次性进程中。 scrape
函数的返回值是一个包含大型二进制文件的映射。因此,他们在Task.async_stream
“工作人员”和运行该功能的进程之间共享。因此,理论上,我可以将persist
与scrape
一起放入Task.async_stream
之内。我宁愿不这样做,并保持persist
的呼叫在整个过程中保持同步。
另一个建议是定期拨打:erlang.garbage_collect
。它看起来像解决了这个问题,但感觉太冒险了。作者也不建议这样做。这是我目前的解决方案:
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解决方案或有更好的选择。
你考虑保持二进制文件ETS?如果你能够可靠地释放它们,那么实际上你就可以开始进行手动分配,并避免BEAM GC中的任何混乱。 OTOH,如果这是一个合适的雪花,也许手动呼叫-GC解决方案足够好? – cdegroot
有趣的想法!让我们看看我是否理解:使用scraper函数将数据放入ETS中,然后在表中的数据上映射“persist”函数是否正确?不过,'Task.async_stream'工作者将会引用大的二进制文件,并且主进程将具有相同的引用,用于从'persist'函数内部的ETS中获取,并且问题将会保留。 – Nagasaki45
嗯,根据Task.async_stream文档,“每个枚举项都作为参数传递给函数并由其自己的任务处理”,因此如果每个scrape调用只是构建二进制文件,则将其存储在ETS中,并返回一个参考,那么你可能会在业务中。 – cdegroot