LabHub

ブログ

eBPFがオブザーバビリティを飲み込む

한국어English日本語中文

はじめに — カーネルの中でコードを走らせるという発想

オブザーバビリティ(可観測性)のツールを導入したことがある人なら、おなじみの疲労感があるはずです。アプリごとにエージェントを入れ、言語ごとに計装ライブラリを付け、ポッドごとにサイドカーを立て、そうして付けたものが今度はCPUとメモリを食っていく。何かを観測するためにシステムをますます重く複雑にしていくという逆説です。

eBPFはこの問題をまったく別の角度から攻めます。アプリに手を入れる代わりに、カーネルの中まで降りていって、その下からすべてを見張るのです。ネットワークパケット、システムコール、関数呼び出し、ディスクI/Oはすべてカーネルを通るので、カーネルで一度観測すれば、その上で何が動いていても計装できます。アプリは自分が観測されていることすら知りません。

この記事では、eBPFが正確には何なのか、どうやってカーネルの中で任意のコードを安全に走らせられるのか、そしてなぜそれがオブザーバビリティのツールを次々と飲み込んでいるのかを扱います。同時に、eBPFが万能ではないこと、その限界がどこにあるのかも正直に指摘します。

eBPFとは何か — カーネルの中の小さな仮想マシン

eBPF(extended Berkeley Packet Filter)は、その名に反してもはやパケットフィルタに限りません。今のeBPFは、Linuxカーネルの中に組み込まれた小さな仮想マシンに近いものです。小さなプログラムを書くと、カーネルがそれを特定のイベントに結び付けておき、そのイベントが起きるたびにカーネルの文脈の中で実行します。

核心はこうです。従来、カーネルの動作を変えたり覗いたりするにはカーネルモジュールを書く必要がありました。カーネルモジュールは強力ですが危険です。バグ一つがカーネル全体をパニックに追い込み、書き方を誤ればシステムを止めてしまいます。だからプロダクションのカーネルに気軽にモジュールを載せるのは、大きな決心が要ることでした。

eBPFはこの危険を取り除きます。eBPFプログラムはカーネルの中で動きますが、カーネルを崩せないように厳しく制約されます。この制約を強制するのが、後で詳しく扱う検証器(verifier)です。おかげで私たちは、カーネルモジュールの力に近づきつつ、システムを止めるリスクなしに、プロダクションでプログラムを付けたり外したりできます。

動作の全体像は次のとおりです。

  ユーザー空間
  +------------------------------+
  | 1. C/RustなどでeBPFを記述     |
  | 2. LLVMがeBPFバイトコードに    |
  |    コンパイル                 |
  +--------------+---------------+
                 | bpf() システムコールでロード
                 v
  カーネル空間
  +------------------------------+
  | 3. 検証器が安全性をチェック   |
  | 4. JITがネイティブコードに変換 |
  | 5. フック地点に取り付け        |
  +--------------+---------------+
                 | イベント発生時に実行
                 v
  +------------------------------+
  | 6. マップ(map)に結果を書き込み |
  |    ユーザー空間がマップを読む  |
  +------------------------------+

プログラムはユーザー空間で記述・コンパイルされ、bpf()システムコールでカーネルにロードされます。検証器を通ればJITコンパイラがそれをネイティブの機械語に変え、指定したフック地点に取り付けます。プログラムが集めたデータは、マップという共有データ構造を通じてユーザー空間へ渡されます。

どこにフックを掛けられるか — kprobe、uprobe、tracepoint、XDP

eBPFの力は「どこに取り付けられるか」から生まれます。フック地点の種類が、そのまま観測できる対象の範囲になります。代表的な四つを見てみましょう。

このリストを貫く洞察が一つあります。システムで興味深いことはほとんどすべてカーネルを通る、ということです。ファイルを開こうと、ソケットを作ろうと、プロセスをフォークしようと、パケットを送ろうと、その瞬間にカーネルの何らかの関数やトレースポイントが実行されます。eBPFはまさにその地点に座って、システム全体の活動を一か所から見張るのです。

検証器 — なぜカーネルは死なないのか

ここで自然な疑問が湧きます。任意のコードをカーネルの中で走らせるなら、無限ループに陥ったり不正なメモリに触れたりしてカーネルを崩せるのではないか? eBPFがプロダクションで信頼される理由は、まさにこの問いへの答え、検証器にあります。

検証器は、プログラムがカーネルにロードされる瞬間、実行される前に静的に解析します。プログラムが取りうるすべての実行経路をたどり、次のことを証明しようとします。

この過程を通らなければ、カーネルはプログラムのロードを拒否します。つまり安全でないeBPFプログラムは、そもそも実行される機会を得られません。これがカーネルモジュールとの決定的な違いです。カーネルモジュールは「信じて載せる」ものですが、eBPFは「証明されてこそ載る」ものです。

もちろん検証器は完璧ではありません。実際には安全なプログラムなのに、検証器が安全性を証明できずに拒否することがあります。eBPF開発者が「検証器と戦う」と冗談を言うのはこのためです。検証器を満足させようとまともなコードをあちこち捻じ曲げる経験は、eBPFを真剣に扱ったことのある人なら誰もがしています。しかしこの厳しさこそ、カーネルが死なない代償なのです。

ゼロ計装トレーシング — コードを変えない

eBPFがオブザーバビリティで革命的な理由を一言でまとめると「ゼロ計装(zero-instrumentation)」です。従来のオブザーバビリティは、アプリのコードに計装を埋め込むところから始まります。トレースするには各リクエストにスパンを開くコードを入れ、メトリクスを取るにはカウンタを増やすコードを入れます。言語ごとにSDKが違い、ライブラリを更新するたびに手直しが必要です。

eBPFはこの前提をひっくり返します。アプリはそのままにして、カーネルのフックから必要な情報を観測します。HTTPリクエストをトレースしたいならソケット関連のカーネル関数に、gRPCのレイテンシを測りたいなら関連するuprobeにフックを掛ければよいのです。アプリは再コンパイルする必要も、再起動する必要も、そもそも自分が観測されていると知る必要もありません。

これが実務で持つ意味は大きいです。

ただし「ゼロ計装」という言葉は誤解を招きかねないので正確にしておきましょう。eBPFがカーネルレベルで自動的に見るのは、システムコール、ネットワークフロー、関数呼び出しといった低レベルの信号です。「このリクエストがどのビジネストランザクションに属するか」といったアプリ固有の意味論はカーネルには分かりません。だから高レベルの分散トレーシングにおけるコンテキスト伝播のようなものは、依然としてアプリの協力を要することが多いです。eBPFは計装の大部分をなくしてくれますが、すべてをなくしてくれるわけではありません。

ツールたち — Cilium、Falco、Pixie、bpftrace

理論を越えて、eBPFはすでにプロダクションで広く使われるツールのエンジンになっています。代表的な四つを見てみましょう。

たとえばbpftraceで、あるプロセスがどのシステムコールをどれだけ呼ぶかを数えるのはこれほど簡単です。

# プロセスごとのシステムコール呼び出し回数を集計
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

この一行がカーネルのトレースポイントにフックを掛け、システムコールが発生するたびにプロセス名(comm)ごとにカウンタを増やし、終了時に集計を出力します。別途のエージェントも、アプリの修正もありません。この即時性こそ、bpftraceがプロダクションのデバッグで愛される理由です。

これらのツールをブラウザで概念的に試してみたいなら、eBPFプレイグラウンドでeBPFの命令セットと検証器の動作をシミュレーションで学べますし、Kubernetesネットワーキングの文脈はKubernetesネットワークラボで見ることができます。

なぜサイドカーに勝つのか

ここ数年、クラウドネイティブの世界の既定のパターンはサイドカーでした。サービスメッシュはすべてのポッドの隣にプロキシコンテナを付けてトラフィックを傍受し観測しました。このモデルはうまく動きましたが、代償が大きかったのです。

サイドカーの問題を挙げてみましょう。

eBPFはこの構造を根本的に変えます。プロキシを各ポッドの隣に置く代わりに、カーネルの中で一度観測してポリシーを適用します。ノードごとにプロキシ1000個ではなく、カーネルという共有地点一つで処理するのです。トラフィックがユーザー空間のプロキシへ迂回して戻る必要がないので、レイテンシも減ります。

もちろんこれは「サイドカーは死んだ」という意味ではありません。サイドカーは依然として、アプリレベルの複雑なロジック(高度なルーティングルール、プロトコル別の精緻な操作)を扱うのに有利です。eBPFは低レベルの速い経路で、サイドカーは高レベルの柔軟な経路で、それぞれの強みがあります。実際、業界は両者を組み合わせた「サイドカーなしのデータプレーン+必要な所だけプロキシ」というハイブリッドに収束しつつあります。

eBPFにできないこと — 限界と落とし穴

eBPFは強力ですが万能ではありません。導入する前に知っておくべき限界がはっきりあります。

これらの限界を総合すると、eBPFは「Linuxの上で、低レベル・高性能・システム全般の観測とネットワーキング」に最適です。クロスプラットフォームが必要な場合、深いアプリの意味論が核心である場合、あるいは検証器が扱いきれないほど複雑なロジックが必要な場合には、別のアプローチのほうが良いです。

おわりに — オブザーバビリティの重心が下がる

eBPFが起こす変化の本質は、オブザーバビリティの重心がアプリからカーネルへ下がるということです。かつては観測のためにアプリごとに計装を埋め込み、ポッドごとにサイドカーを付けました。今はカーネルという共有地点に一度フックを掛けて、その上で何が動いていても見張ります。

この移行が魅力的な理由は明確です。言語に依存せず、コード修正が要らず、オーバーヘッドが小さく、回避しにくい。Ciliumがサービスメッシュを、Falcoがランタイムセキュリティを、Pixieが自動観測を、bpftraceがその場のデバッグを、それぞれのやり方で再定義しているのもこのためです。

同時に冷静さも必要です。eBPFはLinuxに縛られており、検証器は気難しく、カーネルが見られるものには意味論的な限界があります。「eBPFがオブザーバビリティを飲み込む」という言葉は誇張ではありませんが、それがすべてを飲み込むという意味ではありません。カーネルがよく見える低レベルの世界でeBPFは圧倒的で、その上の高レベルの世界では依然として他のツールと共存します。

次にオブザーバビリティのスタックを設計することがあれば、アプリに何かをもう一つ付ける前に、こう問いかけてみてください。「これはカーネルからすでに見えるものではないか?」 意外にも多くの場合、答えは「そうだ」です。

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます