Ruby 如何阻止 Ruby 吃掉所有内存?(已解决)

xixiwelcome · March 05, 2015 · Last by marksgui replied at September 16, 2021 · 6856 hits

问题已经解决,找来找去原来是一个用了很多年的工具类中有内存泄漏的问题。 这个类是一个单例,其中有一个很大的成员变量,如果使用完以后不及时清空,就会占用很多内存。 因为之前跑了很多年,所以一直没有去怀疑这个公共类,这一次数据量暴增才暴露了问题。 严格来说也不能算内存泄漏,只是GC.start是清理不掉依然被引用的内存的。

同时,使用产生巨大无比的变量本身就存在问题,但谁也没有去改动他的想法。 上级给出的指示是,改动已经跑了很多年的文件(人工 + 风险),还不如等到出了问题花点钱买点内存了。 也许这就是许多公司的诟病吧,自己人轻言微,而且也确实不知道怎么解决这类问题(也许头儿是对的)。

所以,目前只是将那个大大的变量使用过以后清空了,其他的就什么都没有做了。

总结起来两点: 1. 不能过分相信已经跑了很久的东西,因为当年的工程师是否严谨你是不知道的 2. 公司还是以利益至上


原问题如下: 近在做一个项目,内容很简单,就是读取数据文件,处理后存入数据库。环境:CentOS6.4,Ruby2.1 伪代码如下:

arr_data = []
file.each { |line|
    arr_data << deal_with(line)   #deal_with处理后返回一个字符串, 存入数组
    if arr_data.size > 1000
      write_file(arr_data)             #每1000行存一次文件,然后清空数组
      arr_data.clear
      #GC.start
    end
}
if arr_data.size > 0
  write_file(arr_data)       
  arr_data.clear
  #GC.start
end
file.close
GC.start
#`hostname`

是的,就是这样一个简单的任务(文件很大,有上亿行), 逐渐把服务器 32G 内存全都吃光了。 但是,是可以顺利执行完毕的。

问题出在,我在他执行完毕之后,需要发邮件报告结果。 发邮件的函数中 用到了 hostname 这一系统命令。

程序在此处 ERROR, Cannot allocate memory - hostname。 也就是说,上面的那个循环已经全部执行完毕,但内存还是没有释放。

尝试了许多做法,包括使用GC.start,使用WeakRef等。

但是这些做法,均只是将内存回收给 Ruby 程序,并没有释放到操作系统。 所以最终还是会把系统内存吃光,导致最后无法执行hostname

借此机会也把 Ruby 的内存管理,垃圾回收机制学习了一下,大致了解了其原理。 但是并没有见哪里提到 强制归还内存给操作系统

希望大神给点提示,解释一下 Ruby 为何疯狂的吃我的内存。 或者给出一些解决该问题的建议。

万分感激~~

在它吃掉所有内存前,自己先吃

#1 楼 @teemo 严肃!严肃!

可能性判断,可能是一次性读入内存的文件太大,在处理的过程中还对部分数据创建了副本方便操作,能否分步处理,先想办法把大文件拆分成若干小文件,然后在依次处理小文件,以达到每次 input 的数据不至于太大呢?

至于内存释放回收至操作系统,可能最简单直接的方式就是结束并退出当前进程,所以能否把工作拆封城若干个进程 one by one 这样去执行呢?这样的话下上一个进程结束后,下一个进程启动前,操作系统会确保回收进程所占用的物理内存。

@xixiwelcome 试试最新的 ruby 版本

file.close 然后 GC 应该就好了

#3 楼 @lgn21st
也曾尝试过把 arr_data 设为全局变量 $arr_data 结果是一样的。 每一次只处理 1000 行,占用内存不多,而且每次用完之后都会进行clearGC.start 内存是逐步被 Ruby 占掉的。Ruby 似乎拿到了内存就不还给操作系统了,留给自己用。

最大可能是在 deal_with 或者 write_file 这 2 个方法里面有内存泄漏,如果没有内存泄漏,只是做 array 操作,是不应该出现这种情况的。你可以测试一下:

def report_rss
  puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`.strip.split.map(&:to_i)[1].to_s + 'KB'
end

arr = []
report_rss
10.times do
  1000000.times do |i|
    arr << i
  end
  report_rss
  arr.clear
  GC.start
end

rss 内存会稳定在 GC 参数设置的一个上限。

#7 楼 @xixiwelcome 我记得 @luikore 大神有一次特别解释过,Ruby VM 就是这尿性,总是很不情愿把内存还给操作系统。

#6 楼 @luikore 不好意思,忘了写,关闭了文件,而且也做了 GC

#9 楼 @lgn21st 乱占内存的情况应该是操作系统需要的时候会还的... 感觉问题可能是 file 这个对象还有引用,就算你使劲 GC 也不会回收它呀。

#10 楼 @xixiwelcome 那,file = nil 然后 GC 哩?

#8 楼 @quakewang 多谢回答,说的很有道理。我再仔细检查一下其他的地方是否有内存泄漏。 顺便问一句,那个所谓的上限有办法查看或者设置吗。似乎是随着对内存的需求的增加而增加的。 找到一片文章如下,意思是随着需求增加,Ruby 会不断索取内存,但是具体细节并没有深入讨论。 http://www.theirishpenguin.com/2009/10/29/understanding-how-ruby-stores-objects-in-memory-the-ruby-heap.html

#9 楼 @lgn21st 是吧,以前没怎么考虑过,一看 32G 内存,随便跑吧。 正好借此机会深入研究一下

#12 楼 @luikore 这么机智~ 我来试试哈

#12 楼 @luikore 之前一直盯着 array 了 没考虑 file~~~

#12 楼 @luikore 不好意思啊,刚才测试了一下,不管多大的文件,file 本身并没有占多少内存~~~~ 这只是一个引用罢了~~~

既然文件很大,能不能流式处理,类似于 sed awk 或者 Ruby 里面有类似的机制,即使没有,也可以调用这些外部工具完成文件操作。

另外 Process#setrlimit 可以用来限制进程的空间大小

改成不是全部加载文件而是每次只读取部分文件来处理的话能有提高么?

什么应用啊,32G 内存吃光,也是醉了

#19 楼 @ywjno 楼主估计把数据全部读入内存了

static VALUE
rb_io_each_line(int argc, VALUE *argv, VALUE io)
{
    VALUE str, rs;
    long limit;

    RETURN_ENUMERATOR(io, argc, argv);
    prepare_getline_args(argc, argv, &rs, &limit, io);
    if (limit == 0)
        rb_raise(rb_eArgError, "invalid limit: 0 for each_line");
    while (!NIL_P(str = rb_io_getline_1(rs, limit, io))) {
        rb_yield(str);
    }
    return io;
}

帮忙搬来 2.1 IO each 的源码,熟悉 C 的看看这里会不会导致不 GC

文件很大不适合用 ruby 处理,C 语言等“低级”语言让人诟病的不支持 unicode 字符串的缺点在大文件面前反而成了优点。

建议用 C 加上 mmap 重写这段逻辑,文件直接映射到内存以后再处理。一定要用 ruby 的话就手工 file.read(4096)。

#8 楼 @quakewang 果然如你所说,是在一处数据库操作中,有一个变量使用后没有设置为 nil 导致 GC 不能将其释放。 多谢了!

25 Floor has deleted

#24 楼 @xixiwelcome 能否具体说说"是在一处数据库操作中,有一个变量使用后没有设置为 nil”代码长什么样哈?我们最近也遇到类似的问题,找不到原因但肉眼看代码感觉都 ok...或者介绍一下你如何找到有问题的代码的?

#26 楼 @jayliud 就是一个 SQL 语句的执行结果很大,使用了之后没有设置成 nil。这样后来的GC.start就没法回收这块内存。根本的解决办法还是 不要一次性产生这么大的变量。最好用limit去限制每次 SQL 的结果数量,用过的变量最好都清空,不让他继续占地儿。我也是用眼睛瞪出来的,哈哈,抱歉没能帮到你。貌似 Ruby 也是有调试工具的,不过没有用过~~

#26 楼 @jayliud 这里是之前看到的一个文章,或许能够有些帮助~~ http://blog.linjunhalida.com/blog/ruby-memory-leak-debug/

#27 楼 @xixiwelcome 但我们一般写代码的时候都不会手动去清除变量吧?难道只有我一个人这样么。。如果要手动释放 一般是哪些变量(或哪些情况下)需要呢?你这里说的是占用内存很大的变量

#29 楼 @jayliud 嗯,我也是菜鸟,不过那些以后用不到的,而且很大的变量 最好就清一下吧。因为如果他被引用这,ruby 是不会回收他的。

理论上只有依然被引用的变量不会被 GC 啊。如果只是临时变量且上下文没有对其作任何引用,这种情况应该不需要将变量设置为 nil 吧?还有 ruby 中 GC.start 会 stw 吗?ruby 新人一枚

You need to Sign in before reply, if you don't have an account, please Sign up first.