<< >>

2008-03-19 [長年日記]

[DTrace] Ruby DTrace

DTrace パッチ済みの Ruby に対しては、以下のプローブが DTrace から利用可能になる。

Ruby DTrace が提供するプローブ

プローブ名実行タイミングarg0arg1arg2arg3
function-entryメソッド呼出開始クラス名メソッド名ソースファイル行番号
function-returnメソッド呼出終了クラス名メソッド名ソースファイル行番号
raise例外発生時クラス名ソースファイル名行番号-
rescuerescue実行時ソースファイル名行番号--
lineコードの1行毎ソースファイル名行番号--
gc-beginGCの開始----
gc-endGCの終了----
object-create-startオブジェクト生成の開始Ruby型名ソースファイル名行番号-
object-create-doneオブジェクト生成の終了Ruby型名ソースファイル名行番号-
object-freeメモリ解放時Ruby型名---
ruby-probeRubyコード上での明示任意の文字列任意の文字列--

function-{entry, return}, gc-{begin, end}, object-create-{start, done}, (fire)-{start, end}という名前の一貫性のなさが辛いので、この表はもう印刷して張っておきたい。

DTraceに関する情報

  1. [SUN] 概要 http://jp.sun.com/products/software/solaris/10/dtracehowto.html
  2. [SUN] 仕様 http://docs.sun.com/app/docs/doc/819-0395
  3. [SUN] 使用例(※1) http://sdc.sun.co.jp/solaris/solaris10/dtrace/guide.html
  4. Ruby DTrace 公式 https://dtrace.joyent.com/projects/ruby-dtrace/wiki/Ruby+DTrace
  5. Ruby DTrace 使用例 http://theexciter.com/articles/a-quick-stroll-through-dtrace
  6. [PDF] Ruby DTrace かんたん入門 (高井乙!) http://recompile.net/uploaded_files/ruby-dtrace.pdf

(※1 要SDC会員アカウント (登録無料))

特別なプローブ(ruby-probe)

Ruby DTrace が提供する殆どのプローブは(DTraced) Ruby によって自動的に呼び出されるが、唯一「ruby-probe」だけはユーザから明示的に呼び出される。具体的には、Ruby コード上での Tracer.fire メソッド(※1)実行によって ruby-probe が発火される。同プローブには最大2個の引数(任意の文字列)を渡すことができ、fireメソッドにブロックが渡された場合は、around メソッドとしてプローブが実行される。

fireメソッドの挙動

コード実行されるプローブarg0arg1
Tracer.fire('name')ruby-probe'name'-
Tracer.fire('age', 14)(TypeErrorが発生)
Tracer.fire('age', '14')ruby-probe'age''14'
Tracer.fire('age', '14', 'ok')(ArgumentErrorが発生)
Tracer.fire('query'){...}ruby-probe'query-start'-
(ブロック内がyield)
ruby-probe'query-end'-

(※1 Mac だと Tracer でなく DTracer として定義されている)

例1: 例外発生の確認

プローブ名実行タイミングarg0arg1arg2arg3
raise例外発生時クラス名ソースファイル名行番号-

raise.d

ruby*:::raise
/pid == $1/
{
  class  = copyinstr(arg0);
  source = copyinstr(arg1);
  printf("%s (%s:%d)", class, source, arg2);
}

irb(main):001:0> $$
=> 1985
irb(main):002:0> aho
NameError: undefined local variable or method `aho' for main:Object
        from (irb):2
        from :0

# dtrace -s raise.d 1985
dtrace: script 'raise.d' matched 2 probes
CPU     ID                    FUNCTION:NAME
  1  41395                 rb_longjmp:raise NameError ((irb):2)
  1  41395                 rb_longjmp:raise NameError (/usr/local/lib/ruby/1.8/irb/workspace.rb:81)

これは raise をプローブするだけの凄く単純な例だが、

  • 事前に何も準備する必要がなく
  • システムの不調を感じたときに
  • その pid を指定するだけで
  • エラーの発生箇所を外部から特定できる

という点で、既に十分実用的である。特に、例外を中途半端に rescue してログに「エラー」とだけ表示している場合のデバッグは非常にやっかいだが、そういう場合に、そのコードを修正することなく、それどころかアプリの停止すら必要なく、その発生箇所情報を取得できるのは非常に心強い。さらに、「pid の絞込み」部分の述語をなくすだけで、そのサーバの Ruby 監視ツールに早変わりする。(※1)

(※1 例外の発生を前提にしたシステムもありえるのでアプリを絞らずに常時使用するとシステム負荷は高くなりえるので注意)

例2: メソッド別実行時間

プローブ名実行タイミングarg0arg1arg2arg3
function-entryメソッド呼出開始クラス名メソッド名ソースファイル行番号
function-returnメソッド呼出終了クラス名メソッド名ソースファイル行番号

method.d

#pragma D option quiet

ruby$1:::function-entry
{
        self->ts = timestamp;
}

ruby$1:::function-return
/self->ts/
{
        @[copyinstr(arg0), copyinstr(arg1)] = sum(timestamp - self->ts);
        self->ts = 0;
}

END
{
        printf("%-15s %-20s %s\n", "Class", "Method", "Time");
        printa("%-15s %-20s %10@d\n", @);
}

# dtrace -s method.d 2035
...
Array           shift                   1079362
String          gsub                    1198380
String          to_s                    1457360
Hash            []=                     1481531
Module          constants               2108975
Hash            key?                    5629032
TCPServer       accept                352271736
Object          sleep                2019952588

実行結果は、Scaffold(Rails) の list アクションを実行したときの、各メソッド毎の実行時間(nsec)である。これにより、高速化を目指すならば Hash#key? 付近に手を入れることになるが、TCP接続の前では誤差になることもわかる。

例3: ActiveRecordのクエリ実行時間の計測

プローブ名実行タイミングarg0arg1arg2arg3
ruby-probeRubyコード上での明示任意の文字列任意の文字列--

config/initializers/dtrace.rb

module DTracer
  module QueryProbe
    def self.included(base)
      base.class_eval <<-EOS
        alias_method :untraced_execute, :execute
        def traced_execute(sql, name=nil)
          Tracer.fire('query', sql) { untraced_execute(sql, name) }
        end
        alias_method :execute, :traced_execute
      EOS
    end
  end
end

ActiveRecord::Base.connection.class.class_eval do
  include DTracer::QueryProbe
end

(参考: http://dev.joyent.com/projects/ruby-dtrace/browse/ruby/examples/rails/dtrace_plugin )

ar.d

ruby*:::ruby-probe
/copyinstr(arg0) == "query-start"/
{
        self->qts = timestamp;
}

ruby*:::ruby-probe
/copyinstr(arg0) == "query-end" && self->qts/
{
        query = copyinstr(arg1);
        @[query] = avg(timestamp - self->qts);
        self->qts = 0;
}

% script/console
>> Kid.count
>> Kid.find(:all)

# dtrace -s ar.d 
  SELECT * FROM kids                                           413311
  SELECT count(*) AS count_all FROM kids                       462813

Rails 全体のパフォーマンスを計測するツールを作りたい。

本日のツッコミ(全1件) [ツッコミを入れる]
_ nksk (2008-03-21 00:33)

GJ