2017-04-25 62 views
12

我有一个elixir/OTP应用程序,由于内存不足问题而在生产中崩溃。导致崩溃的功能每6小时在一个专门的过程中调用。它需要几分钟的时间(〜30)运行,看起来像这样:解决大型二进制泄漏

def entry_point do 
    get_jobs_to_scrape() 
    |> Task.async_stream(&scrape/1) 
    |> Stream.map(&persist/1) 
    |> Stream.run() 
end 

在我的本地机器,我看到大量的二进制内存消耗不断增长,当函数运行:

observer memory usage shows constant memory growth of large binaries

请注意,当我在运行该函数的进程上手动触发垃圾回收时,内存消耗显着下降,因此,几个不能处理GC的进程肯定不会出现问题,但只有一个进程无法正常运行GC。另外,很重要的一点是,每隔几分钟的过程管理GC,但有时这是不够的。生产服务器只有1GB内存,并且在GC启动之前崩溃。

试图解决我遇到的问题Erlang in Anger(请参阅第66-67页)。一个建议是将所有大型的二进制操作放入一次性进程中。 scrape函数的返回值是一个包含大型二进制文件的映射。因此,他们在Task.async_stream“工作人员”和运行该功能的进程之间共享。因此,理论上,我可以将persistscrape一起放入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 

而导致内存负载:

observer memory usage after GC hack

我不太明白书中的其他建议如何适应的问题。

在这种情况下,你会推荐什么?保持hacky解决方案或有更好的选择。

+0

你考虑保持二进制文件ETS?如果你能够可靠地释放它们,那么实际上你就可以开始进行手动分配,并避免BEAM GC中的任何混乱。 OTOH,如果这是一个合适的雪花,也许手动呼叫-GC解决方案足够好? – cdegroot

+0

有趣的想法!让我们看看我是否理解:使用scraper函数将数据放入ETS中,然后在表中的数据上映射“persist”函数是否正确?不过,'Task.async_stream'工作者将会引用大的二进制文件,并且主进程将具有相同的引用,用于从'persist'函数内部的ETS中获取,并且问题将会保留。 – Nagasaki45

+0

嗯,根据Task.async_stream文档,“每个枚举项都作为参数传递给函数并由其自己的任务处理”,因此如果每个scrape调用只是构建二进制文件,则将其存储在ETS中,并返回一个参考,那么你可能会在业务中。 – cdegroot

回答

6

erlang虚拟机具有垃圾收集机制,默认情况下,该机制针对短期数据进行了优化。一个短命的进程可能不会被垃圾收集,直到它死亡,并且大多数垃圾收集只运行检查新添加的项目。在完成扫描之前,不会再次检查GC运行中存活的项目。

我建议您尝试调整fullsweep_after标志。它可以通过:erlang.system_flag(:fullsweep_after, value)在全球范围内设置,也可以使用:erlang.spawn_opt/4为您的特定过程设置。

从文档:

的Erlang运行时系统使用代垃圾收集机制,采用“老堆”对于已经经历至少一个垃圾收集数据。当旧堆没有更多空间时,完成全部扫描垃圾收集。

选项fullsweep_after使得可以在强制全扫描之前指定最大世代集合数,即使旧堆空间不足。将数字设置为零将禁用常规采集算法,即所有实时数据都将在每个垃圾收集处复制。

少数情况下,当它可以改变fullsweep_after有用:

  • 如果不再使用的二进制文件要尽快扔掉。 (将数字设置为零)
  • 一个主要拥有短期数据的进程很少或永远不会完整保留,也就是说,旧堆中大部分都包含垃圾。要确保偶尔会发生全面扫描,请将Number设置为合适的值,例如10或20.
  • 在RAM数量有限且无虚拟内存的嵌入式系统中,可能需要将Number设置为零来保留内存。 (该值可以在全局设置,见二郎:SYSTEM_FLAG/2)

的默认值是65535(除非你已经改变了它通过环境变量ERL_FULLSWEEP_AFTER),因此任何小,会使垃圾收集更具侵略性。

这是关于这个主题很好看的:https://www.erlang-solutions.com/blog/erlang-19-0-garbage-collector.html