Fedoraで実践するbpftraceとBCC入門:eBPFによるLinuxパフォーマンス分析とトラブルシューティング

Fedora tutorial - IT technology blog
Fedora tutorial - IT technology blog

障害発生時、topやhtopが全く役に立たない瞬間

インフラ運用に携わるエンジニアなら、一度はこんな修羅場を経験したことがあるでしょう。Slackにアラートが乱れ飛び、APIのp99レイテンシーは2,500msに跳ね上がり、CPUのiowaitは70%に達している。慌てて対象ノードにSSHログインし、すかさずtop、htop、iotopを叩く。しかし奇妙なことに、表示されるメトリクスは一見正常そのもの。PostgresプロセスはCPUを12%しか消費しておらず、ディスクI/Oも25 MB/s程度で平穏に推移している。どのPID、どのクエリ、あるいはどのinodeがシステムを圧迫しているのか、完全に手がかりを見失ってしまいます。

手詰まりになったときの自然な反射行動として、システムコールを追跡しようとstraceを持ち出しがちです。数か月前、私のチームのジュニアエンジニアが、15,000 QPSを捌いている本番環境のデータベースでまさにこのコマンドを実行してしまいました。

# 高負荷な本番環境では絶対に実行しないでください!
sudo strace -p $(pgrep -f postgres) -T -tt

その結果はどうなったでしょうか。実行からわずか3秒後、データベースは大量のコネクション切断を引き起こしました。ptraceの仕組み上、カーネルはシステムコールの開始(entry)と終了(exit)のたびにスレッドを一時停止させます。アプリケーションが毎秒数万件のI/Oを処理している状況では、straceによるオーバーヘッドでスループットが40〜80分の1にまで激減します。不安定だっただけのシステムを、自らの手で完全ダウンへと追い込んでしまったのです。

ユーザー空間とカーネル空間の間に潜む致命的な死角

従来の標準監視ツールが通用しないのは、構造的な制約が存在するためです。

  • サンプリング精度が粗すぎる: top、vmstat、sarなどは、/procや/sysから定期的に集計値を取得しているに過ぎません。15msだけ一時的なI/O詰まりが発生して収まるような「マイクロバースト」が起きた場合、1秒間隔のサンプリングでは数値が平滑化されてしまいます。グラフ上は極めて穏やかに見えても、エンドユーザーは確実にタイムアウトを食らっています。
  • コンテキストの欠落: iostatがNVMeドライブ/dev/nvme0n1のビジー率98%を報告したとしても、どのスレッドがリクエストを発行したのか、どのファイルディスクリプタが詰まっているのか、どのオフセットのデータブロックなのかまでは特定できません。
  • 過度な干渉(侵襲性): 従来のトレース手法は過剰なコンテキストスイッチを引き起こし、レイテンシーを悪化させるため、本番負荷がかかる環境で安全に実行することは不可能です。

この課題を根本から解決するのがeBPF(Extended Berkeley Packet Filter)です。Fedoraは常に最新のLinuxカーネルを提供しているため、eBPFの検証・運用環境として最適です。カーネル空間とユーザー空間の間で継続的にデータをコピーする代わりに、eBPFはカーネルのverifier(検証機構)によって安全性が保証されたバイトコードを直接カーネル内にロードします。kprobe、tracepoint、あるいはuprobeに直接アタッチすることで、計測時のオーバーヘッドを通常1.5%未満に抑え込むことができます。

FedoraにおけるeBPF活用の3つのアプローチ:即戦力ツールからフルカスタムまで

障害対応時の時間的プレッシャーや問題の複雑さに応じて、3つの選択肢があります。

アプローチ1: BCC(BPF Compiler Collection)の標準ツール群を活用する

BCCは、Brendan Gregg氏をはじめとするコミュニティによって最適化された、実績ある多数のPython/Cスクリプトを集約したツールキットです。自身でコードを書かなくても、一般的なトラブルの8割はこれだけで解決できます。

Fedoraでのツールパッケージのインストール:

sudo dnf install -y bcc-tools kernel-devel-$(uname -r) kernel-headers-$(uname -r)

現場でのTips: 実行ファイルはコマンド名の衝突を避けるため、/usr/binではなくすべて/usr/share/bcc/tools/配下に配置されています。このディレクトリを環境変数PATHに追加するか、絶対パスで直接実行することをおすすめします。

biolatency-bpfccでディスクI/Oのボトルネックを特定する:

# ブロックI/Oレイテンシー(ミリ秒単位)を計測し、ヒストグラムとして集計
sudo /usr/share/bcc/tools/biolatency-bpfcc -m 5

意味のない平均値を眺める代わりに、レイテンシーの分布が一目で把握できます。1ms未満で完了したI/Oが何件あり、ディスクキューの滞留によって危険水域である512ms以上に達したリクエストが何件あるのかを正確に突き止められます。

execsnoop-bpfccで短命プロセスを捕捉する:

# システム内で新たにfork/execされたすべてのプロセスを即座にキャプチャ
sudo /usr/share/bcc/tools/execsnoop-bpfcc -T

この手法は、CPU使用率が一瞬だけ100%にスパイクしてすぐに消え去るようなケースで威力を発揮します。topが1秒周期で画面更新を行う前に、cronから起動された子プロセスのBashスクリプトが処理を終えて終了してしまうような挙動も逃さず捉えられます。

アプローチ2: C言語とlibbpf(CO-RE)でネイティブeBPFプログラムを開発する

このアプローチでは純粋なC言語を用い、Clangで軽量なELFバイトコードにコンパイルしてlibbpf経由でロードします。メモリ効率が極めて高く、本番ノード上に重厚なコンパイラを導入する必要がないため、DatadogやCiliumなどの長期稼働モニタリングデーモンにおけるデファクトスタンダードとなっています。

一方で、事前の準備と設計には相応の工数を要します。BPFリングバッファやBPFマップの管理、データを読み取るユーザー空間コードの実装などをすべて自前で行わなければなりません。システム障害の真っ只中に、単一のカーネル関数を調べるためだけに何時間もかけてCコードの記述やデバッグを行うのは非現実的です。

アプローチ3: bpftraceのワンライナーで迅速に診断する

BCCを「既製のスパナセット」、libbpfを「金属加工工場」に例えるなら、bpftraceはまさに「万能スイスアーミーナイフ」です。awkとC言語を融合させたようなシンタックスにより、ターミナル上で極めて強力なワンライナーを即座に記述できます。

Fedoraでのパッケージインストール:

sudo dnf install -y bpftrace

システム内でどのプロセスがファイルを開いているかをリアルタイム追跡:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("[%s] (PID %d) 開いたファイル: %s\n", comm, pid, str(args.filename)); }'

5秒ごとにディスク読み込み回数が多いプロセスをランキング表示:

sudo bpftrace -e 'kprobe:vfs_read { @[comm] = count(); } interval:s:5 { print(@); clear(@); }'

わずか1行のコマンドで、vfs_read関数の呼び出し回数を正確にカウントし、プロセス名ごとにグループ化して集計結果を出力できます。高速かつ高精度で、システムリソースを浪費しません。

実践トラブルシューティング手順:BCCとbpftraceの連携フロー

幾度もの深夜のオンコール障害対応を経て辿り着いた、最も確実な「2段階トラブルシューティング手順」を紹介します。

  1. フェーズ1 – BCCでマクロな切り分けを実施:

    ボトルネックが特定できていない段階でいきなりスクリプトを自作するのは避けるべきです。まずは以下のクイック診断ツールを実行します。

    • I/Oのボトルネックが疑われる場合: biolatency-bpfccでレイテンシーを把握し、biosnoop-bpfccで原因となっているPIDを特定します。
    • ネットワークの遅延が疑われる場合: tcpconnect-bpfccやtcptracer-bpfccを使用して、TCPハンドシェイクのレイテンシーを測定します。
    • メモリリークが疑われる場合: memleak-bpfcc -p <PID>を実行し、mallocで確保されたままfreeされていないメモリブロックを捕捉します。
  2. フェーズ2 – bpftraceでミクロな詳細分析を実施:

    BCCによって疑わしいPIDやサブシステムが特定できたら、bpftraceを用いてピンポイントで詳細調査を行います。たとえば、Nginxのワーカープロセスがディスクキャッシュ読み出し時にスタックしている疑いがある場合、そのワーカーのreadシステムコールにおけるレイテンシー分布のみを計測します。

# nginxプロセスのreadシステムコールのレイテンシーを計測(単位: マイクロ秒)
sudo bpftrace -e '
  tracepoint:syscalls:sys_enter_read /comm == "nginx"/ {
    @start[tid] = nsecs;
  }
  tracepoint:syscalls:sys_exit_read /@start[tid]/ {
    @duration_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
  }
'

Fedora上でeBPFを運用する際の3つの教訓

  • カーネルヘッダーのバージョン不一致: Fedoraは頻繁にカーネル更新を配信します。dnf upgradeを実行した後に再起動していない場合、新しく導入されたkernel-develのバージョンと、現在メモリ上にロードされているカーネル(uname -r)のバージョンが乖離します。これにより、BCCやbpftraceは即座にコンパイルエラーを吐きます。デバッグ前には必ずrpm -q kernel-develとuname -rの整合性を確認してください。
  • SELinuxのアクセス制御: FedoraではデフォルトでSELinuxがEnforcingモードで動作しています。たとえroot権限で実行したりCAP_BPFを付与していたとしても、SELinuxポリシーによって機密性の高いデーモンのトレースがブロックされることがあります。身に覚えのないPermission deniedが発生した場合は、速やかにjournalctl -t setroubleshootや/var/log/audit/audit.logを確認してください。
  • bpftraceにおけるマップ変数の解放漏れ: トレーススクリプトを記述する際、@start[tid]のようなマップはカーネルメモリ内のスワップ不可領域を直接消費します。高負荷な処理をトレースしている最中に、レイテンシー計算後のdelete(@start[tid])の呼び出しを怠ると、カーネルマップが無制限に肥大化し、メモリ枯渇を引き起こす深刻なリスクとなります。
Share: