2013年9月29日

クラウド時代の core の解析法

クラウド・コンピューティングというと次の2つを主眼とした技術になります。

  • リソースを共有、適切に分散することで、ハードウェア購入・維持費を下げよう
  • 瞬間的に大量の計算機リソースを借りる事で、必要になる計算機能力がバースト的に増大する場合に備えよう

しかし、現在の仮想化技術には、複数の物理マシンを仮想的に1台のマシンに編み上げる方法は存在しません。この事は次の単純な事実をもたらします:

物理的に保持しているマシンの台数より
仮想的に保持しているマシンの台数のほうが多い

一方で、ハードウェアをいくら仮想化しようが、その上で動作するアプリケーションの数は減りません。使うアプリケーションの種類は減りませんし、分散コンピューティングとかですと、複数のマシンで同じソフトウェアを動作させて協調分散して仕事をさせるのですから、むしろプロセス数とかは増えます。


それどころか協調分散システムでは、どこかのプログラムがヘコッとコケるのは当たり前になっているので、

こけても
即座に
別のプロセスや
別の仮想マシンを
立ち上げればええやん

という発想がまかり通っています。つまりソフトウェアの品質は低くなっている。

仮想マシンの台数は増え、ソフトウェアの動作プロセス数も増え、なおかつ品質が悪くなると何が起こるか…

システムの面倒を見る側が
見なくちゃいけない core ファイルの数が
爆発的に増大する

コアファイルを見て、適切なプログラマに
これはテメーの間違いだ!!
直しやがれ
と押し付ける交通整理役の人が大変になるってことです。

先日も、とあるシステムのコアの数を調べたら 6000 を超えてまして…総量を計算したら 3Tbyteを越えてまして…ちょっとどうしようかと思案中。

こういう作業を全部手作業だけで1つ1つ処理していくのは大変すぎます。少なくとも 深さ優先(1つづつコアを分析していく) でやったのでは死んでしまいます。ここは 幅優先(表面を撫でるように簡単なチェックをして、共通な物同士をまとめ、不要なものを破棄しながら分析する) 戦略が必要です。


というわけで、私が普段使っているやり方を少しここに書いてみようと思います。

対象は基本的に Linux になりますが、BSD などの Unix 系 OS なら、ある程度は共通していると思います。

デバッガは gdb を前提とします。多分他のデバッガでもできるんでしょうが、バッチ処理的に実施する上では gdb は便利なので。

最後に。ソースコードが無いと仮定します
あなたの仕事はこけているコアファイルを適切なプログラマのもとに送り届けることだとします。しかもあなたは開発部隊に所属していないので、ソースコードは読めない。
そんな条件下での分析と、プログラマが出鱈目八百を答えてきた時に反論できる程度の知識を予め引きずり出す事が主な仕事とします。

まぁ、実行バイナリがあって、コアファイルがあるんだから、じっくりと時間を掛けて、本気で分析すればそりゃ問題点は判るでしょうが、どうせ直せませんからね。



コアファイルの命名規則を調べる

unix 系 OS では伝統的に、デフォルトのコアファイル名は core です。
厳密に言うと、そのプログラムにとっての「カレントディレクトリ」に core というファイル名で作られます。

すぐ判ることですが、cron で動かしたとか、daemon であるとかのプロセスのカレントディレクトリ…それももうすでにコケて core ファイルを吐いた後のプロセスのカレントディレクトリを探すのは容易なことではありません。
また、同じディレクトリで core というファイルをどんどん上書きされるのも困りますし、逆に色んなディレクトリに core ファイルを撒き散らかされるのも運用上ありがたいことではありません。
だいたい、「core」だけじゃ、何の core ファイルなんだか判らないし。

という訳で、最近の unix 系 OS は何らかの方法で、coreファイル名を指定する方法があります。ファイル名を指定する方法があるという事は、上手に使えば core ファイルを保存する絶対パスを指定できる、と言うことです。これがちゃんと設定してあれば、core ファイルがファイルシステム中に撒き散らされる、なんてことはありません。

Linuxの場合、2.6 か 2.4.21以降であれば、
    /proc/sys/kernel/core_pattern
というファイルに、パス名パターンを指定することで core ファイル名を指定できます。これは
    /etc/sysctl.conf
か、そんな感じのファイル名/ディレクトリ名の下にある設定ファイルで設定できます。ココらへんは distribution/version 依存なので、各自調べて下さい。


ちなみに私の見ているシステムでは
     /var/cores/%e.%p.%t.%s
になっていました。これを解釈すると:

  1. 全てのコアファイルは /var/cores/ ディレクトリ下にあります
  2. ファイル名の最初は %e つまり「プログラムファイルの名前」をパス名無しで…になります
  3. . で分けた後、%p つまりプロセスIDが続きます。
    これは同時刻に同じプログラムが連続してこけた場合でも区別できるようにするための工夫ですね
  4. . を挟んで、次が %t …つまり epoch time が来ます。これはこの core ファイルが作られた(作る要因が発生した)時刻を、1970年1月1日 00:00:00 (UTC) からの時刻で表したものになります
  5. . を挟んで、最後に %s …つまりこの core file の発生要因となったシグナル番号が来ます。


見ての通り、このファイル名をちゃんと設定するだけで、色々と面倒がスキップできることが判ります。特に最後の %s はとても有効です。

もし、この辺りを自由にできる権限があるならば、是非多くの情報がファイル名だけからでも判るように設定することを強くおすすめします。



命名規則に従って、コアファイルの表面的な情報を調べ、分類する

さて、以下の議論では /proc/sys/kernel/core_pattern が /var/cores/%e.%p.%t.%s であると仮定します。

ここで役に立ちそうなのは %t と %s です。%e は最初から human readable ですし、%p は単なる番号なので readable もへったくれもありません。

そこで %t と %s をもっと human readable にしてあげましょう。なお、以下の説明では
「もっと簡単な方法がある」
「もっと判りやすい方法がある」
どちらも受け付けません ( w )。


簡単な方から。

%s は 番号⇒文字列 変換配列があればよいでしょう。bash は配列が使えますので、それを使うと便利。LinuxのSIGNALのマニュアルページに書いてある表の真ん中を元に作ったのがこれです。

declare -a SIGNAL_NAME
SIGNAL_NAME[1]='SIGHUP'
SIGNAL_NAME[2]='SIGINT'
SIGNAL_NAME[3]='SIGQUIT'
SIGNAL_NAME[4]='SIGILL'
SIGNAL_NAME[5]='SIGTRAP'
SIGNAL_NAME[6]='SIGABRT'
SIGNAL_NAME[7]='SIGBUS'
SIGNAL_NAME[8]='SIGFPE'
SIGNAL_NAME[9]='SIGKILL'
SIGNAL_NAME[10]='SIGUSR1'
SIGNAL_NAME[11]='SIGSEGV'
SIGNAL_NAME[12]='SIGUSR2'
SIGNAL_NAME[13]='SIGPIPE'
SIGNAL_NAME[14]='SIGALRM'
SIGNAL_NAME[15]='SIGTERM'
SIGNAL_NAME[16]='SIGSTKFLT'
SIGNAL_NAME[17]='SIGCHLD'
SIGNAL_NAME[18]='SIGCONT'
SIGNAL_NAME[19]='SIGSTOP'
SIGNAL_NAME[20]='SIGTSTP'
SIGNAL_NAME[21]='SIGTTIN'
SIGNAL_NAME[22]='SIGTTOU'
SIGNAL_NAME[23]='SIGURG'
SIGNAL_NAME[24]='SIGXCPU'
SIGNAL_NAME[25]='SIGXFSZ'
SIGNAL_NAME[26]='SIGVTALRM'
SIGNAL_NAME[27]='SIGPROF'
SIGNAL_NAME[28]='SIGWINCH'
SIGNAL_NAME[29]='SIGIO'
SIGNAL_NAME[30]='SIGPWR'
SIGNAL_NAME[31]='SIGSYS'
signum=$( echo $filename | sed -r -e 's%.+\.[[:digit:]]+\.[[:digit:]]+\.([[:digit:]]+)$%\1%g' )
signame=${SIGNAL_NAME[$signum]}


$filename という変数に元のファイル名が入っているとします。フォーマットは最初に指定した通り。で、最後の数値の部分を取り出して、そいつで SIGNAL_NAME 配列を引っ張ると $signame にSIGNAL名が入ります。


%t はもう少し面倒と言うべきか簡単というべきか…

epoch=$( echo $filename | sed -r -e 's%.+\.[[:digit:]]+\.([[:digit:]]+)\.[[:digit:]]+$%\1%g' ) gendate=$(echo $epoch | gawk '{x = strftime("%F-%H-%M-%S",$0); print x}' )

$epoch 変数に %t の部分を取り出すのは、sed による強引な書き換えで実装しています。で、gawk の strftime() を使って、無理やり 年-月-日-時-分-秒 に置き換えます。これを $gendate 変数に入れておきます。


ここまできたら、後は簡単。

newname=$(echo $filename | sed -r -e "s%^(.+\.[[:digit:]]+\.)[[:digit:]]+\.[[:digit:]]+\$%\1$gendate.$signame%g")

これでファイル名を作り出し、必要に応じて新しいファイル名の置き場所のディレクトリ部分とかを書き換えたら

ln $filename $newname

これで、$newname で、$filename の中身を参照できるようになりました。ハードリンクにしておけば、ディスクを消費する量も少なく、元のファイル名を壊すこともなく、新しい名前でコアファイルを参照することができます。

そうやって出来上がったファイル名がこんな感じ…

# ls
httpd.worker.811.2013-08-18-02-41-24.SIGSEGV
js.10354.2013-07-27-16-03-37.SIGSEGV
js.10536.2013-07-17-20-28-30.SIGSEGV
js.1081.2013-07-23-20-08-01.SIGSEGV
js.10878.2013-06-25-16-39-17.SIGSEGV
js.11114.2013-06-24-18-30-48.SIGSEGV
js.11476.2013-07-12-19-13-40.SIGSEGV
js.11863.2013-06-01-17-29-44.SIGSEGV
js.11950.2013-08-05-19-22-43.SIGSEGV
js.12204.2013-07-26-17-21-56.SIGSEGV
             :
mauiss.6915.2013-06-06-04-30-52.SIGABRT
mauiss.719.2013-08-05-06-15-14.SIGABRT
mauiss.794.2013-05-11-15-17-30.SIGABRT
mauiss.9527.2013-06-08-10-05-47.SIGABRT
mauiss.9578.2013-05-11-19-39-35.SIGABRT
mauiss.9914.2013-07-05-11-00-33.SIGABRT
mds_10401.20200.2013-06-01-22-28-45.SIGABRT
mongrel_rails.16093.2012-09-17-18-34-17.SIGABRT
mongrel_rails.16528.2013-02-14-21-05-59.SIGABRT

これだけでもいくつかのことが判ります。

  1. mauiss, mds_10401, mongrel_rails の3種類はやたらと SIGABRT で死んでいる。

    SIGABRT はそもそもプログラマが assert() 構文で
    「こんなことになったら終わりや…コア吐いて死によし」
    と組み込んでおいた、まさにその条件にヒットしたので core 吐いて死んだ、と言うことです。これらはまさにソースコードの assert() 文の中身を見ない限り、何が悪いのか判らない、と言う事。これらに関して深堀りはするだけ無駄です。
    つーても一応この次の一手分まで実行しておきますが。
  2. http.worker と js というプログラムは SIGSEGVで死んでいるので、多分 bad pointer 系の何かだろう。

    ということは、stack の状態を調べて、同じ call tree を辿って、同じ場所でコケてる場合は、同じような理由で死んでいる可能性が高いってことです。
  3. PID邪魔かも…

    いつのコアファイルなのかを知る上で、PID順に並んでいるのはとてつもなく邪魔である事がよく判ります。PIDを切り出して、一番最後に回せばよかったよ…
まぁ、とりあえず反省点は見つけたので、めでたく次の一手へと進みます。



どんなコアファイルでも最初に調べるべき内容を吐き出させる

さて次は。

いよいよgdbにコアファイルを分析させるのですが。

当たり前ですが、コアファイルの分析には、そのコアファイルを吐いたプログラムの実行ファイルが必要になります。いまどきのOSですとそのような実行ファイルは動作するのに動的ライブラリを必要としますが、動的ライブラリのバージョンもぴったり一致している必要があります。

コアファイルは、正確には「stackとheap」のメモリイメージをファイルに吐き出したものです。ですので、実行ファイルとかロードした動的ライブラリのイメージは含まれていません(a.out形式ならあり得たかもしれませんが)。こいつらを適切にメモリ上に展開してやらないと、

「0x00238d93fc で SIGSEGVを起こしました」
「そんな所に実行プログラムあらへんわ」

とか

「0x00238d93fc で SIGSEGVを起こしました」
「そこにあるのは~ おぉ malloc() やな(古いライブラリでは free でした)」

とか、間違った解析になりかねません。ですので、コア解析でベストなのは、実行環境そのもので実施する事なのですが…それが無理なら、せめて同じインストールセットの環境を整える必要があります。

判ると思いますが、これが実運用環境をなかなか変更するべきではない本当の理由です。逆にこの手の分析をしないのなら、どんどんバージョンアップをするべきなのですよ!! それなのに日本人が運用すると、意味もなくゴネるばかりでブツブツブツブツ…はっ、閑話休題、閑話休題。

とにかく、実行ファイルとかライブラリが全部あって、それもコアファイルを作った環境と全く同じ環境が作れるとしましょう。

現実問題として core file からこれらの環境を推測するのは多分不可能ではないとは思うのですが、1つ1つやってたらたまったものではないので、今回はパスします。
さて、実行ファイルの名前は、コアファイルの先頭にあります。先の例なら mauiss とか js とか言うやつですよね。こいつを見つけ出します。同じ名前のものは、基本的に同じ場所にあると考えてよいでしょう。

which httpd.worker
which mauiss
which js
とかやればきっと出てくる。出てこない場合は泣きながら探す。もし沢山コアファイルがあるなら、きっとその涙はダイヤモンド。1つしか無かったらすねて諦めて寝るのとどっちが効率が良いか、一考の余地がありますが。




実行ファイルが見つかって、コアファイルもある、となればあとは gdb にお任せ…とはまいりません。「何をするのか」を教えてあげなくちゃいけない。

幸い、gdb には「バッチモード」というのがあります。事前に何をして欲しいかを指定しておけば、OK…なのですが、このgdbのバッチ、2つ問題があります。


  1. 分岐もループも書けない
    厳密に言うと「私は知らない」だけで、もしかするとあるのかもしれませんが、取得した情報をもとに条件分岐するとか、繰り返しループを書くとか、そういう方法が、無いのか、私が無知すぎるだけなのか…
    とにかくきれいな方法が提供されていません。
  2. 途中でエラーを起こすとそこから先を実行してくれない。
    例えば SIGSEGV で倒れた理由が NULL pointer を実行アドレスとした call 命令の結果だったとしましょう。stack の frame 0 はこの null pointer を実行するためのアドレスになっています。

    ここで「frame0の Instruction Pointer が指している辺りを disassemble して」とお願いすると、通常モードなら gdb は
        No function contains specific address.
    とか何とか言ってエラーになった後、プロンプトを出すのですが、バッチモードの場合は、そのまま gdb を終了してしまうのです。

    な・ん・の・た・め・の・バッチモードなのやら。

ですので、gdb のバッチコマンドは、なるべく失敗しそうな命令を別々のバッチファイルにして、それらをシェルなどで駆動する…あるいは gdb に対するフロントエンドを作ってやって、それを使ってコマンドをくべてやる必要があるのです。

(以下、未完)