未来を形作るテクノロジーの深掘り記事。

eBPFスピンロックとカーネルデバッグの実践

eBPFのスピンロックバグがLinuxカーネルでどう表面化するのか、なぜ見つけにくいのか、デバッグの過程からeBPFの安全性モデルが何を示すかを解説します。

凍りついた回路基板の風景の中で、火花を散らしながら回転する光るパッドロック

eBPFのスピンロックバグは、スタックトレースもエラーメッセージも出さずに存在を知らせてきます。知らせてくるのは、マシンが応答しなくなるという形です。ログも警告もなく、穏やかな劣化すらありません。解放されないロックを待って、CPUコアがひたすら回り続けるだけです。運が良ければウォッチドッグタイマーが作動し、トレース付きのカーネルパニックが出ます。運が悪ければマシンがただフリーズして、電源を入れ直すしかありません。

eBPFは安全であるはずです。カーネル内で動作しますが、ベリファイアがプログラムの終了性、不正なメモリアクセスの不在、カーネル状態の非破壊を証明します。では、なぜスピンロックのバグがすり抜けるのでしょうか。ベリファイアが検査するのは個々のプログラムだからです。一方、スピンロックのバグは、プログラム同士、マップ、そしてカーネルのスケジューリング判断の相互作用から生まれます。静的なベリファイアには完全には予測できないものです。

eBPFスピンロックの役割

eBPFプログラムはカーネルのコンテキストで動作し、複数のCPUで同時に走ることも珍しくありません。2つのeBPFプログラムが同じマップの値、たとえばカウンタやデータ構造、ステートマシンを更新する必要がある場合は、同期が必要になります。そのためにeBPFはbpf_spin_lockとbpf_spin_unlockを提供しています。

スピンロックは考えうる中で最もシンプルなロックです。取得を試みて、すでに保持されていれば解放されるまでタイトなループで回り続けます。スリープもyieldもキューイングもありません。eBPFプログラムは割り込みハンドラ、softirq、プリエンプション無効区間といった、スリープが許されないコンテキストで動くため、この方式が適しています。呼び出し元をスリープさせるmutexを使えばカーネルはクラッシュします。スピンロックなら、ロックが空くまでCPUサイクルを消費し続けるだけです。

struct map_value {
struct bpf_spin_lock lock;
__u64 counter;
__u32 last_pid;
};
SEC("tp/sched/sched_switch")
int handle_sched_switch(struct trace_event_raw_sched_switch *ctx) {
__u32 key = 0;
struct map_value *val;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
bpf_spin_lock(&val->lock);
val->counter++;
val->last_pid = ctx->next_pid;
bpf_spin_unlock(&val->lock);
return 0;
}

ここまでは単純に見えます。そして、ロックして値を更新してアンロックするという単純なケースでは、確かにきちんと動きます。問題が起きるのは、ロックのパターンが複雑になったときや、eBPFプログラムとカーネル自身のロック機構が予期しない依存関係を作ったときです。

スピンロックのバグはどう現れるか

典型的なスピンロックのバグはデッドロックです。プログラムAがロック1を保持したままロック2を取得しようとし、プログラムBがロック2を保持したままロック1を取得しようとします。両者は互いを待って永遠に回り続けます。カーネルのコンテキストでは、これは該当のCPUコアが失われることを意味し、そのコアはもう何も実行できません。

eBPFベリファイアは、こうしたケースの一部を防ぎます。同時に複数のスピンロックを保持するプログラムを拒否するので、古典的なAB-BAデッドロックは排除されます。ただし、すべてのデッドロックを防げるわけではありません。eBPFスピンロックとカーネル自身のロックとの関係から生まれるものがあるからです。

次のようなシナリオを考えてみてください。トレースポイントに接続されたeBPFプログラムがマップ内のスピンロックを取得します。ロックを保持している最中に、同じCPU上でハードウェア割り込みが発生します。割り込みハンドラが別のeBPFプログラムを実行し、そのプログラムが同じスピンロックを取得しようとします。デッドロックです。割り込みはロックを取得するまで戻れず、ロックは割り込まれたプログラムが再開しなければ解放できず、そのプログラムは割り込みが戻らなければ再開できません。

Deadlock scenario (same CPU):
CPU 0 running eBPF program A
→ acquires spinlock on map_value
→ hardware interrupt fires
→ CPU 0 runs interrupt handler
→ interrupt handler runs eBPF program B
→ program B tries to acquire same spinlock
→ spinlock is held by program A
→ program B spins... forever
→ interrupt handler never returns
→ program A never resumes
→ program A never releases lock
→ CPU 0 is permanently stuck

カーネル自身のスピンロックは、ロック保持中に割り込みを無効化する(spin_lock_irqsave)ことでこの問題を回避しています。eBPFスピンロックも同じ考え方で、bpf_spin_lockはプリエンプションを無効化し、カーネル5.1以降ではsoftirqも無効化します。ただし、すべての割り込みコンテキストがカバーされるわけではなく、カーネルのバージョンによって扱いも異なります。巧妙なバグが潜むのは、まさにこの部分です。

デバッグの難しさ

eBPFのスピンロック問題のデバッグが難しいのには、具体的な理由があります。

再現が非決定的です。スピンロックのバグはタイミングに依存します。どのCPUがどのプログラムを実行するか、いつ割り込みが発生するか、クリティカルセクションがどれくらい続くか、といったことで結果が変わります。999回は問題なく動くテストが、1000回目にデッドロックすることもあります。開発用マシンのCPUトポロジ、割り込み頻度、負荷パターンは本番環境と違うため、開発環境で確実に再現することはできません。

障害の形が証拠を消してしまいます。スピンロックのデッドロックが起きると、影響を受けたCPUは何も実行できなくなります。何が起きたかを教えてくれるはずのログ基盤や監視基盤も含めてです。デッドロックが1つのCPU上だけなら、マシンは何とか動き続けるかもしれません。複数のCPUがデッドロックしたり、ロックを保持したCPUが他のCPUの必要とするリソースを握っていたりすると、マシン全体がハングします。

従来のデバッグツールは役に立ちません。デッドロックしたカーネルの状態を調べるためにデバッガを接続することはできません(KGDBを使えばできますが、デッドロックより前にシリアル接続を準備しておく必要があります)。dmesgも無意味です。書き込んでくれるものが何もないからです。信頼できる情報が得られるのは、ロックアップ検知機構の出力だけです。マシンが完全に止まる前に作動していればの話ですが。

実際に効く対策

こうした困難があっても、eBPFのスピンロックバグを見つけて直している人たちは実際にいます。そのやり方を紹介します。

ロック順序の解析。何かを実行する前に、すべてのeBPFプログラムのロック動作を解析します。各プログラムはどのマップにアクセスするか。どのロックを取得するか。同じロックに触れる2つのプログラムが同じCPU上で動く可能性はあるか(同じトレースポイント、またはトレースポイントと割り込みの組み合わせ)。手作業の地味な分析ですが、よくあるデッドロックのパターンを事前に捕まえられます。

ロック保持時間の監視。eBPFプログラムに計測を入れて、スピンロックをどれくらいの時間保持しているかを測ります。数マイクロ秒を超えて保持しているスピンロックは危険信号です。割り込み起因のデッドロックが起きる時間窓が広がり、他のCPUが回転して無駄なサイクルを使うことにもつながります。クリティカルセクションは短いほど速く、安全です。

// Monitoring lock hold time in eBPF
SEC("tp/sched/sched_switch")
int handle_switch(void *ctx) {
struct map_value *val;
__u64 start, elapsed;
__u32 key = 0;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
start = bpf_ktime_get_ns();
bpf_spin_lock(&val->lock);
// Critical section — keep this minimal
val->counter++;
bpf_spin_unlock(&val->lock);
elapsed = bpf_ktime_get_ns() - start;
// Report if lock was held too long
if (elapsed > 1000) {  // > 1 microsecond
bpf_printk("lock held for %llu ns", elapsed);
}
return 0;
}

CPUごとのデータ構造。最良のスピンロックバグは、そもそも起きないものです。各CPUが自分専用のデータのコピーを扱い、後から集約すれば、競合もなくロックも不要になります。eBPFのBPF_MAP_TYPE_PERCPU_HASHとBPF_MAP_TYPE_PERCPU_ARRAYは、まさにこのために設計されています。カウンタ、ヒストグラム、アキュムレータが共有状態のスピンロックを必要とすることはほとんどありません。

ロックの代わりにアトミック操作を使う。カウンタのインクリメントや値の比較交換のような単純な操作なら、__sync_fetch_and_addなどのアトミック操作の方が速く、デッドロックも起きません。カウンタを増やすのにスピンロックは要りません。スピンロックは、複数のフィールドをアトミックに更新する必要が本当にある処理のために取っておきましょう。

eBPFの安全性モデルが示すこと

eBPFの安全性の仕組みは印象的ですが、一筋縄ではいきません。ベリファイアは個々のプログラムが安全であることを保証します。終了すること、不正なメモリにアクセスしないこと、カーネル状態を壊さないことです。しかし「安全な」プログラム同士も、相互作用によってベリファイアが解析できないタイミング依存を生むと、全体として安全でないシステムを構成してしまうことがあります。

これは静的解析の本質的な限界です。ベリファイアは各プログラムを単独で見ます。どの他のプログラムがロードされているか、どのマップを共有しているか、どのカーネルコンテキストで動くかは分かりません。スピンロックを取得するプログラムは、正しく取得して解放するなら「安全」です。しかし、それがロードされている他のすべてのeBPFプログラムと組み合わさったときに安全かどうかは、実行時の条件に依存します。

カーネルコミュニティは、これに段階的に取り組んでいます。最近のパッチでは、どのeBPFプログラムタイプがスピンロックを使えるか、クリティカルセクションをどれくらい長くできるか、スピンロック保持中に何の操作を禁止するかといった制限が加えられています。制限が増えるたびにバグの入り込む余地は狭まりますが、eBPFプログラムにできることも限られていきます。

eBPFロックの実践ルール

スピンロックの問題をいくつもデバッグしてきた中で、避けるためのパターンが見えてきました。

  1. CPUごとのマップを優先する。データをCPUごとに分割して、ユーザー空間で集約できるなら、そうしてください。ロックも競合もデッドロックもなくなります。カウンタ、イベントログ、ヒストグラムなど、ユースケースの8割はこれで対応できます。
  2. スピンロックよりアトミック操作を優先する。共有カウンタや比較交換が必要なら、アトミック操作を使いましょう。ロックフリーなのでデッドロックは起きません。
  3. スピンロックを使うなら、クリティカルセクションは極小に保つ。カウンタを増やす、タイムスタンプを更新する、ポインタを入れ替える。それ以上は何もしないでください。ロック保持中にヘルパー関数を呼ぶのは避けましょう。その関数が内部でどんなロックを取るか分からないからです。
  4. 同じロックを2種類のeBPFプログラムタイプから保持しない。kprobeプログラムとトレースポイントプログラムが同じマップの値をロックするなら、割り込み1回でデッドロックします。マップを分けるか、CPUごとのデータを使ってください。
  5. 負荷をかけてテストする。スピンロックのバグはタイミングに依存します。競合が起きたとき、つまりCPU使用率が高いとき、割り込みが頻発するとき、多数のeBPFプログラムが同時に走るときに表面化します。アイドル状態の開発機ではなく、現実に近い本番負荷でテストしてください。

eBPFはLinuxカーネルとのやり取りの仕方を根本から変え、ユーザー空間のプログラムにカーネルレベルのオブザーバビリティとネットワーキングへの安全なアクセスを与えました。ただし、「安全」には境界があります。ベリファイアのおかげで、生のカーネルモジュールを書くよりeBPFは格段に安全です。しかし、ユーザー空間のプログラミングが持つような安全性とは別物です。その境界がどこにあるのか、特に共有状態とロックまわりでどこにあるのかを理解しているかどうかで、テストでは動くのに本番で動かないeBPFプログラムと、本番でも動くプログラムに分かれます。