第16回「 kernel-debug ノススメ」


Linux カーネルの中には、カーネル内でのデッドロックやメモリリークなど、カーネル自身のデバッグを行うための様々な機能が含まれています。しかし、 kernel パッケージに含まれている普通のカーネルでは大部分が無効化されているため、実際に運用を開始してトラブルに遭遇するまで、カーネル自身のバグに気が付かないケースが多くあります。 RHEL の場合、多くのデバッグ機能を有効にした kernel-debug というパッケージも提供されています。 kernel-debug パッケージに含まれているデバッグ用カーネルを用いてシステムの構築時に試験を行うことで、運用開始後に遭遇する可能性のあるバグの一部を事前に見つけて対処を行うことができます。また、運用開始後でも、原因の究明に役立つことがあります。

kernel-debug パッケージのインストールは、以下に示すように yum コマンドを用いて簡単に行えます。注意すべき点としては、ソースコードが公開されていないカーネルモジュールを使用しているシステムの場合、デバッグ用カーネル向けにコンパイルされたカーネルモジュールを入手できないために、当該カーネルモジュールについての試験を行えないことが挙げられます。また、様々なデバッグ機能が有効になっていることにより、性能面でのオーバーヘッドが増えることと、デバッグ機能自身の制約やバグに遭遇する可能性があることが挙げられます。それ以外は、デバッグ用カーネルは普通のカーネルと同様に使うことができます。

  # yum install kernel-debug

RHEL 5 以降のデバッグ用カーネルで有効になっている機能の1つに、 lockdep( CONFIG_PROVE_LOCKING=y )があります。これは、 GPL 互換ではないカーネルモジュールを使用している場合などを除き、デッドロックが発生する可能性のあるロックの獲得要求が発生すると、実際にデッドロックが発生する前に警告メッセージを出力することができます。もしも lockdep の警告メッセージが出力された場合、未修正のバグに遭遇している可能性がありますので、有識者に照会することを推奨します。また、 /proc/sys/kernel/hung_task_timeout_secs により出力される警告メッセージの中にも、当該プロセスが獲得しているロックの一覧が含まれている (*1)ので、ロックの獲得待ちに関する問題が発生している可能性を見つける際に有効です。同様に、 SysRq (*2) で使用できるコマンド一覧 (*3) の中に、カーネル内で獲得済みの全てのロックの情報を列挙する show-all-locks というコマンド( SysRq-d )が追加されているので、システムの応答が無い場合などにスレッドの動作状況( SysRq-t )と一緒にロックの獲得状況を確認することができます。

なお、 lockdep は実測に基づくテスト手法であるため、 lockdep が有効な状態で発生したロックの獲得要求しかチェックできません。 lockdep で不具合を発見するためには、定型的/定常的なワークロードを掛けるだけではなく、普段は発生しないような挙動(例:大量の通信トラフィックが発生している最中にネットワークケーブルの抜き差しを行う)を積極的に試すことで、なるべく多くの動作パターンが実行されるようにすることが大切です。 Linux カーネル開発者の間では、テスト用の環境を用意し、 trinity というファジングツールを実行するというケースが多いようです。

RHEL 6 以降のデバッグ用カーネルで有効になっている機能の1つに、 kmemleak( CONFIG_DEBUG_KMEMLEAK=y )があります。これは、カーネルのコマンドラインオプションに kmemleak=on という指定をすることで、カーネル内でのメモリリークを検出することができる (*4) ようになります。使い方は、 /sys/kernel/debug/ にdebugfs をマウントして、 /sys/kernel/debug/kmemleak に scan という文字を書き込み、その後 /sys/kernel/debug/kmemleak の内容を読み出すだけです。もしも kmemleak のメッセージが出力された場合、未修正のバグに遭遇している可能性がありますので、念のため有識者に照会することを推奨します。

  # mount -t debugfs debugfs /sys/kernel/debug/
  # echo scan > /sys/kernel/debug/kmemleak
  # cat /sys/kernel/debug/kmemleak

実際のシステムでの試験内容はシステムにより異なるため、ここでは説明できません。でも、経験則からは、トラブルの多くは、定型的/定常的なワークロードだけを掛けていたつもりが、想定外の過負荷状態を発生させてしまってメモリが枯渇しそうになった際に発生しています。そのため、システムの運用開始後に遭遇する可能性のあるトラブルを事前に見つけて対処するという観点では、故意に過負荷状態を発生させる試験を行うことも有効と考えます。そこで、ここでは過負荷状態を発生させる簡単な手順について説明します。

過負荷状態を試験するには非力なマシンのほうが適しています。私は Windowsデスクトップマシン上で VMware Player を使っており、「 4 CPU / 2GB RAM / swap無し」という仮想ハードウェア構成で Linux を動かすことが多いです。また、問題が発生した場合にカーネルメッセージがホスト側の serial.txt というファイルにも出力されるようにするために、 console=ttyS0,115200n8 console=tty0 というカーネルコマンドラインオプションを追加しています。

過負荷状態を発生させる基本は、同じ処理をたくさん同時に実行させることです。簡単な例では、以下のようになるでしょう。

  $ for i in `seq 1 10`; do dd if=/dev/zero of=/tmp/file bs=1048576 count=100 & done

上記の例では、システムの応答が遅くなるとしても、応答が無くなることは無いものと思います。しかし、以下のように負荷を掛けすぎると、メモリが枯渇してシステムが無応答状態になってしまうかもしれません。

  $ for i in `seq 1 100`; do dd if=/dev/zero of=/tmp/file bs=104857600 count=100 & done

上述した構成で私が行った実験によると、 RHEL 7 のデフォルトである xfs ファイルシステムは、 ext 系ファイルシステムと比較して、 OOM killer が発動する直前または発動した際にシステムが無応答状態になってしまうケースが発生しやすいという結果が出ています。 OOM killer がメモリ枯渇状態を解消し損ねるケースは容易に発生する(*5) ので、実際の運用では OOM killer が発動するような過負荷状態を発生させないようにご注意ください。

(*1) 以下に示します。

----------
INFO: task dd:3560 blocked for more than 10 seconds.
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
dd              D ffff880073204f20  5096  3560   3478 0x00000080
 ffff880071f0dc78 0000000000000046 00000000001d4640 ffff880071f0dfd8
 ffff880071f0dfd8 00000000001d4640 ffff880074048000 ffff880073204f20
 ffff8800701c9080 ffff88007192d0b8 ffff88007192d0c0 0000000000000246
Call Trace:
 [] schedule_preempt_disabled+0x39/0x90
 [] mutex_lock_nested+0x1bd/0x520
 [] ? xfs_file_buffered_aio_write+0x71/0x2a0 [xfs]
 [] ? xfs_file_buffered_aio_write+0x71/0x2a0 [xfs]
 [] xfs_file_buffered_aio_write+0x71/0x2a0 [xfs]
 [] ? sched_clock+0x9/0x10
 [] ? sched_clock_cpu+0xb5/0x100
 [] xfs_file_aio_write+0xdb/0x160 [xfs]
 [] do_sync_write+0x8d/0xd0
 [] vfs_write+0xc0/0x1f0
 [] ? fget_light+0x3a7/0x510
 [] SyS_write+0x5b/0xb0
 [] system_call_fastpath+0x16/0x1b
2 locks held by dd/3560:
 #0:  (sb_writers#8){.+.+.+}, at: [] vfs_write+0x1bb/0x1f0
 #1:  (&sb->s_type->i_mutex_key#12){+.+.+.}, at: [] xfs_file_buffered_aio_write+0x71/0x2a0 [xfs]
----------

(*2) RHEL 7 のデフォルト設定では、キーボードから発行できる SysRq コマンドが制限されています。キーボードから全ての SysRq コマンドを発行できるようにするには、
/proc/sys/kernel/sysrq に 1 を設定しておく必要があります。なお、 RHEL 6 以降であれば、 sysrq_always_enabled というカーネルコマンドラインオプションを指定することでも、全ての SysRq コマンドを発行できるようになります。

(*3) 以下に示します。

---------- 通常の RHEL 7 カーネルで使える SysRq コマンド ここから ----------
SysRq : HELP : loglevel(0-9) reboot(b) crash(c) terminate-all-tasks(e) memory-full-oom-kill(f) kill-all-tasks(i) thaw-filesystems(j) sak(k) show-backtrace-all-active-cpus(l) show-memory-usage(m) nice-all-RT-tasks(n) poweroff(o) show-registers(p) show-all-timers(q) unraw(r) sync(s) show-task-states(t) unmount(u) show-blocked-tasks(w) dump-ftrace-buffer(z)
---------- 通常の RHEL 7 カーネルで使える SysRq コマンド ここまで ----------
---------- デバッグ用の RHEL 7 カーネルで使える SysRq コマンド ここから ----------
SysRq : HELP : loglevel(0-9) reboot(b) crash(c) show-all-locks(d) terminate-all-tasks(e) memory-full-oom-kill(f) kill-all-tasks(i) thaw-filesystems(j) sak(k) show-backtrace-all-active-cpus(l) show-memory-usage(m) nice-all-RT-tasks(n) poweroff(o) show-registers(p) show-all-timers(q) unraw(r) sync(s) show-task-states(t) unmount(u) show-blocked-tasks(w) dump-ftrace-buffer(z)
---------- デバッグ用の RHEL 7 カーネルで使える SysRq コマンド ここまで ----------
---------- 通常の RHEL 6 カーネルで使える SysRq コマンド ここから ----------
SysRq : HELP : loglevel(0-9) reBoot Crash terminate-all-tasks(E) memory-full-oom-kill(F) kill-all-tasks(I) thaw-filesystems(J) saK show-backtrace-all-active-cpus(L) show-memory-usage(M) nice-all-RT-tasks(N) powerOff show-registers(P) show-all-timers(Q) unRaw Sync show-task-states(T) Unmount show-blocked-tasks(W) dump-ftrace-buffer(Z)
---------- 通常の RHEL 6 カーネルで使える SysRq コマンド ここまで ----------
---------- デバッグ用の RHEL 6 カーネルで使える SysRq コマンド ここから ----------
SysRq : HELP : loglevel(0-9) reBoot Crash show-all-locks(D) terminate-all-tasks(E) memory-full-oom-kill(F) kill-all-tasks(I) thaw-filesystems(J) saK show-backtrace-all-active-cpus(L) show-memory-usage(M) nice-all-RT-tasks(N) powerOff show-registers(P) show-all-timers(Q) unRaw Sync show-task-states(T) Unmount show-blocked-tasks(W) dump-ftrace-buffer(Z)
---------- デバッグ用の RHEL 6 カーネルで使える SysRq コマンド ここまで ----------
---------- 通常の RHEL 5 カーネルで使える SysRq コマンド ここから ----------
SysRq : HELP : loglevel0-8 reBoot Crashdump tErm Full kIll thaw-filesystems(J) saK showMem Nice powerOff showPc unRaw Sync showTasks Unmount shoWcpus
---------- 通常の RHEL 5 カーネルで使える SysRq コマンド ここまで ----------
---------- デバッグ用の RHEL 5 カーネルで使える SysRq コマンド ここから ----------
SysRq : HELP : loglevel0-8 reBoot Crashdump show-all-locks(D) tErm Full kIll thaw-filesystems(J) saK showMem Nice powerOff showPc unRaw Sync showTasks Unmount shoWcpus
---------- デバッグ用の RHEL 5 カーネルで使える SysRq コマンド ここまで ----------

(*4) kmemleak は、メモリリークではないものを誤検出することもあります。また、kmemleak のためのメモリを確保できなかった場合は以下のようなメッセージを出力して自動的に無効化されます。

----------
kmemleak: Cannot allocate a kmemleak_object structure
kmemleak: Kernel memory leak detector disabled
kmemleak: Automatic memory scanning thread ended
----------

(*5) これは、「フォールトインジェクションノススメ」 の「使い方2:メモリー割り当て要求を失敗させる。」で説明した挙動の影響を、xfs のほうがより受けやすいためであると考えています。

(半田哲夫)

コーヒーブレーク「私的サポート論」

前々回の記事、「サポートにおける環境問題」では、サポートセンターで事象が再現できるかどうかが問題解決の成否を左右すること、実際の問い合わせを見ていると、依頼元で発生した事象がサポートセンターで再現できないケースが多いこと、再現しない場合の具体的事例などについて説明しました。引き続き「環境」について、もう少し考えてみたいと思います。

注:タイトルにあるように本記事の内容はあくまで私個人の見解です。どうぞその点ご理解の上、お読みください。

サポートは、依頼に対して技術者が情報を提供するサービスで、サポート技術者の内容がサポートの内容に直結します。より多く、広い範囲に対応できる技術者を集めれば、それだけ対応できる範囲が広がり魅力的なサービスを提供することができますが、当然ながら原価も増大します。事業として成立させるためには、多くのユーザーが求める「最大公約数」的な範囲をターゲットとして、想定する問い合わせをこなせるだけの体制を組むという考え方が一般的でしょう。

「最大公約数」的な範囲の設定について、よく使われる製品(たとえばApache, PostgreSQL等)ごとにその内容に通じた技術者を集めることが一般的です。その応用形として、部品を組み合わせたものを「モデル」として定義し、モデルごとに手順や推奨する設定を作成し、想定した「環境」のもと動作を検証するというものも見かけます。モデルは、お惣菜屋さんが単品で販売しているものを組み合わせてお弁当として販売しているようなイメージです。お惣菜には種類しかありませんが、ソフトウェアにはバージョンがあります。サポートセンターにいると「使わないほうが・・・」というバージョンがあることが、実感としてよくわかりますが、モデルでは吟味されたものが組み合わされています(モデルそのものを使わなかったとしても、モデルで採用されている製品のバージョンを採用するだけでも将来のリスクを減らすことができます)。

ということで、良いことづくめのモデルですが、モデルはあくまでモデルであり、実際のシステム構築ではさまざまにアレンジ(カスタマイズ)して使うことになります。アレンジの内容や使い方の相違により、モデルとしての利点は薄くなり、部品ごとのレベルに落ちてしまいます。

ここで以下の質問について考えてみてください。
Q1: 同一の「環境」を実現することは可能か?できたとして、それを維持することは可能か?
Q2: 二つの「環境」が同一かどうかどのようにすれば判断できるか?

そもそも「環境」を明確に定義していないので、答えようがないとおしかりを受けるかもしれませんが、ここでは「ハードウェア、OS、ライブラリ、アプリケーション、設定等」とお考えください。

Q1について、仮想化技術を使われている方は「VMware/kvmならできる」と思われたのではないでしょうか?それはそうだと思います。しかし、VMのファイルを複製して配布したとして、それを使い始めたら、もはや同一とは言えなくなります。
Q2は、まず「同一とは何か」の定義から始めなければいけませんが、実際にできることは、これまでの技術では、せいぜいバージョンを含む導入済みパッケージ一覧や設定ファイルの比較くらいかと思います。

つまるところ、あなたの環境、あるいは問題が発生した環境は、そこにしかなく、サポートセンターや他の場所で完全に同じ環境を用意することはできません。新たな脆弱性が発見された場合、「この脆弱性の影響があるでしょうか?」と聞かれることがあります。ここで影響とは「私が管理するシステム」への影響を指しています。質問したくなる気持ちはよくわかるのですが、サポートセンターでは「あなたが管理するシステム(という環境)」について、提供された情報の範囲しかわかりません。通常、「最大公約数」の範囲で情報提供を行いますが、本当の答えは「お答えできません」となります。

(原田季栄)



第16回「 kernel-debug ノススメ」