ObjectSpace を利用して Ruby 製 worker のメモリ使用量を改善した

現在、Ruby 製 worker を作成することを業務で行っているのですが、継続的にメモリ使用量が増加する現象に遭遇してました。 (グラフの値が下がったタイミングが数回ありますが、それぞれでデプロイが行われてリセットされただけ)

ObjectSpace.allocation_sourcefileObjectSpace.allocation_sourceline を利用してどのファイルのどの行でメモリ確保が多くなるのか調べれるようにしました。 以下のようなコードを worker に追加しました。

require 'objspace'

class MemoryProfiler
  INTERVAL = 5 * 60
  DISPLAY_COUNT = 20

  def self.start
    Thread.start do
      loop do
        memory_usages = {}

        ObjectSpace.trace_object_allocations_start
        sleep INTERVAL

        # メモリ割当を表示する箇所でも Object の allocation が行われ、それが表示されてしまうため
        # 一旦 trace を止める
        ObjectSpace.trace_object_allocations_stop

        ObjectSpace.each_object.each do |obj|
          # Ruby 本体など C 言語で実装されている部分については、どのファイルで allocation が行われたか取得できない
          # またそれらで大量に allocation されるので、いったん除外する
          if ObjectSpace.allocation_sourcefile(obj)
            key = "#{ObjectSpace.allocation_sourcefile(obj)}:#{ObjectSpace.allocation_sourceline(obj)}"
            memory_usages[key] ||= Hash.new { |hash, key| hash[key] = 0 }

            memory_usages[key][:count] += 1
            memory_usages[key][:bytes] += ObjectSpace.memsize_of(obj)
          end
        end

        memory_usages.sort_by { |_, v| -v[:bytes] }.take(DISPLAY_COUNT).each do |key, value|
          puts "#{key} retain #{value[:bytes]} bytes, #{value[:count]} allocations"
        end
      end
    end
  end
end

MemoryProfiler.start

# 以下は動作検証用のコード
@store = []
@hoge = []
loop do
  100.times { |i| @store << i.to_s }


  1000.times { |i| @hoge << i.to_s }
  sleep 1 * 60
end

これを動かすと、以下のように<ファイル名:行番号 メモリ確保したバイト数 オブジェクト生成回数> というフォーマットでログが出力されます。

/app/vendor/bundle/ruby/3.0.0/gems/timeout-0.3.2/lib/timeout.rb:101 retain 1049136 bytes, 1 allocations
/app/vendor/bundle/ruby/3.0.0/gems/json-2.6.3/lib/json/common.rb:216 retain 909308 bytes, 2159 allocations
/app/vendor/bundle/ruby/3.0.0/gems/rdkafka-0.10.0/lib/rdkafka/consumer.rb:424 retain 240414 bytes, 45 allocations
/app/vendor/bundle/ruby/3.0.0/gems/activesupport-7.0.4.3/lib/active_support/code_generator.rb:15 retain 123523 bytes, 2298 allocations
/app/vendor/bundle/ruby/3.0.0/gems/graphql-2.0.21/lib/graphql/schema/loader.rb:102 retain 76112 bytes, 67 allocations
/app/vendor/bundle/ruby/3.0.0/gems/graphql-2.0.21/lib/graphql/schema/introspection_system.rb:132 retain 48912 bytes, 36 allocations
    :

しばらく動かしたまま放置しておくと

/app/vendor/bundle/ruby/3.0.0/gems/graphql-2.0.21/lib/graphql/schema/loader.rb:102 retain 536192 bytes, 472 allocations

でどんどんメモリ確保したバイト数が増えていくことがわかりました。

この問題は使用しているライブラリである graphql-ruby の v2.0.24 でPerformance: Reduce memory usage when adding types to a schema #4533 とあり、どうやら修正されていそうだということだった。

graphql-ruby を v2.0.24 にバージョンアップしメモリ使用量は落ち着いた感じになりました。

追記 (2023/08/06)

笹田さんが作っている GitHub - ko1/allocation_tracer: Add ObjectSpace::AllocationTracer module. を使うと、さらに有益な情報が取得できそう。