td-agentのprofiling

末端ノードでnginxのlogをtailしてparseしてごにょごにょしているtd-agent 2.3.1のCPU使用率が50%ぐらいになっていたので、下記を使ってprofilingしてみた。

https://github.com/sonots/fluent-stackprof

なおtd-agentのモニタリングには
https://github.com/matsumana/td-agent_exporter
を使いました。

いままでちゃんとモニタリングしてなくてし始めて気づいたけどピーク時は普通にCPU使用率100%いってた。。。

profiling対象マシンでstackprofをinstall

/usr/sbin/td-agent-gem install stackprof

in_debug_agentを有効化するために下記をtd-agent.confに追加してtd-agentを再起動する。

<source>
  type debug_agent
  port 24230
</source>

そうするとtd-agent.logに下記のようなログが出る。

2017-06-07 13:16:08 +0900 [info]: listening dRuby uri="druby://0.0.0.0:24230" object="Engine"

同じマシンの別ruby環境でfluent-stackprofとstackprofをinstall

$ gem install fluent-stackprof
$ gem install stackprof

そしてprofilingスタート

$ fluent-stackprof start -h localhost -p 24230

1分後ぐらいにstop

$ fluent-stackprof stop -h localhost -p 24230 -o /tmp/fluent-stackprof.dump

fluent-stackprofがやっていることはdRuby経由でリモートのtd-agentに対してRPCしてます。
stackprofを実行している。なのでリモートのtd-agent側にもstackprofが必要
マジカルだ。

https://github.com/sonots/fluent-stackprof/blob/47b07f63b4fa4459d5b3b9a4fb174881d55b948e/lib/fluent/stackprof.rb#L75-L91

stackprofはrubyのprofilingツールです。
https://github.com/tmm1/stackprof

profilingしたデータを解析してみます。

$ stackprof /tmp/fluent-stackprof.dump --text
==================================
  Mode: cpu(1000)
  Samples: 40864 (0.06% miss rate)
  GC: 4616 (11.30%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
      8509  (20.8%)        8509  (20.8%)     block in URI::Parser#unescape
     13709  (33.5%)        5200  (12.7%)     URI::Parser#unescape
     34272  (83.9%)        4237  (10.4%)     Fluent::NewTailInput::TailWatcher::IOHandler#on_notify
      2500   (6.1%)        2500   (6.1%)     Fluent::ParserFilter#handle_parsed
      2426   (5.9%)        2426   (5.9%)     block in Fluent::TextParser::LabeledTSVParser#parse
     15309  (37.5%)        1399   (3.4%)     block (2 levels) in Fluent::URIDecoder#emit
     36174  (88.5%)        1282   (3.1%)     Coolio::Loop#run
      2279   (5.6%)        1051   (2.6%)     Fluent::TextParser::ValuesParser#values_map
      3544   (8.7%)         983   (2.4%)     Fluent::TextParser::JSONParser#parse
       909   (2.2%)         909   (2.2%)     block in Fluent::GeoIP#create_placeholder
       949   (2.3%)         859   (2.1%)     block in Fluent::GeoIP#add_geoip_field
       781   (1.9%)         781   (1.9%)     block in Fluent::GeoIP#geolocate
       584   (1.4%)         584   (1.4%)     Fluent::NewTailInput::TailWatcher::RotateHandler#on_notify
       582   (1.4%)         582   (1.4%)     Fluent::NewTailInput::FilePositionEntry#update_pos
       512   (1.3%)         512   (1.3%)     block in Fluent::EventStream#to_msgpack_stream
       410   (1.0%)         410   (1.0%)     Fluent::TextParser::ValuesParser#convert_value_to_nil
       386   (0.9%)         386   (0.9%)     Fluent::MultiEventStream#initialize
       546   (1.3%)         367   (0.9%)     Fluent::TextParser::TypeConverter#convert_type
      3914   (9.6%)         362   (0.9%)     block in Fluent::ParserFilter#filter_stream
       226   (0.6%)         226   (0.6%)     Fluent::MemoryBufferChunk#<<
     13910  (34.0%)         201   (0.5%)     URI::Escape#unescape
       200   (0.5%)         200   (0.5%)     block in Fluent::GeoIP#get_address
       179   (0.4%)         179   (0.4%)     block in <module:TypeConverter>
       179   (0.4%)         179   (0.4%)     Fluent::EngineClass::DummyMessagePackFactory#packer
       175   (0.4%)         175   (0.4%)     Fluent::URIDecoder#tag_mangle
       144   (0.4%)         144   (0.4%)     MonitorMixin#mon_enter
       681   (1.7%)         135   (0.3%)     block in Fluent::TextParser::ValuesParser#convert_field_type!
       133   (0.3%)         133   (0.3%)     rescue in Fluent::NewTailInput::TailWatcher::IOHandler#on_notify
       131   (0.3%)         131   (0.3%)     Fluent::MultiEventStream#add
       119   (0.3%)         119   (0.3%)     Fluent::EventRouter::MatchCache#get

Parser#unescapeが一番使われているようなのでこれを呼び出しているメソッドを探します。

$ stackprof /tmp/fluent-stackprof.dump --method 'Parser#unescape'
block in URI::Parser#unescape (/opt/td-agent/embedded/lib/ruby/2.1.0/uri/common.rb:331)
  samples:  8509 self (20.8%)  /   8509 total (20.8%)
  callers:
    8509  (  100.0%)  URI::Parser#unescape
  code:
 8509   (20.8%) /  8509  (20.8%)  |   331  |       str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(str.encoding)
                                  |   332  |     end
URI::Parser#unescape (/opt/td-agent/embedded/lib/ruby/2.1.0/uri/common.rb:330)
  samples:  5200 self (12.7%)  /   13709 total (33.5%)
  callers:
    13709  (  100.0%)  URI::Escape#unescape
  callees (8509 total):
    8509  (  100.0%)  block in URI::Parser#unescape
  code:
                                  |   330  |     def unescape(str, escaped = @regexp[:ESCAPED])
 13709   (33.5%) /  5200  (12.7%)  |   331  |       str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(str.encoding)
                                  |   332  |     end

Escape#unescapeだったのでさらにこれを呼んでいるメソッドを探します。

$ stackprof /tmp/fluent-stackprof.dump --method 'Escape#unescape'
URI::Escape#unescape (/opt/td-agent/embedded/lib/ruby/2.1.0/uri/common.rb:647)
  samples:   201 self (0.5%)  /   13910 total (34.0%)
  callers:
    13910  (  100.0%)  block (2 levels) in Fluent::URIDecoder#emit
  callees (13709 total):
    13709  (  100.0%)  URI::Parser#unescape
  code:
                                  |   647  |     def unescape(*arg)
  185    (0.5%) /   185   (0.5%)  |   648  |       warn "#{caller(1)[0]}: warning: URI.unescape is obsolete" if $VERBOSE
 13725   (33.6%) /    16   (0.0%)  |   649  |       DEFAULT_PARSER.unescape(*arg)
                                  |   650  |     end

URIDecoder#emitを呼んでいるメソッドを探します。

$ stackprof /tmp/fluent-stackprof.dump --method 'URIDecoder#emit'
block (2 levels) in Fluent::URIDecoder#emit (/opt/td-agent/embedded/lib/ruby/gems/2.1.0/gems/fluent-plugin-uri_decoder-0.2.0/lib/fluent/plugin/out_uri_decode.rb:61)
  samples:  1399 self (3.4%)  /   15309 total (37.5%)
  callers:
    15309  (  100.0%)  block in Fluent::URIDecoder#emit
  callees (13910 total):
    13910  (  100.0%)  URI::Escape#unescape
  code:
                                  |    61  |       @_key_names.each do |key_name|
 15309   (37.5%) /  1399   (3.4%)  |    62  |         record[key_name] = URI.decode(record[key_name] || '')
                                  |    63  |       end
Fluent::URIDecoder#emit (/opt/td-agent/embedded/lib/ruby/gems/2.1.0/gems/fluent-plugin-uri_decoder-0.2.0/lib/fluent/plugin/out_uri_decode.rb:53)
  samples:    10 self (0.0%)  /   24318 total (59.5%)
  callers:
    24318  (  100.0%)  Fluent::EventRouter#emit_stream
  callees (24308 total):
    24125  (   99.2%)  Fluent::MultiEventStream#each
     175  (    0.7%)  Fluent::URIDecoder#tag_mangle
       8  (    0.0%)  Fluent::NullOutputChain#next
  code:
                                  |    53  |   def emit(tag, es, chain)
  175    (0.4%)                   |    54  |     tag = tag_mangle(tag)
                                  |    55  |
    8    (0.0%) /     8   (0.0%)  |    56  |     @_key_names ||= @key_names.split(/,\s*/)
                                  |    57  |     @_key_names << @key_name if @key_name
                                  |    58  |     @_key_names.uniq!
                                  |    59  |
 24125   (59.0%)                   |    60  |     es.each do |time, record|
                                  |    61  |       @_key_names.each do |key_name|
                                  |    62  |         record[key_name] = URI.decode(record[key_name] || '')
                                  |    63  |       end
                                  |    64  |
                                  |    65  |       Fluent::Engine.emit(tag, time, record)
                                  |    66  |     end
                                  |    67  |
   10    (0.0%) /     2   (0.0%)  |    68  |     chain.next
                                  |    69  |   end
block in Fluent::URIDecoder#emit (/opt/td-agent/embedded/lib/ruby/gems/2.1.0/gems/fluent-plugin-uri_decoder-0.2.0/lib/fluent/plugin/out_uri_decode.rb:60)
  samples:     5 self (0.0%)  /   24112 total (59.0%)
  callers:
    24112  (  100.0%)  block in Fluent::MultiEventStream#each
  callees (24107 total):
    15309  (   63.5%)  block (2 levels) in Fluent::URIDecoder#emit
    8798  (   36.5%)  Fluent::EngineClass#emit
  code:
                                  |    60  |     es.each do |time, record|
 15309   (37.5%)                   |    61  |       @_key_names.each do |key_name|
                                  |    62  |         record[key_name] = URI.decode(record[key_name] || '')
                                  |    63  |       end
                                  |    64  |
 8803   (21.5%) /     5   (0.0%)  |    65  |       Fluent::Engine.emit(tag, time, record)
                                  |    66  |     end

fluent-plugin-uri_decoderのurl decode処理が重いようです。

諸事情があってurl decodeの処理が必要なのですが、どうすっかなあ。
rubyを最新にすれば早くなったりするかな。