Ruby 关于 Ruby 内存使用的一些优化和探索

jackxu · 发布于 2015年08月24日 · 最后由 realwol 回复于 2016年12月21日 · 3292 次阅读
19891

前段时间写了一个benchmark来测试下最近写的一个服务的api,发现随着访问次数的增加,使用内存在慢慢上升,在上升到一定量之后,使用的内存基本稳定了下来。当然除了本身是一个图片上传的服务,需要消耗内存相比一般的request更多点之外,还是想弄明白下ruby内存是怎样一种回收和使用机制,为什么短时间内Ruby进程消耗的内存会增加如此之快,Ruby内部的Garbage Collect(简称GC)到底多久回收一次垃圾,到底怎么个回收流程,到底能把消耗的内存变小多少。于是便查阅了下GC相关的一些文档和内存跟踪的一些使用工具,实践并总结了下可以优化内存的一些方法,希望能对大家有所帮助。本文中使用的Ruby环境是Ruby 2.1.4 + Rails 4.2.1, 后端app server使用的是Unicorn, 运行在2台AWS t2.large 上面,以及为了测试方便的本地mac机器(OS X 10.10.1)。由于本人经验和认知有限,若有失实之处,还望指正。也希望大家能畅所欲言,探讨并分享下自己的理解。

app server 方面着手

首先来看下测试开始前的状态Unicorn的内存使用状态,通过下面的命令查来看下占用内存最多的15个应用进程(在此隐去真实的服务器和项目相关信息,但测试数据均为实测数据)。你可以发现,这这里消耗内存最多的是我们的Unicorn应用进程,我在Unicorn配置文件里,每台机器配置了10个worker_processes(为了让测试的效果更为拔群)。每个Unicorn进程大概占用了2%左右的内存,再加上master进程,总共占用了21%的内存总量。最初始的每个Unicorn所使用的内存量和你工程本身的大小有关系。在一个空的Rails工程中(本文使用Rails 4.2.1测试),占用了70多m左右的内存。

[user001@xxxx-web01 ~]$ ps -eo pmem,pcpu,vsize,pid,cmd | sort -k 1 -nr | head -15
 2.0  0.0 545968 25897 unicorn_rails master -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26124 unicorn_rails worker[9] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26119 unicorn_rails worker[8] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26116 unicorn_rails worker[7] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26111 unicorn_rails worker[6] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26104 unicorn_rails worker[5] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26100 unicorn_rails worker[4] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26098 unicorn_rails worker[3] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26092 unicorn_rails worker[2] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26090 unicorn_rails worker[1] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 1.9  0.0 545968 26088 unicorn_rails worker[0] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 0.6  0.0 1243972 2447 /opt/ds_agent/ds_agent -w /var/opt/ds_agent
 0.1  0.0 155540 17719 /usr/bin/perl -wT /usr/sbin/munin-node
 0.0  0.0 243976  2427 /sbin/rsyslogd -i /var/run/syslogd.pid -c 5

下面我们来运行下benchmark,benchmark采用了多线程的方式来访问我的api服务,在近三分钟的时间内,总共上传了1500张图片并做了相关的人脸识别处理,再来看下我们的内存占用前15位的内存使用状况。我们能够发现,Unicorn进程的内存除了master进程之外,各个workers都和以前相比有了较大的增加。从最低的1.9%增加到了3.6%左右。

[user001@ipsa-web01 ~]$ ps -eo pmem,pcpu,vsize,pid,cmd | sort -k 1 -nr | head -15
 3.6  0.0 675644 26119 unicorn_rails worker[8] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 671416 26090 unicorn_rails worker[1] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 669144 26098 unicorn_rails worker[3] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.5  0.0 668872 26092 unicorn_rails worker[2] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.5  0.0 667220 26100 unicorn_rails worker[4] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.5  0.0 663384 26116 unicorn_rails worker[7] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.4  0.0 661892 26088 unicorn_rails worker[0] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.3  0.0 650784 26111 unicorn_rails worker[6] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.3  0.0 645692 26124 unicorn_rails worker[9] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.2  0.0 647860 26104 unicorn_rails worker[5] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 2.0  0.0 545968 25897 unicorn_rails master -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 0.6  0.0 1243972 2447 /opt/ds_agent/ds_agent -w /var/opt/ds_agent
 0.1  0.0 155540 17719 /usr/bin/perl -wT /usr/sbin/munin-node
 0.0  0.0 243976  2427 /sbin/rsyslogd -i /var/run/syslogd.pid -c 5 

在此基础上我再一次上传了1500张图片,下面是跑完之后各个进程的内存使用情况。我们发现这次各个Unicorn worker使用内存只是略有增加,但已经远小于上一次跑benchmark时的内存增幅了。

[user001@ipsa-web01 ~]$ ps -eo pmem,pcpu,vsize,pid,cmd | sort -k 1 -nr | head -15 
 4.1  0.0 709428 26098 unicorn_rails worker[3] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.8  0.0 689600 26116 unicorn_rails worker[7] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.7  0.0 691624 26104 unicorn_rails worker[5] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 676696 26088 unicorn_rails worker[0] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 675644 26119 unicorn_rails worker[8] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 671520 26100 unicorn_rails worker[4] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 671416 26090 unicorn_rails worker[1] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 669372 26124 unicorn_rails worker[9] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.6  0.0 666984 26092 unicorn_rails worker[2] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 3.2  0.0 651596 26111 unicorn_rails worker[6] -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 2.0  0.0 545968 25897 unicorn_rails master -c /home/app/image_test/production/current/config/unicorn.rb -E production -D
 0.6  0.0 1243972 2447 /opt/ds_agent/ds_agent -w /var/opt/ds_agent
 0.1  0.0 155540 17719 /usr/bin/perl -wT /usr/sbin/munin-node
 0.0  0.0 243976  2427 /sbin/rsyslogd -i /var/run/syslogd.pid -c 5 

然而,可以发现的是内存占用最大的进程从已经从最初的1.9%到了4.1%,如果继续这样下去的话,每个Unicorn worker使用的内存可能会越来越大(事实上也是如此,在此就不再把相关的运行结果列出来了)。这个时候,如果我们能够限制每个Unicorn worker的大小,总体来说就能控制了Unicorn所占用的总内存大小。 这个时候,我们可以利用一个叫unicorn-woker-killer 的工具来达到这一目的。它可以设置你的request个数达到设定值的时候杀死worker,也可以在你worker内存达到设定值的时候杀死这个worker。它的优点在于在杀死unicorn woker的时候并不会影响到你的request请求。

这个gem的使用方法非常简单, 首先你的Gemfile里面(最好放在group :production下面)设置如下:

group :production do
  gem 'unicorn-worker-killer
end

然后在你Rails应用的config.ru文件中,require ::File.expand_path('../config/environment', __FILE__)之前,加入如下代码。

require 'unicorn/worker_killer' 
use Unicorn::WorkerKiller::Oom, (260*(1024**2)), (290*(1024*2)) 

这里的 Unicorn::WorkerKiller::Oom指定的是内存,在这里设定worker使用内存达到260M和290M之间的值某个值时,就把这个worker杀死。注意这里某个值指的是260M到290M之间的一个随机值。(另:unicorn-worker-killer也可以通过设置max的request个数来杀死worker,大家可以根据自己的需要设定,在这里就暂时不介绍)

设置好了之后,我们再来一发(上传1500张图片并处理),看看占用前15的进程有什么变化。(注意:由于设置好了需要重启Unicorn,为了观察进程的变化,上面的设置我试验前就设置并部署好了)

[user001@xxxx-web01 ~]$ ps -eo pmem,pcpu,vsize,pid,cmd | sort -k 1 -nr | head -15
 4.1  0.0 709428 26098 unicorn_rails worker[3] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.9  0.0 693076 26092 unicorn_rails worker[2] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.8  0.0 699880 26124 unicorn_rails worker[9] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.8  0.0 691624 26104 unicorn_rails worker[5] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.8  0.0 689600 26116 unicorn_rails worker[7] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.6  0.0 676696 26088 unicorn_rails worker[0] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.6  0.0 675644 26119 unicorn_rails worker[8] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.6  0.0 671520 26100 unicorn_rails worker[4] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.1  0.0 634372 26111 unicorn_rails worker[6] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 2.9  1.2 619084 23679 unicorn_rails worker[1] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 2.0  0.0 545968 25897 unicorn_rails master -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 0.6  0.0 1243972 2447 /opt/ds_agent/ds_agent -w /var/opt/ds_agent
 0.1  0.0 155540 17719 /usr/bin/perl -wT /usr/sbin/munin-node
 0.0  0.0 243976  2427 /sbin/rsyslogd -i /var/run/syslogd.pid -c 5

对比下上一次前15位的进程,我们发现一个变化,woker[1]的pid发生了改变,并且由原来的内存使用3.6%变为2.9%, 这点说明,unicorn-woker-killer确实在限制各个unicorn worker大小的时候起到了作用,并在总使用内存的控制上起到了作用。当然,通过unicorn-worker-killer方式可以做到杀死worker,也就可以通过batch的方式让他自动重启了,比如在cron job中根据时间来定期杀死worker进程,但这需要根据你的实际需要而定。

探讨完工具unicorn-worker-killer的使用,你可能也会发现,这Unicorn worker的数目也太多了。为了实验的效果更为拔群,我在每台机器上启动了10个worker来处理request。但实际使用的时候,你到底要需要几个Unicorn worker是你的访问量和请求处理的复杂度来决定的。当然理想的情况下,我希望有大概20%-30%(这个数据只是我自己的一个喜好,并不代表标准)左右的worker处于比较空闲的状态,以便高峰到来的时候能够进行处理。那么,我们将Unicorn的worker_processes设置为6,再次运行下benchmark,来看下Unicorn使用内存的情况。

[user001@ipsa-web01 ~]$ ps ax -eo pmem,pcpu,pid,cmd | sort -k 1 -nr | head -15
 4.0  0.0  3947 unicorn_rails worker[5] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.6  0.0  3932 unicorn_rails worker[1] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.5  0.0  3938 unicorn_rails worker[3] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.5  0.0  3936 unicorn_rails worker[2] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.5  0.0  3930 unicorn_rails worker[0] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 3.3  0.0  3941 unicorn_rails worker[4] -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 2.0  0.0  3743 unicorn_rails master -c /home/app/ipsa/production/current/config/unicorn.rb -E production -D
 0.5  0.0  2130 /opt/ds_agent/ds_agent -w /var/opt/ds_agent
 0.1  0.0  2555 /usr/bin/perl -wT /usr/sbin/munin-node
 0.0  0.1  6824 nginx: worker process
 0.0  0.0 24457 [kworker/1:2]
 0.0  0.0 16735 [kworker/0:0]
 0.0  0.0 15440 [kworker/0:1H]
 0.0  0.0  7699 head -15

上面的结果可以看到,虽然单个的Unicorn worker占用内存没有多大变化,但整体的Unicorn进程个数从10到6,使用的总内存已经减少不小了。在使用前后两次benchmark的时候,记录了下运行时间,10个worker的时候比6个worker的时候快10几秒,而不是第一次整体运行时间的2/5,这说明其实最开始Unicorn worker是过多的,远超出我们的实际需要。由此可见,在满足访问需求的合理的前提下,尽量减少Unicorn worker的个数,能较大程度的减少你内存的使用。

Ruby的GC和Rails方面着手

通过上面Unicorn的方式来改变内存使用的大小, 虽然达到了效果,但还是没明白内存使用量变化究竟是怎么一回事。Unicorn的内存使用中,除了最开始访问的时候需要将相关的代码加载入内存,程序内构建的对象所占用的内存外(包括代码,中间变量及请求数据和返回数据),以及一部分cache之外及程序可能存在memory leak之外,是什么东西的增加,让一个worker从100多m变到最大300m左右?Ruby的垃圾回收是怎么工作的,怎么去判断他在正常工作呢?好在Ruby给我们提供了一些比较直观的module。它们就是GCObjectSpace

GC提供了一些方法来了解Ruby的垃圾回收机制,ObjectSpace则可以允许你查看现行状态下living objects的状态。在Ruby的GC中,采用了一种叫mark and sweep的机制来回收垃圾。比较简便的来说,GC在回收垃圾的时候,它会从根对象开始遍历内存中的对象,如果能够reach那个对象,就mark下,最后再检查下所有的对象,看那些对象没有被mark,就回收这些对象(即sweep)。此处有一幅较为清楚的图来表现这一过程(来源

通过这一过程,能够大致的有个形象的认知,但是并不明了Ruby对象在内存中是怎样存在的。在MRI中,Ruby有这样一种类似于C语言中结构体的结构,Ruby中称其为RValue结构,对象被存放在这个结构体中。在结构体中,除了对象之外,还附有一个flag,也就是我们前面所讲的在mark-and-sweep机制被mark时要使用的flag。内存中,Ruby将许多这样的RValue结构体组织在一种被称为heap的数组里面。在一个进程当中,有许多这样的heap数组存在。每个RValue结构体的大小是40bytes,这也预示着这里不能存放较大的对象,故Ruby将一些较小的常用的String, Array或Hash等对象存放在此处。(详情可参考此文

有了上面的一些基本的了解,我们就可以通过小小的实验来查看下GC的运行机制了。在GC module中,为我们提供了一个GC.stat方法来查看GC的状态。 下面在irb中来看看GC为我们提供了那些状态。

  ~  irb --simple-prompt
>> GC.stat
{
                            :count => 34,
                        :heap_used => 261,
                      :heap_length => 261,
                   :heap_increment => 0,
                   :heap_live_slot => 106167,
                   :heap_free_slot => 216,
                  :heap_final_slot => 0,
                  :heap_swept_slot => 12646,
            :heap_eden_page_length => 261,
            :heap_tomb_page_length => 0,
           :total_allocated_object => 891605,
               :total_freed_object => 785438,
                  :malloc_increase => 312768,
                     :malloc_limit => 16777216,
                   :minor_gc_count => 29,
                   :major_gc_count => 5,
          :remembered_shady_object => 784,
    :remembered_shady_object_limit => 850,
                       :old_object => 73265,
                 :old_object_limit => 82108,
               :oldmalloc_increase => 1036768,
                  :oldmalloc_limit => 16777216
}

GC.stat给我们返回了一个Hash,其中count所指向的值是GC运行的次数,它是由minor_gc_count + major_gc_count (29 + 5 = 34)来组成的。其中minor_gc_count指的是GC进行的一次小型的回收,而major_gc_count则是GC进行的是一次full垃圾回收。之所以这样区分开来,是因为每次minor回收所需要使用的时间远小于major使用的时间。每次进行GC回收的时候,会消耗很大的资源和性能。根据情况进行minor回收能够较好的实现性能的提升。head_used指代的是当前正在使用的heap的个数,而heap_live_slot指的是在GC运行以来,还存活的对象的个数。在此处,一个heap可以简单的理解为有很多个slot来组成,而heap_free_slot就是原来已经被分配的slot,但是里面的对象已经被释放了,可以用来存放新的对象的slot的个数。

根据上面的一些概念,我们来做一个简单的实验。写一个简单的script,来测试下GC运行的状态和内存使用的情况。script如下:

 1 def report
 2   puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
 3         .strip.split.map(&:to_i)[1].to_s + 'KB'
 4 end
 5
 6 report
 7 puts GC.stat
 8
 9 long_str = "a" * 10_000_000
10 report
11 puts GC.stat
12
13 long_str = nil
14 ObjectSpace.garbage_collect
15 report
16 puts GC.stat
17
18 sleep 3
19 report
20 puts GC.stat 

来看下运行的结果:

ruby -v memory_profiler.rb

ruby 2.1.4p265 (2014-10-27 revision 48166) [x86_64-linux]
Memory 7196KB
{:count=>5, :heap_used=>57, :heap_length=>81, :heap_increment=>24, :heap_live_slot=>22865, :heap_free_slot=>368, :heap_final_slot=>0, :heap_swept_slot=>3398, :heap_eden_page_length=>57, :heap_tomb_page_length=>0, :total_allocated_object=>41004, :total_freed_object=>18139, :malloc_increase=>519376, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>2, :remembered_shady_object=>151, :remembered_shady_object_limit=>300, :old_object=>5855, :old_object_limit=>11710, :oldmalloc_increase=>519824, :oldmalloc_limit=>16777216}
Memory 17036KB
{:count=>5, :heap_used=>58, :heap_length=>81, :heap_increment=>23, :heap_live_slot=>22938, :heap_free_slot=>703, :heap_final_slot=>0, :heap_swept_slot=>3398, :heap_eden_page_length=>58, :heap_tomb_page_length=>0, :total_allocated_object=>41077, :total_freed_object=>18139, :malloc_increase=>10533056, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>2, :remembered_shady_object=>151, :remembered_shady_object_limit=>300, :old_object=>5855, :old_object_limit=>11710, :oldmalloc_increase=>10533504, :oldmalloc_limit=>16777216}
Memory 7340KB
{:count=>6, :heap_used=>58, :heap_length=>81, :heap_increment=>23, :heap_live_slot=>7218, :heap_free_slot=>16423, :heap_final_slot=>0, :heap_swept_slot=>16446, :heap_eden_page_length=>58, :heap_tomb_page_length=>0, :total_allocated_object=>41146, :total_freed_object=>33928, :malloc_increase=>9448, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>3, :remembered_shady_object=>162, :remembered_shady_object_limit=>324, :old_object=>7008, :old_object_limit=>14016, :oldmalloc_increase=>9896, :oldmalloc_limit=>16777216}
Memory 7344KB
{:count=>6, :heap_used=>58, :heap_length=>81, :heap_increment=>23, :heap_live_slot=>7287, :heap_free_slot=>16354, :heap_final_slot=>0, :heap_swept_slot=>16446, :heap_eden_page_length=>58, :heap_tomb_page_length=>0, :total_allocated_object=>41215, :total_freed_object=>33928, :malloc_increase=>20568, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>3, :remembered_shady_object=>162, :remembered_shady_object_limit=>324, :old_object=>7008, :old_object_limit=>14016, :oldmalloc_increase=>21016, :oldmalloc_limit=>16777216} 

我们可以看到,在创建long_str对象之前,进程的内存是7196KB,在创建long_str之后,使用内存上升到17036KB, 这个是可以理解的。再看我们在script的第13,14行将long_str置空,并通过 ObjectSpace强行启动一次回收,使用内存是7340KB, 使用的内存变小了,基本回到了对象创建前的大小。GC.stat中的count从5变成了6,而且我们发现是major_gc_count从2变成了3,也就是说启动的是一次full回收。并且,heap_live_slot从22938变成了7218,存活的对象的slot数目大幅减小,也就是说明很多对象已经被回收了。heap_free_slot从原来的703变成了16423,这个说明接下来如果进程还需要创建别的对象的话(需要的slot小于当前的free_slot),可能就不需要操作系统分配新的内存而直接使用现在空闲的free_slot了。到底是不是上面的猜想呢,那我们再来实践下吧,将上面脚本的18,19,20行改成如下( 字符从a变成了b):

18 long_str = b" * 10_000_000
19 report
20 puts GC.stat 

看下结果如何:

ruby -v memory_profiler.rb

ruby 2.1.4p265 (2014-10-27 revision 48166) [x86_64-linux]
Memory 7180KB
{:count=>5, :heap_used=>56, :heap_length=>81, :heap_increment=>25, :heap_live_slot=>22420, :heap_free_slot=>404, :heap_final_slot=>0, :heap_swept_slot=>3845, :heap_eden_page_length=>56, :heap_tomb_page_length=>0, :total_allocated_object=>41007, :total_freed_object=>18587, :malloc_increase=>518040, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>2, :remembered_shady_object=>151, :remembered_shady_object_limit=>300, :old_object=>5855, :old_object_limit=>11710, :oldmalloc_increase=>518488, :oldmalloc_limit=>16777216}
Memory 17028KB
{:count=>5, :heap_used=>57, :heap_length=>81, :heap_increment=>24, :heap_live_slot=>22493, :heap_free_slot=>739, :heap_final_slot=>0, :heap_swept_slot=>3845, :heap_eden_page_length=>57, :heap_tomb_page_length=>0, :total_allocated_object=>41080, :total_freed_object=>18587, :malloc_increase=>10531688, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>2, :remembered_shady_object=>151, :remembered_shady_object_limit=>300, :old_object=>5855, :old_object_limit=>11710, :oldmalloc_increase=>10532136, :oldmalloc_limit=>16777216}
Memory 7320KB
{:count=>6, :heap_used=>57, :heap_length=>81, :heap_increment=>24, :heap_live_slot=>7219, :heap_free_slot=>16013, :heap_final_slot=>0, :heap_swept_slot=>16036, :heap_eden_page_length=>57, :heap_tomb_page_length=>0, :total_allocated_object=>41149, :total_freed_object=>33930, :malloc_increase=>9432, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>3, :remembered_shady_object=>162, :remembered_shady_object_limit=>324, :old_object=>7009, :old_object_limit=>14018, :oldmalloc_increase=>9880, :oldmalloc_limit=>16777216}
Memory 17064KB
{:count=>6, :heap_used=>57, :heap_length=>81, :heap_increment=>24, :heap_live_slot=>7290, :heap_free_slot=>15942, :heap_final_slot=>0, :heap_swept_slot=>16036, :heap_eden_page_length=>57, :heap_tomb_page_length=>0, :total_allocated_object=>41220, :total_freed_object=>33930, :malloc_increase=>10020560, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>3, :remembered_shady_object=>162, :remembered_shady_object_limit=>324, :old_object=>7009, :old_object_limit=>14018, :oldmalloc_increase=>10021008, :oldmalloc_limit=>16777216}

可以发现使用的内存确实增加了,从7320KB变成了17064KB,然而当前使用的heap个数(heap_used)并没有变化,heap_live_slot和heap_free_slot的变化也不大。Ruby并没有将新的对象大量填充到heap_free_slot,正因为先前所说的,heap中的每个RValue结构体大小为40bytes,并不能存放long_str那么大的字符串,当超出这个范围的时,Ruby将其存放到了系统的heap去了,已经不由RValue结构来存储了。从上面的两次实验中也能看出,在每次启动full回收的时候,存储在系统heap中的大的对象被回收了,并且将这部分的内存归还给了操作系统。那么接下来就对比下,对于存放在RValue结构体中的变量,内存在垃圾回收后会不会将内存交还给系统呢?

将上面的最初始的代码做如下修改, 将名称改为long_array, 并赋予一个很长的字符串数组:

#long_str = "a" * 10_000_000
long_array = 10_000_000.times.map(&:to_s)
#long_str = nil
long_array = nil 

看下改变之后的内存使用状态:

ruby -v memory_profiler.rb

ruby 2.1.4p265 (2014-10-27 revision 48166) [x86_64-linux]
Memory 7172KB
{:count=>5, :heap_used=>56, :heap_length=>81, :heap_increment=>25, :heap_live_slot=>22416, :heap_free_slot=>407, :heap_final_slot=>0, :heap_swept_slot=>3848, :heap_eden_page_length=>56, :heap_tomb_page_length=>0, :total_allocated_object=>41002, :total_freed_object=>18586, :malloc_increase=>517832, :malloc_limit=>16777216, :minor_gc_count=>3, :major_gc_count=>2, :remembered_shady_object=>151, :remembered_shady_object_limit=>300, :old_object=>5855, :old_object_limit=>11710, :oldmalloc_increase=>518280, :oldmalloc_limit=>16777216}
Memory 511440KB
{:count=>16, :heap_used=>24552, :heap_length=>28695, :heap_increment=>4143, :heap_live_slot=>10007226, :heap_free_slot=>341, :heap_final_slot=>0, :heap_swept_slot=>0, :heap_eden_page_length=>24552, :heap_tomb_page_length=>0, :total_allocated_object=>10041080, :total_freed_object=>33854, :malloc_increase=>29463768, :malloc_limit=>29936205, :minor_gc_count=>9, :major_gc_count=>7, :remembered_shady_object=>162, :remembered_shady_object_limit=>324, :old_object=>6497858, :old_object_limit=>7219894, :oldmalloc_increase=>49100440, :oldmalloc_limit=>20132659}
Memory 433404KB
{:count=>17, :heap_used=>19660, :heap_length=>28695, :heap_increment=>19603, :heap_live_slot=>7221, :heap_free_slot=>8006375, :heap_final_slot=>0, :heap_swept_slot=>8006398, :heap_eden_page_length=>57, :heap_tomb_page_length=>19603, :total_allocated_object=>10041149, :total_freed_object=>10033928, :malloc_increase=>9432, :malloc_limit=>29337480, :minor_gc_count=>9, :major_gc_count=>8, :remembered_shady_object=>162, :remembered_shady_object_limit=>324, :old_object=>7007, :old_object_limit=>14014, :oldmalloc_increase=>9880, :oldmalloc_limit=>19737900}
Memory 433408KB
{:count=>17, :heap_used=>19660, :heap_length=>28695, :heap_increment=>19603, :heap_live_slot=>7290, :heap_free_slot=>8006306, :heap_final_slot=>0, :heap_swept_slot=>8006398, :heap_eden_page_length=>57, :heap_tomb_page_length=>19603, :total_allocated_object=>10041218, :total_freed_object=>10033928, :malloc_increase=>20552, :malloc_limit=>29337480, :minor_gc_count=>9, :major_gc_count=>8, :remembered_shady_object=>162, :remembered_shady_object_limit=>324, :old_object=>7007, :old_object_limit=>14014, :oldmalloc_increase=>21000, :oldmalloc_limit=>19737900} 

可以发现,分配大得数组之后使用内存飙升,从7172KB到511440KB,GC运行的次数也从5变成了16,为系统分配了大量内存。在强行启动一次full回收之后,内存变为了433404KB。和先前的例子对比下,发现内存在强行回收之后并没有回到之前的近似7172KB的内存大小, 这是为什么呢?可以看到,long_array是一个长度很大的数组,但是数组中的每个元素都很小,RValue足够装下它。因此,虽然数组本身很大,但是这里的数组的元素还都是存在RValue结构中的,所以创建数组后heap_used的从56激增至24552。但是在启动full回收后然而启动回收之后heap_used变成19660,说明有一部分heap被回收了,仍然有很大一部分heap在使用。那又没有装对象呢?可以看到heap_live_slot由10007226个变成7221,说明大部分对象都被释放掉了。很显然,Ruby在释放对象这部分对象之后,并没有这部分对象占有的heap的内存全部交还给操作系统,即使你启动的是一次full的回收。这是Ruby的一种平衡,使得Ruby在下次分配对象的时候不需要时刻都向操作系统请求内存资源,而可以直接使用原来使用过的free_slot。

由此可见,不管是在你的程序中哪里,应尽量去避免创建这种极大的对象。因为一般情况下,Ruby为了性能考虑,并不会经常启动full回收,也就是说有一部分对象并不会做sweep处理,依然残留在内存中,这会导致你的内存逐渐变大。即使对象被回收之后,内存也不会立马变得很小。结合我们前面的Unicorn的实例,我在极短的时间内,做了1500次图片的上传及处理,内存也在短时间内飙升,并在一定时间内并没有减小太多,这个道理是一致的。Ruby在短时间内大量对象被创建的时候,是会申请较多的内存来处理的。即使在free掉相关对象后不将这部分内存归还给操作系统,所以你看到的现象是我为什么请求都停下来了,内存怎么还不降下去呢。当内存上升到一定程度,Ruby会使用先前的free_slot来存放新的对象,这也就是为什么访问上升到一定程度之后,继续请求,内存使用增加依然较小。

当然,除了上述情况之外,我们在写Rails和Ruby的时候还是要注意内存泄露的问题。在创建这些对象的时候,尽量不要把对象赋予给全局的变量或是类或module的属性,这会导致这些对象一直被retained而无法释放。此外,比较常用的跟ActiveRecord相关的操作,使用的好坏也对Rails内存有较大的影响。比如model的all和find_each方法的使用。在对数据库操作的时候,循环的使用是否合理等等,都对内存的使用会产生重大的影响。这里就不一一列举了。当然,应该还有许多我不知的相关方法来避免内存泄露和减少内存使用的问题,欢迎大家补充。

此外,由于Ruby的不同版本,采用的GC实现方式也不同,特别是早前的1.8, 1.9, 升级到2.0之后的版本,性能会有较大的提升。特别是web app, 最开始都使用的是同一份代码,由于Ruby版本GC的实现机制不同,导致Copy On Write机制被利用程度也不同。在此就不详细展开了。详细可以参考此文

总结下,以Unicorn服务器为例,可以通过减少Unicorn 进程个数,杀死并重启新的Unicorn Worker来达到减少内存的使用。此外,通过对Ruby的GC机制的探讨,你可以尽量少创建大的对象及减少对全局变量的使用来控制内存。由于Ruby版本对于不同GC实现机制的不同,使得资源的使用效率上有着很大的差别,所以尽量把你的Ruby版本升级到2.0以上。

下面的参考文献对文中提到的有一些概念都有比较深入的探讨,建议有时间可以阅读下。另外,附几个查看内存相关的gem

参考文献: http://patshaughnessy.net/2012/3/23/why-you-should-be-excited-about-garbage-collection-in-ruby-2-0 http://engineering.heroku.com/blogs/2015-02-04-incremental-gc/ http://samsaffron.com/archive/2013/11/22/demystifying-the-ruby-gc http://stackoverflow.com/questions/20385767/finding-the-cause-of-a-memory-leak-in-ruby

参考gems: https://github.com/tmm1/rbtrace 这个可以在你测试时查看GC等运行的状态,GC多久跑一次等都可以监测到 https://github.com/SamSaffron/memory_profiler 查看运行程序的内存状态 https://github.com/ko1/gc_tracer Ruby GC作者写的,虽然没有试用过_^

共收到 6 条回复
15615

内容比较丰富,慢慢消化

7659

如果不是使用unicorn服务器,可以使用God监控内存,当超过设定的阀值自动重启,但不确定是否影响请求。

19891

#1楼 @douxiance 比较长,可以分成两部分来读~

19891

#2楼 @chq God确实可以在配置文件里面设置memory和cpu的使用极限,但这部分我也没有实际的使用经验,只是知道而已。

15615

@jackxu 会的,多谢!

4933

#2楼 @chq #4楼 @jackxu 清理收藏时候看到了 顺便回复一下 god 在kill的时候 只负责发信号给程序 程序自己控制请求完成之后杀 还是立刻杀 rej神告诉我的

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册