查找 Ruby 内存泄露

May 29, 2016 10:28


查找方式

基本上分为两种方式

本文主要讲的是动态分析

检查 Ruby 对象分配

Dump Objects

通过 ObjectSpace#dump 方法,可以得到一个对象在内存的位置,及它的一些类型、内存占用等信息

require 'objspace'

class A
  def initialize
    @a = 'hello'
  end
end

a = A.new
puts '=' * 80
puts "a dump: #{ObjectSpace.dump(a)}"
puts
puts "A dump: #{ObjectSpace.dump(A)}"
puts
puts "hello dump: #{ObjectSpace.dump('hello')}"

输出

a dump: {"address":"0x007fd77c134560", "type":"OBJECT", "class":"0x007fd77c1345d8", "ivars":3, "references":["0x007fd77c134538"], "memsize":40, "flags":{"wb_protected":true}}
A dump: {"address":"0x007fd77c1345d8", "type":"CLASS", "class":"0x007fd77c1345b0", "name":"A", "references":["0x007fd77c1345d8", "0x007fd77c135118", "0x007fd77c136220", "0x007fd77c8dec70"], "memsize":904, "flags":{"wb_protected":true}}
hello dump: {"address":"0x007fd77c134380", "type":"STRING", "class":"0x007fd77c8dcec0", "embedded":true, "bytesize":5, "value":"hello", "encoding":"UTF-8", "memsize":40, "flags":{"wb_protected":true}}

从上面可以看到通过 dump 可以输出一个对象信息的 json,有对象 address type memsize 等内容, 同样的,我们可以使用 ObjectSpace#dump_all 来将当前进程的所有对象输出到一个文件

require 'objspace'

name = "what's your name?"
str = 'asdfasdf'
array = ['test_array1', str]

out_path = File.expand_path("../dump.json", __FILE__)
File.open(out_path, 'w+') do |file|
  ObjectSpace.dump_all(output: file)
end

Trace Objects

通过 ObjectSpace 的 trace object allocation 系列方法,可以追踪到对象是由哪一行代码分配的

file: trace/trace.rb

require 'objspace'

extend ObjectSpace

def create_string
  "hello"
end

trace_object_allocations do
  obj = Object.new

  [obj, create_string].each do |o|
    puts "#{o.inspect} in #{allocation_sourcefile(o)}:#{allocation_sourceline(o)}"
  end
end

输出

#<Object:0x007fbba9084730> in trace/trace.rb:10
"hello" in trace/trace.rb:6

如果我们结合 dump 使用的话,dump 出来的数据就会带有每个对象的生成位置

file: traceanddump/traceanddump.rb

require 'objspace'

def create_string
  "hello"
end

ObjectSpace.trace_object_allocations do
  obj = Object.new
  str = create_string

  puts "obj dump: #{ObjectSpace.dump(obj)}"
  puts "str dump: #{ObjectSpace.dump(str)}"
end

输出

obj dump: {"address":"0x007f93eb884150", "type":"OBJECT", "class":"0x007f93ea8dec78", "ivars":0, "file":"trace_and_dump/trace_and_dump.rb", "line":8, "method":"new", "generation":4, "memsize":40, "flags":{"wb_protected":true}}
str dump: {"address":"0x007f93eb884128", "type":"STRING", "class":"0x007f93ea8dcec8", "embedded":true, "bytesize":5, "value":"hello", "encoding":"UTF-8", "file":"trace_and_dump/trace_and_dump.rb", "line":4, "method":"create_string", "generation":4, "memsize":40, "flags":{"wb_protected":true}}

从上面输出可以看到,对象分配的 file line 已经出现在 dump 的结果当中了,通过 ObjectSpace#dump_all 的数据同样会带有对象的分配位置信息

分析内存泄露

判定方式

如果一个 object 在运行时创建后,一直存在而没有被 GC 清理的话,很可能就是造成内存泄露的 object

分析过程

  1. dump 程序三个时段的所有 objects
  2. 比较第一二时段新增 objects,得到一些可疑对象
  3. 将可疑对象与第三时段的 objects 对比,如果依然存在,很可能就是内存泄露的凶手了

Dump Example

下面示例假设是在一个 Rails 项目中,通过 Thread.new { Debugger.new.trace_objs } 会异步 dump 三个时段的所有对象到三个文件中

require 'objspace'

class Debugger
  def trace_objs(interval = 15, out_dir = nil)
    puts "[ Debugger ] start trace objs"

    out_dir ||= Rails.root.join('tmp/objs_dump')
    ts = Time.now.to_i

    ObjectSpace.trace_object_allocations do
      dump_objs(out_dir.join("#{ts}-1.json"))
      sleep interval
      dump_objs(out_dir.join("#{ts}-2.json"))
      sleep interval
      dump_objs(out_dir.join("#{ts}-3.json"))
    end

    puts "[ Debugger ] trace end"
  end


  private

  def dump_objs(out_path)
    puts "[ Debugger ] dump objs"
    GC.start
    File.open(out_path, 'w') do |file|
      ObjectSpace.dump_all(output: file)
    end
  end
end

分析代码

分析代码源自 http://blog.skylight.io/hunting-for-leaks-in-ruby/ ,我优化了下分析速度,由于过长,具体代码就不贴在这了,有需要的同学可以去下面的链接中参考 objsdumpanalyzer.rb

具体过程其实就是按上面的分析过程来解析 dump 数据并打印相关的对象泄露信息,下面是输出样式

Leaked 566 STRING objectsof size 24215/51329 at: :
Leaked 279 ARRAY objectsof size 0/15824 at: :
Leaked 171 HASH objectsof size 0/38856 at: :
Leaked 168 STRING objectsof size 4540/11217 at: /Users/xxx/tmp/blog/lib/debugger.rb:28
Leaked 145 CLASS objectsof size 0/146632 at: :
Leaked 144 OBJECT objectsof size 0/10552 at: :
Leaked 106 NODE objectsof size 0/4240 at: :
Leaked 100 STRING objectsof size 500/4000 at: /Users/xxx/tmp/blog/app/controllers/topics_controller.rb:15
...

上面输出中没有文件名的,就是 Ruby 内部创建的(我猜的),但我没有过滤; deubgger.rb:28 中是我们 dump 时执行的代码,直接无视,第一次执行时会出现的情况; topics_controller.rb:15 是我故意写的内存泄露代码,已经被找出来的

线上 debug

很多时候,只有在线上大量请求处理时才可以明显的看出内存泄露,所以我们需要在线上执行上面的 dump 操作,rbtrace 可以帮我们在已经运行的进程上执行代码

rbtrace -p $app_pid -e "Thread.new { Debugger.new.trace_objs }"

作者表明这个在 production 运行基本上不会产生影响 rbtrace is designed to have minimal overhead, and should be safe to run in production.

最后

看了上面的,相信我们已经可以快快乐乐的线上 debug 内存泄露了,如果因此文你解决了内存泄露的问题,不用谢,如果因此文你把线上服务弄挂了,请不要给我寄刀片:),折腾线上服务需慎重!

Comments: