Rubyで拡張ライブラリーを使っているとクラッシュすることがあります。自分が開発している拡張ライブラリーならどうにかして直したいものです。そのときに役立つのがGDBなどのデバッガーです。Cレベルのより詳細な情報を取得できるため、問題の特定に役立ちます。しかし、次のようにデバッガー上でクラッシュさせることが難しいことがあります。
- GDB上で動かすとクラッシュしない
消極的な理由ですが次のようなケースもあります。
- なかなかクラッシュしないので、ずっとGDB上で動かしているわけにもいかない
- SIGPIPEなどを捕まえて止まって欲しくない。「handle SIGPIPE nostop」などをするのが面倒。
- クラッシュしたら自動で起動しなおしてサービスは継続して欲しい
- GDB上でいろいろ作業しているとポートを専有したままで起動できない。
そのようなときに便利なgemがsegv-handler-gdbです。
似たようなツール
便利と書きましたが、便利なケースは限られています。多くの場合は次の似たようなツールの方が便利です。
- sigdump
- Rubyレベルの詳細な情報をダンプするツール
- segv-handler-gdbの方が向いているケース:Cレベルの情報が欲しい場合
- gdbruby.rb
では、どのようなときに便利なのか。このgemを作った背景を説明するとどのようなときに便利なのかわかるでしょう。
背景
segv-handler-gdbはRroongaがクラッシュする問題を調査するために作ったツールです。既存のツールでは問題を調査するには不便だったのです。それでは、何が不便だったのかを説明します。
RroongaはGroongaというC/C++で書かれた全文検索エンジンを使用しています。Rroongaレベルでクラッシュすることもあれば、Groongaレベルでクラッシュすることもあります。どちらかというとGroongaレベルでクラッシュすることが多いです。クラッシュした問題の原因を調べる場合はCレベルのバックトレースがあると役立ちます。
既存のツールでもCレベルのバックトレースを出力する機能がありました。例えば、RubyはクラッシュするとCレベルのバックトレースを出力します。
% ruby -e 'sleep' &
[1] 12621
% kill -SEGV 12621
-e:1: [BUG] Segmentation fault
ruby 2.0.0p299 (2013-08-29) [x86_64-linux-gnu]
...
-- C level backtrace information -------------------------------------------
/usr/lib/x86_64-linux-gnu/libruby-2.0.so.2.0(+0x176a5b) [0x7fa26684ba5b]
/usr/lib/x86_64-linux-gnu/libruby-2.0.so.2.0(+0x64aca) [0x7fa266739aca] vfscanf.c:653
/usr/lib/x86_64-linux-gnu/libruby-2.0.so.2.0(rb_bug+0xb3) [0x7fa26673a1d3] vfscanf.c:651
...
rb_bug
のように関数名も入っていますし、ファイル名や行数も入っています。これはbacktrace(3)を使った出力で、よく見かけるフォーマットです2。Groongaもコマンドとして使った場合は同じように出力します3。
たしかにCレベルのバックトレースは手に入っています。しかしこれだと足りないのです。引数の情報があるともっとうれしいのです。引数の情報もあると、「引数にNULL
が渡ってしまっているからクラッシュしたんだな」などということがわかります。
デバッグシンボル付きでビルドしたバイナリーなら、GDBを使えば引数の情報も取得できます4。
% groonga -d
14129
% gdb --pid 14129 --batch --eval-command 'backtrace'
warning: Could not load shared library symbols for linux-vdso.so.1.
Do you need "set solib-search-path" or "set sysroot"?
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f782398dfb3 in __epoll_wait_nocancel () at ../sysdeps/unix/syscall-template.S:81
81 ../sysdeps/unix/syscall-template.S: そのようなファイルやディレクトリはありません.
#0 0x00007f782398dfb3 in __epoll_wait_nocancel () at ../sysdeps/unix/syscall-template.S:81
#1 0x00007f7824f00fe6 in grn_com_event_poll (ctx=0x7fff86e7fc40, ev=0x7fff86e7f330, timeout=1000) at com.c:578
#2 0x0000000000405393 in run_server_loop (ctx=0x7fff86e7fc40, ev=0x7fff86e7f330) at groonga.c:534
#3 0x0000000000405a5e in run_server (ctx=0x7fff86e7fc40, db=0x18a8b90, ev=0x7fff86e7f330, dispatcher=0x40a42b <g_dispatcher>, handler=0x40aa30 <g_handler>) at groonga.c:595
#4 0x0000000000405bab in start_service (ctx=0x7fff86e7fc40, db_path=0x0, dispatcher=0x40a42b <g_dispatcher>, handler=0x40aa30 <g_handler>) at groonga.c:625
#5 0x000000000040ae04 in g_server (path=0x0) at groonga.c:1597
#6 0x000000000040c913 in main (argc=2, argv=0x7fff86e80038) at groonga.c:2508
「grn_com_event_poll (ctx=0x7fff86e7fc40, ev=0x7fff86e7f330, timeout=1000)
」というように引数の情報も入っています。
「backtrace」だけでなく、「backtrace full」にするとローカル変数の情報も入ります。
% gdb --pid 14129 --batch --eval-command 'backtrace full'
...
#1 0x00007f7824f00fe6 in grn_com_event_poll (ctx=0x7fff86e7fc40, ev=0x7fff86e7f330, timeout=1000) at com.c:578
nevents = 0
com = 0x0
ep = 0x7f7825463010
__FUNCTION__ = "grn_com_event_poll"
#2 0x0000000000405393 in run_server_loop (ctx=0x7fff86e7fc40, ev=0x7fff86e7f330) at groonga.c:534
No locals.
...
スレッドを使っている場合は、さらに「thread apply all backtrace full」とします。
デバッグの役にたちそうですね!
要件
backtrace(3)ではなくGDBを使えば引数の情報などより詳細な情報を取得できます。それならクラッシュした時のcoreをGDBで開いても問題ありません。
しかし、Rroongaの場合はcoreを出力することが現実的でない場合が多いのです。Groongaはデータベースの内容をメモリー上にマップして使います。coreはメモリーの内容を含むので、Groongaがデータベースの内容すべてをメモリー上にマップしている場合はデータベースと同じくらいの大きさのcoreができます。データベースが数10GBあればcoreもそのくらい大きくなります。そのサイズのcoreを出力すると大量のディスクI/Oが発生し、システムにも影響がでます。
そのため、Rroongaで便利に使うためにはcoreを出力せずにクラッシュした瞬間のバックトレースを取得する方法が必要だったのです。
まとめ
Rroongaがクラッシュしたときに問題の調査が便利になるgem、segv-handler-gdbを紹介しました。多くの場合はsegv-handler-gdbではなく他のツールの方が便利でしょう。