bpftraceを極める:ワンライナーで解き明かすLinuxパフォーマンスの謎

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

ダッシュボードを超えて:なぜ従来のツールでは不十分なのか

私は10年にわたりLinuxサーバー管理の最前線に身を置いてきましたが、最も手強いバグは常に「目に見えない」ものでした。topでCPUスパイクを確認したり、iostatでディスク遅延の急増を見たりすることはできますが、これらのツールは症状を示しているだけで、原因を特定しているわけではありません。長年、深い調査にはstraceが使われてきました。しかし、稼働中のデータベースでstraceを使用するのはリスクが伴います。高頻度のシステムコールに対して50〜100%のオーバーヘッドを加え、救おうとしているサービスそのものを窒息させてしまう場面を何度も見てきました。

そこで登場するのがbpftraceです。これはeBPF(Extended Berkeley Packet Filter)を利用して、カーネル内でサンドボックス化されたプログラムを実行します。これは単なる改良ではなく、パラダイムシフトです。1秒間に10,000リクエストを処理する高負荷なUbuntu 22.04ウェブサーバーで、私はbpftraceを使用して、アプリケーション側がトレーサーの実行に全く気づかないレベルで遅延の原因を突き止めたことがあります。スループットへの影響をほぼゼロに抑えつつ、イベントをフックできるのです。

クイックスタート:5分で始めるトレーシング

モダンなディストリビューションであれば、bpftraceのセットアップは驚くほど簡単です。これは高レベルなラッパーとして機能し、簡潔なドメイン固有言語を効率的なBPFバイトコードにその場でコンパイルします。

インストール

Ubuntu/Debianの場合:

sudo apt update
sudo apt install bpftrace

Fedora/RHELの場合:

sudo dnf install bpftrace

最初の実践的ワンライナー

今、システム全体でどのファイルがアクセスされているか知りたいですか?これを実行してみてください:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s (%d) が %s を開きました\n", comm, pid, str(args->filename)); }'

内容を詳しく見てみましょう。tracepointがフック(検知場所)です。comm変数はプロセス名、args->filenameはファイルパスを抽出します。最近私はこれを使って、1時間ごとに存在しない50万個のファイルを誤って開こうとしていた、制御不能になったログローテーションスクリプトを捕まえました。

エンジン:プローブが実際に動作する仕組み

効果的なトレーシングには、センサーの理解が欠かせません。プローブは、カーネル内やユーザー空間のどこにでも配置できる手術器具のようなものだと考えてください。

プローブの階層

  1. トレースポイント (Tracepoints): これらはカーネル内の安定した定義済みマーカーです。カーネルのアップグレードで壊れることが稀なため、常にこれらを最初に使用してください。
  2. Kprobe (Kernel Probes): 事実上すべての内部カーネル関数をフックするために使用します。強力ですが、カーネルバージョン間で関数名が変わる可能性があるため、脆弱です。
  3. Uprobe (User Probes): MySQLやNode.jsプロセス内の特定のメソッドなど、ユーザーバイナリ内の関数をターゲットにします。
  4. ソフトウェア/ハードウェアイベント: CPUサイクル、命令、ページフォルトなどの低レベルメトリクスを追跡します。

マップによるデータの集計

すべてのイベントを画面に出力すると、パフォーマンスが著しく低下します。代わりに「マップ (maps)」を使用して、カーネル内でデータを要約しましょう。このワンライナーは、プロセス名ごとにシステムコールの回数をカウントします:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_read { @[comm] = count(); }'

数秒後にCtrl+Cを押してください。最も頻繁に呼び出しを行っているプロセスのきれいなテーブルが表示されます。混み合ったコンテナ環境で「うるさい隣人(ノイジー・ネイバー)」を見つける最速の方法です。

実践バトルドリル

本番システムが異常な挙動を示したときに、私が頼りにしている3つのワンライナーを紹介します。

1. ブロックI/O遅延の可視化

ディスクの遅延は、データベースクエリが遅い原因として隠れていることがよくあります。単一の平均値ではなく、分布ヒストグラムを作成します:

sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @latency = hist(nsecs - @start[tid]); delete(@start[tid]); }'

10,000,000ナノ秒(10ミリ秒)以上のイベントが固まって表示される場合、SSDやSANが飽和している可能性があります。

2. 短命なプロセスの捕捉

ゾンビプロセスや頻繁なシェル実行はパフォーマンスを低下させます。これは、すべての新しい実行をフル引数とともにキャプチャします:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%-10u %-15s", pid, comm); join(args->argv); }'

ここでのjoin()関数はまさに魔法です。コマンドライン文字列を再構築してくれるため、正体不明のcronジョブが具体的に何をしているのかを正確に把握できます。

3. TCP再送のデバッグ

ネットワーク遅延の切り分けは非常に困難です。このフックは、カーネルがパケットを再送しなければならないたびに実行されます:

sudo bpftrace -e 'kprobe:tcp_retransmit_skb { printf("再送検知: %s (PID %d)\n", comm, pid); }'

このコマンドの結果が激しくスクロールし始めたら、コードを疑うのをやめて、ケーブルやスイッチ、あるいはクラウドプロバイダーのステータスページを確認し始めましょう。

本番環境での安全策

bpftraceは効率的ですが、万能ではありません。トラブルを避けるための簡単なルールがいくつかあります。

出力を制限する

10Gbpsリンクのパケット処理など、毎秒数百万回発生するイベントに対しては、決してprintfを使用しないでください。ターミナルが溢れ、テキストのフォーマット処理でCPUサイクルを浪費してしまいます。代わりにヒストグラムやカウンターを使用し、カーネルに計算を任せて、要約だけを報告させるようにしましょう。

フックする前に確認する

特定のカーネルバージョンでプローブが存在するかどうかを、事前に確認してください:

sudo bpftrace -l '*openat*'

これにより時間を節約でき、異なるLinuxディストリビューション間で切り替える際の「コマンドが見つかりません」といったエラーを防ぐことができます。

クリーンな終了

常にbpftraceプロセスが適切に終了することを確認してください。BPFプログラムは安全で自己クリーニングするように設計されていますが、クラスター全体で忘れ去られたトレーサーが何十個も動いていると、積み重なったオーバーヘッドが深刻な問題を引き起こす恐れがあります。まずはシンプルに始め、重い処理にはマップを活用してください。そうすれば、bpftraceがあなたの道具箱の中で最も欠かせないツールであることに気づくはずです。

Share: