问题已经解决,找来找去原来是一个用了很多年的工具类中有内存泄漏的问题。
这个类是一个单例,其中有一个很大的成员变量,如果使用完以后不及时清空,就会占用很多内存。
因为之前跑了很多年,所以一直没有去怀疑这个公共类,这一次数据量暴增才暴露了问题。
严格来说也不能算内存泄漏,只是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 为何疯狂的吃我的内存。 或者给出一些解决该问题的建议。
万分感激~~
可能性判断,可能是一次性读入内存的文件太大,在处理的过程中还对部分数据创建了副本方便操作,能否分步处理,先想办法把大文件拆分成若干小文件,然后在依次处理小文件,以达到每次 input 的数据不至于太大呢?
至于内存释放回收至操作系统,可能最简单直接的方式就是结束并退出当前进程,所以能否把工作拆封城若干个进程 one by one 这样去执行呢?这样的话下上一个进程结束后,下一个进程启动前,操作系统会确保回收进程所占用的物理内存。
最大可能是在 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 参数设置的一个上限。
#8 楼 @quakewang 多谢回答,说的很有道理。我再仔细检查一下其他的地方是否有内存泄漏。 顺便问一句,那个所谓的上限有办法查看或者设置吗。似乎是随着对内存的需求的增加而增加的。 找到一片文章如下,意思是随着需求增加,Ruby 会不断索取内存,但是具体细节并没有深入讨论。 http://www.theirishpenguin.com/2009/10/29/understanding-how-ruby-stores-objects-in-memory-the-ruby-heap.html
既然文件很大,能不能流式处理,类似于 sed
awk
或者 Ruby 里面有类似的机制,即使没有,也可以调用这些外部工具完成文件操作。
另外 Process#setrlimit
可以用来限制进程的空间大小
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)。
#24 楼 @xixiwelcome 能否具体说说"是在一处数据库操作中,有一个变量使用后没有设置为 nil”代码长什么样哈?我们最近也遇到类似的问题,找不到原因但肉眼看代码感觉都 ok...或者介绍一下你如何找到有问题的代码的?
#26 楼 @jayliud 这里是之前看到的一个文章,或许能够有些帮助~~ http://blog.linjunhalida.com/blog/ruby-memory-leak-debug/
#27 楼 @xixiwelcome 但我们一般写代码的时候都不会手动去清除变量吧?难道只有我一个人这样么。。如果要手动释放 一般是哪些变量(或哪些情况下)需要呢?你这里说的是占用内存很大的变量
理论上只有依然被引用的变量不会被 GC 啊。如果只是临时变量且上下文没有对其作任何引用,这种情况应该不需要将变量设置为 nil 吧?还有 ruby 中 GC.start 会 stw 吗?ruby 新人一枚