eBPF によるゼロインストルメンテーション可観測性
アプリを1行も改変せず、L7メトリクスやCPUプロファイルを採取できる理由を原理から理解。カーネル内VMでフックを安全に実行するeBPFの仕組みと、TLS暗号化やコンテキスト欠落といった本質的な限界まで押さえられます。
- 1.eBPFはカーネル内のサンドボックス化されたVMで、kprobe/uprobe/tracepointにアタッチしたプログラムを実行する。ロード前に検証器(verifier)が停止性とメモリ安全性を静的に証明するため、カーネルをクラッシュさせずにフックできる。
- 2.ゼロインストルメンテーションが成立するのは、計測点がアプリのコードではなくカーネルのsyscall/ネットワーク経路にあるから。socketやtracepointを観測すれば、アプリ無改変でL7メトリクスを再構成できる。
- 3.限界は明確。TLSで暗号化されたペイロードはuprobeでライブラリ関数を直接フックしない限り読めず、syscall境界からはtrace_idなどのアプリ文脈を復元できない。
可観測性の常識は「計測したければアプリにSDKを埋め込む(instrument する)」でした(/devops/observability/)。eBPF はこの前提を覆し、アプリを1行も改変せずにL7メトリクスやプロファイルを採る道を開きます。本稿はその仕組みと、無視できない限界を原理から解きます。
eBPF とは:カーネル内の安全な実行基盤
eBPF(extended Berkeley Packet Filter)は、カーネル空間で動く小さな仮想マシン(VM)にユーザー定義のプログラムを安全にロードして実行する仕組みです。元はパケットフィルタリング用でしたが、現在は汎用のカーネルフックとして使われます。
ユーザーは制限付きC等でプログラムを書き、それをeBPFバイトコードにコンパイルして bpf() システムコールでカーネルにロードします。カーネルはこれを JITコンパイルしてネイティブ命令に変換し、特定のイベント発生時に実行します。重要なのは、カーネルモジュールのように再コンパイルや再起動を要さず、稼働中のカーネルへ動的にコードを注入できる点です。
[ユーザー空間] C風コード → eBPFバイトコード
│ bpf() syscall でロード
▼
[カーネル] verifier で静的検証 → JIT でネイティブ化
│ kprobe/uprobe/tracepoint にアタッチ
▼
イベント発火時に実行 → map 経由でユーザー空間へ
なぜカーネルをクラッシュさせないのか:verifier
任意コードをカーネルで動かせば普通はシステム全体を巻き込んで落ちます。eBPF がこれを避ける核心が verifier(検証器)です。ロード時に、verifier はプログラムの全実行経路を有向グラフとして走査し、次の性質を静的に証明します。
- 停止性:無限ループを禁じる。後方ジャンプは検証可能な有界ループに限られ、命令数・複雑度に上限がある。だから「必ず終わる」ことが保証される。
- メモリ安全性:すべてのポインタアクセスについて、参照先と境界を追跡し、範囲外参照を拒否する。任意アドレスの読み書きはできない。
- 型・初期化の健全性:未初期化レジスタの読み出しや、許可されていないヘルパー関数の呼び出しを弾く。
verifier は保守的です。安全だと証明しきれないプログラムは、実際には安全でもロードを拒否します。だから上級者がeBPFを書くときの戦いの多くは「ロジックのバグ」ではなく「verifier を納得させる」ことになります。停止性とメモリ安全性をコンパイル時に強制するこの仕組みこそ、本番カーネルに任意コードを注入できる前提です。
3種類のフック点:kprobe / uprobe / tracepoint
eBPF プログラムは「いつ走るか」を**アタッチ先(フック)**で決めます。観測の文脈で要となるのが次の3つです。
| フック | アタッチ対象 | 安定性 | 観測できるもの |
|---|---|---|---|
| kprobe | 任意のカーネル関数の入口/出口 | 不安定(関数名はカーネル版で変わる) | syscall実装・ネットワークスタック内部 |
| tracepoint | カーネルが明示的に埋めた静的計測点 | 安定(ABIとして維持される) | sched/syscall/blockなど定義済みイベント |
| uprobe | ユーザー空間バイナリの任意関数 | シンボル依存 | アプリ/ライブラリ関数(例: TLS関数) |
実務上は tracepoint を優先します。kprobe は任意のカーネル関数にフックできて強力ですが、関数名やシグネチャはカーネルのバージョンで変わるため移植性が低い。tracepoint はカーネルが安定ABIとして維持するので、版を超えて壊れにくい。uprobe はユーザー空間関数を狙えるため、後述のTLS可視化で決定的な役割を果たします。
ゼロインストルメンテーションが成立する原理
「アプリ無改変でL7メトリクスが採れる」のはなぜか。答えは、計測点をアプリのコードではなく、アプリが必ず通るカーネル境界に置くからです。
HTTPリクエストを処理するアプリは、最終的に必ず read() / write()(あるいは sendmsg() / recvmsg())といったsyscallでソケットへ読み書きします。このsyscall境界に tracepoint や kprobe でフックすれば、アプリが何のフレームワークで書かれていようと、流れるバイト列とタイミングを横取りできます。socket層に近い sk_msg などのフックを使えば、TCPストリームを再構成してHTTP/gRPCのリクエスト/レスポンスを境界で切り出し、レイテンシ・ステータス・スループットを算出できます。
アプリ(無改変)
└─ write(fd, "GET /cart HTTP/1.1 ...") ← syscall
│ ここに tracepoint/kprobe をアタッチ
▼
eBPF: バイト列を観測 → HTTPメソッド/パス/タイミングを抽出
│ perf/ring buffer 経由
▼
ユーザー空間エージェント: L7メトリクスとして集計
同じ発想は CPUプロファイリングにも効きます。perf_event を周期的サンプリング源としてeBPFをアタッチし、サンプルごとにカーネル/ユーザー両方のスタックを採取してスタックトレースの出現回数をmap上で集計すれば、アプリに何も仕込まずに常時プロファイル(continuous profiling)が作れます。これがフレームグラフの母数になります。
eBPFプログラムは状態を map(ハッシュ・配列・ring bufferなどのカーネル管理データ構造)に書きます。ユーザー空間のエージェントはこのmapを読み、集計やエクスポートを行います。eBPF側は「観測してmapに足す」だけの軽量処理に徹し、重い集計はユーザー空間に逃がす——この役割分担が、ホットパスへのオーバーヘッドを小さく保つ鍵です。
限界:原理から導かれる「採れないもの」
ゼロインストルメンテーションは万能ではありません。限界は実装の未熟さではなく、観測点がカーネル境界にあること自体から導かれます。
1. TLS暗号化ペイロード。 syscall境界(write/read)を流れるのは、TLS終端後の暗号化済みバイト列です。よってsyscallフックだけではHTTPの中身を読めません。回避策は uprobe を SSL_write / SSL_read(OpenSSL等)に直接アタッチし、暗号化される前/復号された後の平文を捉えること。ただしこれはライブラリのシンボルに依存し、静的リンクやライブラリ差し替えで容易に壊れ、もはや「完全無依存」とは言えません。
2. アプリ文脈の欠落。 syscall層から見えるのはバイト列であり、trace_id や user_id といったアプリ内部の意味は復元できません。eBPFは個々のsyscallやsocketは追えても、それらを「同じ論理リクエスト」へ束ねる文脈伝播を知りません(/devops/tracing-context-propagation/)。HTTPヘッダにIDが載っていれば抽出できますが、それは結局アプリ側の規約に依存します。
3. 因果の組み立てが困難。 L7メトリクスは境界で再構成できても、サービスをまたいだスパンの親子関係を自動で正しく繋ぐのは別問題です。分散トレースが求める因果順序(/devops/distributed-tracing/)は、結局どこかでアプリ側の文脈伝播を要します。
eBPFはインストルメンテーションを消し去るのではなく、計装の責任をアプリからカーネル/プラットフォームへ移す技術です。だから「アプリ無改変」と引き換えに、アプリしか知らない高位の意味(リクエストID・ビジネス属性)は原理的に採りにくくなります。多くの可観測性の難所が共有キーの欠落に帰着するのと同根です(/devops/observability-pillars-internals/)。
L7可視化はサービスメッシュのサイドカープロキシでも実現できます(/devops/service-mesh/)。違いは費用構造です。サイドカーは追加プロセスとプロキシのホップを足す(メモリ・遅延の固定費)のに対し、eBPFはカーネル内でホットパスに薄く相乗りするためプロセス追加もホップ増もありません。代わりに特権(CAP_BPF等)と新しめのカーネル、そしてTLS可視化の難しさを引き受けます。
まとめ
- eBPF はカーネル内のサンドボックスVMで、
bpf()でロードしたプログラムをkprobe/uprobe/tracepointにアタッチして実行する。verifier が停止性とメモリ安全性を静的に証明するから、本番カーネルに任意コードを注入してもクラッシュしない。 - ゼロインストルメンテーションが成立するのは、計測点をアプリではなくアプリが必ず通るsyscall/socket境界に置くから。バイト列とタイミングを横取りしてL7メトリクスを再構成し、
perf_eventサンプリングで常時プロファイルも作れる。 - 限界は原理由来。TLS平文は uprobe でライブラリ関数を直接フックしない限り読めず、syscall境界からは
trace_idなどのアプリ文脈や因果関係を復元できない。 - eBPFは計装を消すのではなく、その責任をアプリからカーネル/プラットフォームへ移す技術。サイドカー(/devops/service-mesh/)との費用構造の違いを踏まえ、文脈が要る領域はアプリ側の伝播と併用するのが現実解。
DevOps/インフラ Article
eBPF によるゼロインストルメンテーション可観測性を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
eBPF
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 5
導入後に効く点
ゼロインストルメンテーションが成立するのは、計測点がアプリのコードではなくカーネルのsyscall/ネットワーク経路にあるから。socketやtracepointを観測すれば、アプリ無改変でL7メトリクスを再構成できる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「eBPF / オブザーバビリティ」に近いか確認する。
- 強みである「eBPFはカーネル内のサンドボックス化されたVMで、kprobe/uprobe/tracepointにアタッチしたプログラムを実行する。ロード前に検証器(verifier)が停止性とメモリ安全性を静的に証明するため、カーネルをクラッシュさせずにフックできる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。