如何处理Ruby 2.1.2内存泄漏?

时间:2014-08-21 14:29:54

标签: ruby memory memory-management memory-leaks

我有一个工作进程,最多可生成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 "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堆大小没有增加,但是,进程内存仍在增长。最初我以为它在某些时候停止了增长,但几个小时后它超过了极限并且过程崩溃了。

3 个答案:

答案 0 :(得分:18)

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

  1. 数据存储在堆外(字符串,数组等)
  2. 使用本机代码的gem中的泄漏
  3. Ruby解释器本身的漏洞(最不可能)
  4. 值得注意的是,问题出现在OSX和Heroku(Ubuntu Linux)上。

    对象数据和"堆"

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

    require 'objspace'
    ObjectSpace.count_objects_size({})
    

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

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

    ObjectSpace.memsize_of(some_object)
    ObjectSpace.reachable_objects_from_root
    

    其中一个ruby developers blog以及this SO answer中有更详细的信息。我喜欢他们的JRuby / VisualVM概况分析。

    测试原生宝石

    使用bundle将宝石安装到本地路径中:

    bundle install --path=.gems/
    

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

    find .gems/ -name "*.c"
    

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

    • 消化-的StringBuffer-0.0.2
    • 消化-murmur哈希-0.3.0
    • 引入nokogiri-1.6.3.1
    • JS​​ON-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
    

    然后报告issue

    我非常未经训练的C眼看来,他们分配pointerbuffer,但只清理buffer

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

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

    测试MRI

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

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

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

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

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

    如果没有什么显而易见的话,submit a bug向Ruby详细说明问题和你已经消除的所有事情。等待开发人员帮忙并提供他们需要的任何东西。如果您能够获得问题设置的最简洁/最简单的示例,他们很可能想要重现该问题。这样做通常可以帮助您确定问题所在。

答案 1 :(得分:1)

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

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

您可以按v0.0.3再试一次。

答案 2 :(得分:0)

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

确实,它似乎在digest / stringbuffer中有泄漏。

我稍后会解决。

你能解释更多代码吗?

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

Digest::MurmurHash1.hexdigest(some_data)

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