如何处理Ruby 2.1.2内存泄漏?

我有一个工作进程,它产生多达50个线程并执行一些异步操作(大多数是http调用)。 当我启动该过程时,它从大约35MB的已用内存开始,并迅速增长到250MB。 从那时起它进一步增长,问题是内存永远不会停止增长(即使增长阶段随着时间的推移而减少)。 几天后,进程只会超出可用内存并崩溃。

我做了很多分析和分析,似乎无法找到问题所在。 即使堆大小非常不变,进程内存也在不断增长。 我已将GC.stat输出收集到您可以在此处访问的电子表格中:

https://docs.google.com/spreadsheets/d/17TohDNXQ_MXM31CeAmR2ptHFYfvOeF3dB6WCBkBS_Bc/edit?usp=sharing

即使过程内存最终稳定在415MB,但它将在未来几天继续增长,直到达到512MB的限制并崩溃。

我也尝试使用对象空间跟踪对象,但跟踪对象的内存总和从不超过70-80MB,这与GC报告完全一致。 剩下的300MB +(并且还在增长)花在哪里……我不知道。

如何处理这些问题? 是否有任何工具可以让我更清楚地了解内存的使用方式?

更新:gem和操作系统

我正在使用以下gem:

 gem "require_all", "~> 1.3" gem "thread", "~> 0.1" gem "equalizer", "~> 0.0.9" gem "digest-murmurhash", "~> 0.3", require: "digest/murmurhash" gem "google-api-client", "~> 0.7", require: "google/api_client" gem "aws-sdk", "~> 1.44" 

该应用程序部署在heroku上,但在Mac OS X 10.9.4上本地运行时,内存泄漏是显而易见的。

更新:泄漏

我已经升级了stringbuffer并分析了像@mtm建议的所有内容,现在没有leak工具识别的内存泄漏,随着时间的推移没有增加ruby堆大小,然而,进程内存仍在增长。 最初我以为它在某些时候停止了增长,但几个小时后它超过了极限并且过程崩溃了。

从GC日志看,问题不是ruby对象引用泄漏,因为heap_live_slot值没有显着增加。 这表明问题是:

  1. 数据存储在堆外(字符串,数组等)
  2. 使用本机代码的gem中的泄漏
  3. Ruby解释器本身泄漏(最不可能)

值得注意的是,问题出现在OSX和Heroku(Ubuntu Linux)上。

对象数据和“堆”

Ruby 2.1垃圾收集仅对包含少量数据的对象使用报告的“堆”。 当Object中包含的数据超过某个限制时,数据将被移动并分配给堆外部的区域。 您可以使用ObjectSpace获取每种数据类型的总体大小:

 require 'objspace' ObjectSpace.count_objects_size({}) 

将此与GC统计信息一起收集可能会指示在堆外部分配内存的位置。 如果您找到特定类型,请说:T_ARRAY比其他类型增加更多,您可能需要寻找一个您永远追加的数组。

您可以使用pry-byebug进入控制台以绕过特定对象,甚至可以查看根目录中的所有对象:

 ObjectSpace.memsize_of(some_object) ObjectSpace.reachable_objects_from_root 

关于ruby开发者博客之一以及SO答案中的更多细节。 我喜欢他们的JRuby / VisualVM分析想法。

测试本机gem

使用bundle将gem安装到本地路径:

 bundle install --path=.gems/ 

然后你可以找到包含本机代码的那些:

 find .gems/ -name "*.c" 

这给了你:(按我的怀疑顺序)

  • 摘要-的StringBuffer-0.0.2
  • 摘要-murmur哈希-0.3.0
  • 引入nokogiri-1.6.3.1
  • JSON-1.8.1

OSX有一个有用的开发工具叫做leaks ,可以告诉你它是否在正在运行的进程中找到未引用的内存。 对于识别内存来自Ruby的位置并不是很有用,但有助于识别它何时发生。

首先要测试的是digest-stringbuffer 。 从自述文件中获取示例并使用gc_tracer添加一些GC日志记录

 require "digest/stringbuffer" require "gc_tracer" GC::Tracer.start_logging "gclog.txt" module Digest class Prime31 < StringBuffer def initialize @prime = 31 end def finish result = 0 buffer.unpack("C*").each do |c| result += (c * @prime) end [result & 0xffffffff].pack("N") end end end 

让它运行很多

 while true do a=[] 500.times do |i| a.push Digest::Prime31.hexdigest( "abc" * (1000 + i) ) end sleep 1 end 

运行示例:

 bundle exec ruby ./stringbuffertest.rb & pid=$! 

监视ruby进程的驻留和虚拟内存大小,以及发现的leaks计数:

 while true; do ps=$(ps -o rss,vsz -p $pid | tail +2) leaks=$(leaks $pid | grep -c Leak) echo "$(date) m[$ps] l[$leaks]" sleep 15 done 

看起来我们已经找到了一些东西:

 Tue 26 Aug 2014 18:22:36 BST m[104776 2538288] l[8229] Tue 26 Aug 2014 18:22:51 BST m[110524 2547504] l[13657] Tue 26 Aug 2014 18:23:07 BST m[113716 2547504] l[19656] Tue 26 Aug 2014 18:23:22 BST m[113924 2547504] l[25454] Tue 26 Aug 2014 18:23:38 BST m[113988 2547504] l[30722] 

驻留内存正在增加,泄漏工具正在发现越来越多的未引用内存。 确认GC堆大小,对象计数仍然稳定

 tail -f gclog.txt | awk '{ print $1, $3, $4, $7, $13 } 1581853040832 468 183 39171 3247996 1581859846164 468 183 33190 3247996 1584677954974 469 183 39088 3254580 1584678531598 469 183 39088 3254580 1584687986226 469 183 33824 3254580 1587512759786 470 183 39643 3261058 1587513449256 470 183 39643 3261058 1587521726010 470 183 34470 3261058 

然后报告问题 。

在我未经训练的C眼中看来,它们既分配指针又分配缓冲区,但只清理缓冲区 。

看看digest-murmurhash ,它似乎只提供依赖于StringBuffer的函数,所以一旦stringbuffer被修复,泄漏可能会很好。

当他们修补它,再次测试并移动到下一个gem。 对于每个gem测试而不是通用示例,最好使用实现中的代码片段。

测试MRI

第一步是在相同的MRI下在多台机器上certificate问题,以排除您已经完成的任何本地操作。

然后在不同的操作系统上尝试相同的Ruby版本,您也已经完成了。

如果可能的话,尝试使用JRuby或Rubinius上的代码。 是否会出现同样的问题?

如果可能,请尝试在2.0或1.9上使用相同的代码,看看是否存在相同的问题。

尝试从github的头部开发版本,看看是否有任何区别。

如果没有什么变得明显,请向 Ruby 提交一个错误 ,详细说明问题以及您已消除的所有内容。 等待开发人员帮忙并提供他们需要的任何东西。 如果您能够获得问题设置的最简洁/最简单的示例,他们很可能想要重现该问题。 这样做通常可以帮助您确定问题所在。

我修复内存泄漏并释放digest / stringbuffer v0.0.3。

https://rubygems.org/gems/digest-stringbuffer

您可以通过v0.0.3再试一次。

我是digest / murmurhash和digest / stringbuffer gems的作者。

实际上,似乎在digest / stringbuffer中有泄漏。

我稍后会解决它。

你能解释一下代码吗?

我建议使用像这样的单例方法。

 Digest::MurmurHash1.hexdigest(some_data) 

也许,因为单例方法没有使用digest / stringbuffer,所以不会泄漏。