对Ruby VM的GC的思考
程序员文章站
2022-03-08 20:17:04
...
Ruby虽然是动态脚本语言,但是和Java一样,带有VM,有自己的内存堆,创建对象的时候在堆里面分配内存,对象使用完毕由GC进行回收。但是通过我们运营Rails网站两年多的实践来看,Ruby VM的GC还是存在很大的问题。简单的来说,就是GC之后,尽管对象已经完全回收,但是物理内存释放不够充分,有泄漏的现象。通过pmap来dump ruby进程物理内存地址映射表进行分析,观察到ruby的内存堆总是在不停的扩张,GC之后回收不干净。而我对比观察Java VM,其Full GC之后,物理内存释放的非常干净。所以用Ruby做服务器长期的跑,就会发现Ruby进程没有理由的缓慢泄漏内存,内存堆缓慢增长,貌似没有上限。
由于pmap命令可以dump进程的内存映射表,因此我们可以对比RubyVM和JVM在GC前后的内存映射情况。比方说Ruby的内存状况大概是这样的:
下图是一个Ruby进程的物理内存映射表,堆内存占据的空间是我抽取出来的三行:
可以看出来Ruby的堆内存分配比较连续,分段不多。而JVM的堆内存分配段就很多了,由于堆地址段太多,我就不贴出来了,大家可以自己观察。
由于Ruby的内存分配算法和回收算法还是比较原始的,因此在进行多次回收之后,内存堆很容易出现大量的内存碎片,很多内存碎片并不能够被有效的利用,并且ruby没有好的碎片归并压缩算法,因此碎片造成的内存堆地址空间浪费就会越来越大。其结果就是Ruby进程在长期高负载运行之下,表现出缓慢的内存泄漏现象!
对比JVM的堆分配,他分配了很多的段,每个段的内存存活期不一样,根据分代算法,可以把不同存活期的对象在堆之间移动,堆内部则进行碎片归并。比方说我对一个Tomcat应用服务器的实际应用先pmap记录内存映射,然后GC,再pmap记录内存映射,两者diff一下,就可以发现某些堆在收缩,但是某些堆甚至在GC后扩张了,这便是对象在堆之间进行移动的现象。因此我不得不赞赏一下JVM的内存分配。
话说回来,由于Ruby的VM内存分配的碎片问题,导致Ruby进程几乎无可避免的内存泄漏。其结果就是你必须实时监控Ruby进程的运行情况,一旦发现内存使用超过限额,则必须果断的杀掉进程重起。比方说现在很多流行的Rails网站都是用monit去监控mongrel实例,一旦发现内存使用超过限额就杀掉重起。这种监控方式虽然可以有效的解决ruby的内存泄漏问题,但是太过简单粗暴,如果杀掉进程重起的时候,Ruby进程正好在处理请求,那么该请求是肯定会失败掉的,对于一些极端的情况,似乎很难令人接受这种现象的存在。我现在没有用monit,而是自己写shell脚本来监控(写几行shell就可以搞定的事情没必要那么麻烦搞什么monit),每天大概能出现两三次这种需要杀掉重起的情况,对比每天要处理将近100 万动态请求来说,可靠性还是达到了99.999%,还算可以。
那么将于今年年底发布的ruby 1.9.1能够解决这个问题吗? 答案是悲观的,1.9的GC并没有本质的提高,可以预见还是会出现无可避免的内存泄漏问题。但是1.9的内存泄漏会比现在的1.8要轻微一些,原因是1.9会对堆空间的内存碎片从小到大进行排序,因此对于内存碎片的利用率要高一些,再加上1.9的GC相对来说更积极主动一些,因此在一定程度上可以减轻内存碎片问题。
但不管怎么说,在可以预见的未来,Ruby的内存泄漏问题无可避免,我们还是要做好动不动杀掉ruby进程重起的准备。所以你要好好操练一下monit,或者像我一样,写个shell脚本进行监控,用crontab每隔几分钟跑一下。
我们的Rails部署方式是lighttpd+fcgi,用Unix Socket通信,监控脚本示例如下:
由于pmap命令可以dump进程的内存映射表,因此我们可以对比RubyVM和JVM在GC前后的内存映射情况。比方说Ruby的内存状况大概是这样的:
下图是一个Ruby进程的物理内存映射表,堆内存占据的空间是我抽取出来的三行:
00000000005d4000 125260K rwx-- [ anon ] 0000002a95c23000 23456K rw--- [ anon ] 0000002a99186000 50980K rw--- [ anon ]
0000000000400000 760K r-x-- /usr/local/ruby/bin/ruby 00000000005bd000 92K rw--- /usr/local/ruby/bin/ruby 00000000005d4000 125260K rwx-- [ anon ] 0000002a95556000 84K r-x-- /lib64/ld-2.3.3.so 0000002a9556b000 12K rw--- [ anon ] 0000002a9556e000 24K r--s- /usr/lib64/gconv/gconv-modules.cache 0000002a95574000 4K rw--- [ anon ] 0000002a95577000 12K rw--- [ anon ] 0000002a9557a000 204K r---- /usr/lib/locale/en_US.utf8/LC_CTYPE 0000002a9566a000 12K rw--- /lib64/ld-2.3.3.so 0000002a9566d000 8K r-x-- /lib64/libdl.so.2 0000002a9566f000 1024K ----- /lib64/libdl.so.2 0000002a9576f000 4K rw--- /lib64/libdl.so.2 ......
可以看出来Ruby的堆内存分配比较连续,分段不多。而JVM的堆内存分配段就很多了,由于堆地址段太多,我就不贴出来了,大家可以自己观察。
由于Ruby的内存分配算法和回收算法还是比较原始的,因此在进行多次回收之后,内存堆很容易出现大量的内存碎片,很多内存碎片并不能够被有效的利用,并且ruby没有好的碎片归并压缩算法,因此碎片造成的内存堆地址空间浪费就会越来越大。其结果就是Ruby进程在长期高负载运行之下,表现出缓慢的内存泄漏现象!
对比JVM的堆分配,他分配了很多的段,每个段的内存存活期不一样,根据分代算法,可以把不同存活期的对象在堆之间移动,堆内部则进行碎片归并。比方说我对一个Tomcat应用服务器的实际应用先pmap记录内存映射,然后GC,再pmap记录内存映射,两者diff一下,就可以发现某些堆在收缩,但是某些堆甚至在GC后扩张了,这便是对象在堆之间进行移动的现象。因此我不得不赞赏一下JVM的内存分配。
话说回来,由于Ruby的VM内存分配的碎片问题,导致Ruby进程几乎无可避免的内存泄漏。其结果就是你必须实时监控Ruby进程的运行情况,一旦发现内存使用超过限额,则必须果断的杀掉进程重起。比方说现在很多流行的Rails网站都是用monit去监控mongrel实例,一旦发现内存使用超过限额就杀掉重起。这种监控方式虽然可以有效的解决ruby的内存泄漏问题,但是太过简单粗暴,如果杀掉进程重起的时候,Ruby进程正好在处理请求,那么该请求是肯定会失败掉的,对于一些极端的情况,似乎很难令人接受这种现象的存在。我现在没有用monit,而是自己写shell脚本来监控(写几行shell就可以搞定的事情没必要那么麻烦搞什么monit),每天大概能出现两三次这种需要杀掉重起的情况,对比每天要处理将近100 万动态请求来说,可靠性还是达到了99.999%,还算可以。
那么将于今年年底发布的ruby 1.9.1能够解决这个问题吗? 答案是悲观的,1.9的GC并没有本质的提高,可以预见还是会出现无可避免的内存泄漏问题。但是1.9的内存泄漏会比现在的1.8要轻微一些,原因是1.9会对堆空间的内存碎片从小到大进行排序,因此对于内存碎片的利用率要高一些,再加上1.9的GC相对来说更积极主动一些,因此在一定程度上可以减轻内存碎片问题。
但不管怎么说,在可以预见的未来,Ruby的内存泄漏问题无可避免,我们还是要做好动不动杀掉ruby进程重起的准备。所以你要好好操练一下monit,或者像我一样,写个shell脚本进行监控,用crontab每隔几分钟跑一下。
我们的Rails部署方式是lighttpd+fcgi,用Unix Socket通信,监控脚本示例如下:
#!/bin/sh . /etc/profile.local RUBY_HEAP_MIN_SLOTS=600000 RUBY_HEAP_SLOTS_INCREMENT=600000 RUBY_HEAP_FREE_MIN=100000 RUBY_GC_MALLOC_LIMIT=60000000 RAILS_ENV=production export RUBY_HEAP_MIN_SLOTS RUBY_HEAP_SLOTS_INCREMENT RUBY_GC_MALLOC_LIMIT RUBY_HEAP_FREE_MIN RAILS_ENV SPAWN=/usr/local/lighttpd/bin/spawn-fcgi DISPATCH_PATH=/.../yourrailsapp/public/dispatch.fcgi SOCKET_PATH=/yourlighttpd/socket PID_PATH=/yourlighttpd/pids RSS_MAX=307200 for PSDATA in `ps -e v | grep dispatch.fcgi | awk '{print $1 ":" $8 }'` do RSS=${PSDATA#*:} PID=${PSDATA%:*} if [ $RSS -ge $RSS_MAX ]; then echo echo `date` echo "----------------------------------------" echo "PID["$PID"]: RSS="$RSS"KB is too big!" for num in 0 1 2 3 4 5 6 7 8 9 do if [ $PID -eq `cat $PID_PATH/javaeye.pid-$num` ]; then echo "PID["$PID"] using socket: "$num kill -9 $PID rm -rf $SOCKET_PATH/javaeye.socket-$num $SPAWN -f $DISPATCH_PATH -s $SOCKET_PATH/javaeye.socket-$num -P $PID_PATH/javaeye.pid-$num fi done fi done sleep 10 for num in 0 1 2 3 4 5 6 7 8 9 do if [ ! -d /proc/`cat $PID_PATH/javaeye.pid-$num` ]; then echo echo "Ruby Server using socket: "$num" had been crashed, need to be starting..." rm -rf $SOCKET_PATH/javaeye.socket-$num $SPAWN -f $DISPATCH_PATH -s $SOCKET_PATH/javaeye.socket-$num -P $PID_PATH/javaeye.pid-$num fi done