コンテンツにスキップ

SREのためのeBPF:本番プロファイリングガイド

· 13 min read · default
ebpfperformanceprofilingsrelinuxobservabilitymonitoring-&-observability

はじめに

本番プロファイリングは常に勇敢な者の領域でした。何年もの間、SREは不快なトレードオフに直面してきました:測定可能なレイテンシを導入し本番ワークロードを不安定にするリスクがある重量級のプロファイリングツールをアタッチするか、症状を表示するが根本原因を決して示さないメトリクスダッシュボードに頼って盲目的に運用するかです。メインストリームのLinuxカーネル技術としてのeBPFの登場は、この計算を根本的に変えました。eBPFを使用すれば、無視できるオーバーヘッドでカーネルとユーザースペースのほぼすべてのレイヤーを計装でき、以前はステージング環境でのみ利用可能だった種類の深いオブザーバビリティデータを生成できます。

eBPF(extended Berkeley Packet Filterの略)は、ネットワークパケットフィルタリングメカニズムとしての起源から、汎用のカーネル内仮想マシンへと進化しました。eBPF用に書かれたプログラムは実行前にカーネルによって検証され、システムをクラッシュさせたり、無限ループに入ったり、不正なメモリにアクセスしたりできないことが保証されます。この安全性の保証が、eBPFを本番プロファイリングに独自に適したものにしています。カーネルを再コンパイルしたりサービスを再起動したりすることなく、カーネル関数、tracepoint、ユーザースペース関数エントリ、ハードウェアパフォーマンスカウンタにプローブをアタッチできます。

このガイドでは、SREチームが本番Linuxシステムのパフォーマンス問題を診断するために毎日使用している実践的なワークフローを扱います。標準のeBPFツールチェーンを使用して、CPUプロファイリング、レイテンシ分析、メモリリーク検出、ネットワークトレーシング、I/Oプロファイリングを説明します。ここに示すすべてのコマンドとスクリプトは、カーネル5.15以降を実行する本番環境で使用されてきました。

eBPFが本番プロファイリングを変える理由

従来のプロファイリングツールは、本番での使用を非実用的にするコストを課します。ビジーなサービスでstraceを実行すると、straceがptrace を使用してすべてのsyscallをインターセプトし、各呼び出しごとにトレース対象プロセスとトレースプロセス間でコンテキストスイッチを行うため、100倍以上遅くなる可能性があります。ハードウェアパフォーマンスカウンタを使用しstraceよりはるかに軽量なperfでさえ、サンプルデータのディスクへの書き込みが必要で、高いサンプルレートでは相当なI/O圧力を生成する可能性があります。

eBPFは3つの重要な方法で方程式を変えます。第一に、eBPFプログラムはカーネル内部で実行され、ptraceベースのツールのコンテキストスイッチオーバーヘッドを排除します。kprobeやtracepointをアタッチすると、eBPFプログラムはカーネルコードパスとインラインで実行され、通常は呼び出しあたり数十ナノ秒しか追加しません。第二に、eBPFプログラムはmapを使用してカーネル内でデータを集約でき、すべてのイベントをユーザースペースにコピーすることなくヒストグラム、カウント、サマリーを計算できます。straceで毎秒数百万のイベントを生成するレイテンシヒストグラムが、eBPFでは間隔あたり1回のmap読み取りしか生成しません。第三に、eBPFベリファイアが安全性を保証します:プログラムがnullポインタのデリファレンス、境界外メモリへのアクセス、無限ループの実行を行うことはできません。

実際の影響は劇的です。biolatencyのようなツールは、数十万IOPSを処理するシステム上のすべてのブロックI/Oリクエストをトレースし、1%未満のCPUオーバーヘッドでレイテンシヒストグラムを生成できます。ピークトラフィックを提供しながら、アプリケーションサーバーのホット関数に対してfunclatencyを実行できます。これは以前の世代のトレーシングツールでは単純に不可能でした。

eBPFツールチェーンのセットアップ

eBPFエコシステムは3つの主要なツールセットに統合されています:bcc-tools、bpftrace、libbpfベースのCO-REプログラム。それぞれ異なるユースケースに対応しており、十分に装備されたSREワークステーションには3つすべてが利用可能であるべきです。

UbuntuおよびDebianシステムでは、完全なツールチェーンをインストールします:

sudo apt-get update
sudo apt-get install -y bpfcc-tools bpftrace linux-headers-$(uname -r)
sudo apt-get install -y libbpf-dev bpftool

RHELおよびFedoraシステムでは:

sudo dnf install -y bcc-tools bpftrace kernel-devel-$(uname -r)
sudo dnf install -y libbpf-devel bpftool

カーネルがモダンなbpftrace機能とCO-REプログラムに必要なBTFをサポートしていることを確認します:

ls /sys/kernel/btf/vmlinux
bpftool btf dump file /sys/kernel/btf/vmlinux format raw | head -c 100

BTFが利用できない場合、カーネルがCONFIG_DEBUG_INFO_BTF=yでコンパイルされていることを確認する必要があります。2022年以降のほとんどのディストリビューションカーネルにはBTFサポートが含まれています。

bcc-toolsパッケージは、最も一般的なプロファイリングシナリオをカバーする数十の即使用可能なツールを提供します。これらは通常、Debianベースのシステムでは-bpfccサフィックスが付いた実行ファイルとして、RHELでは/usr/share/bcc/tools/の下に直接インストールされます。Bpftraceは、カスタムワンライナーや短いスクリプトを書くための高水準スクリプト言語を提供します。Libbpfおよび CO-RE(Compile Once, Run Everywhere)プログラムは、自己完結型バイナリとして配布されるプロダクショングレードのポータブルeBPFツールの構築に使用されます。

CPUプロファイリングワークフロー

CPUプロファイリングは、パフォーマンス問題を調査する際の最も一般的な出発点です。目標は、カーネルスペースでもユーザースペースでも、どの関数が最もCPU時間を消費しているかを特定し、ホットスポットを視覚的に明確にするフレームグラフを生成することです。

最もシンプルなアプローチは、bccのprofileツールを使用して固定周波数でスタックトレースをサンプリングすることです:

sudo profile-bpfcc -F 99 -a --stack-storage-size 16384 30 > /tmp/cpu-stacks.txt

これは30秒間すべてのCPUを99 Hzでサンプリングします。99 Hzの周波数は、100 Hzで頻繁に実行されるタイマーベースのアクティビティとのエイリアシングを回避します。出力にはBrendan GreggのFlameGraphツールに直接入力できるフォールドされたスタックトレースが含まれます:

git clone https://github.com/brendangregg/FlameGraph.git
cat /tmp/cpu-stacks.txt | FlameGraph/stackcollapse-bpf.pl | FlameGraph/flamegraph.pl > cpu-flame.svg

よりターゲットを絞ったプロファイリングには、bpftraceを使用して特定のプロセスをプロファイリングし、CPUでフィルタリングできます:

sudo bpftrace -e 'profile:hz:99 /pid == 12345/ { @[ustack(perf), comm] = count(); }' > stacks.bt

実行時間ではなくCPUスケジューリング動作を理解する必要がある場合、cpudistツールはスレッドがデスケジュールされる前にCPU上で実行される時間を示します:

sudo cpudist-bpfcc -p 12345 10 1

これはプロセス12345の10秒間のon-CPU持続時間のヒストグラムを出力します。高いコンテキストスイッチと組み合わされた短いon-CPU時間はロックコンテンションを示唆します。低いスループットと組み合わされた長いon-CPU時間は計算上のボトルネックを示唆します。

NUMAシステムでのCPUマイグレーション問題を調査するために、スケジューライベントをトレースできます:

sudo bpftrace -e 'tracepoint:sched:sched_migrate_task {
    printf("pid=%d comm=%s from_cpu=%d to_cpu=%d\n",
        args->pid, args->comm, args->orig_cpu, args->dest_cpu);
}'

レイテンシ分析

レイテンシ分析はeBPFが真に輝く分野です。測定されるコードパスを撹乱することなく、任意のイベント間の時間を測定できるからです。bcc-toolsコレクションには、専用に構築されたいくつかのレイテンシツールが含まれています。

ブロックI/Oレイテンシは、ブロックI/Oリクエストから完了までの時間をトレースするbiolatencyで測定されます:

sudo biolatency-bpfcc -D 10 1

-Dフラグはレイテンシをディスクデバイスごとに分類し、どのドライブが遅いかを容易に特定できます。出力はマイクロ秒単位のレイテンシ分布を示す2のべき乗ヒストグラムです。

実行キューレイテンシは、スレッドがCPU時間を得る前にスケジューラキューで待機する時間を測定し、runqlatで測定されます:

sudo runqlat-bpfcc -p 12345 10 1

高い実行キューレイテンシは、プロセスがCPUを待っていることを意味し、CPU飽和を示します。通常の運用中に10ミリ秒を超えるレイテンシが見られる場合、CPU容量を増やすか、何がCPUを消費しているかを調査する必要があります。

関数レイテンシは、特定のカーネルまたはユーザースペース関数の実行時間を測定します:

sudo funclatency-bpfcc -p 12345 'c:malloc' 10 1

これはプロセス12345のlibcでのmalloc呼び出しをトレースし、レイテンシヒストグラムを表示します。バイナリまたは共有ライブラリにシンボルがある任意の関数をトレースできます。カーネル関数の場合:

sudo funclatency-bpfcc 'vfs_read' 10 1

bpftraceを使用したアプリケーションレベルのレイテンシトレーシングでは、2つのプローブポイント間の時間を測定できます:

sudo bpftrace -e '
uprobe:/usr/bin/myapp:process_request { @start[tid] = nsecs; }
uretprobe:/usr/bin/myapp:process_request /@start[tid]/ {
    @latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
END { clear(@start); }
'

メモリ分析

本番でのメモリ問題は、数日かけてOOMキルを引き起こす段階的なリークから、パフォーマンスを低下させるキャッシュ非効率まで多岐にわたります。eBPFは各カテゴリに対して複数のツールを提供します。

memleakツールはメモリアロケーションとフリーの呼び出しをトレースし、リークを特定するために未解放のアロケーションを追跡します:

sudo memleak-bpfcc -p 12345 --combined-only 30

これはプロセス12345にアタッチし、30秒後に解放されなかったアロケーションのスタックトレースを、未解放バイトの合計順にソートして表示します。専門的なアロケータデバッグを有効にして再起動することなく、長時間実行サービスでのリーク検出に非常に有用です。

カーネルメモリアロケーションの追跡の場合:

sudo memleak-bpfcc --combined-only 30

pidフラグなしで、memleakはkmallocとkfreeを介してカーネルアロケーションをトレースし、ドライバーやカーネルモジュールによるカーネルメモリリークを特定できます。

キャッシュ動作はアプリケーションパフォーマンスに甚大な影響を与えます。cachestatツールはページキャッシュに関する秒単位の統計を提供します:

sudo cachestat-bpfcc 5

出力にはヒット、ミス、ダーティページ、読み取り/書き込み比率が含まれます。高いミス率はワーキングセットが利用可能なメモリを超えていることを示し、アプリケーションがより多くのRAMを必要としているか、より良いアクセスパターンが必要かを検討すべきです。

OOMキルはリアルタイムでトレースして、どのプロセスがキルされなぜキルされるかを理解できます:

sudo bpftrace -e 'kprobe:oom_kill_process {
    printf("OOM kill: pid=%d comm=%s pages=%d\n",
        ((struct task_struct *)arg1)->pid,
        ((struct task_struct *)arg1)->comm,
        arg0);
}'

よりシンプルなアプローチとして、bccのoomkillツールがこれを自動的にキャプチャします:

sudo oomkill-bpfcc

これはOOMキラーが呼び出されるたびに、トリガーしたプロセスとキルされたプロセスの詳細を含む行を出力します。

ネットワークトレーシング

ネットワークパフォーマンスの問題は、アプリケーション、カーネルTCPスタック、ネットワークインフラストラクチャ間の相互作用を含むため、診断が非常に困難であることで知られています。eBPFは各レイヤーに対する精密なツールを提供します。

tcplifeツールはTCPセッションのライフタイムをトレースし、接続がいつ確立され閉じられたか、転送されたバイト数を表示します:

sudo tcplife-bpfcc -D

-Dフラグはタイムスタンプを含みます。各行にはPID、プロセス名、ローカルおよびリモートアドレス、ポート、持続時間、送受信バイトが表示されます。接続のチャーン、予想外の短命接続、予想より長く接続を保持しているサービスの特定に不可欠です。

TCP再送はネットワーク健全性の重要な指標です:

sudo tcpretrans-bpfcc -l

-lフラグはtail loss probeを含みます。各再送イベントは、ソースおよびデスティネーションアドレス、状態、再送をトリガーしたカーネル関数とともに出力されます。特定のデスティネーションへの再送のクラスターはネットワークパスの問題を示します。

TCP層でドロップされたパケットはtcpdropでトレースできます:

sudo tcpdrop-bpfcc

ドロップされた各パケットはカーネルスタックトレースとともに出力され、カーネルがなぜパケットをドロップしたかを正確に示します。一般的な原因にはソケットバッファオーバーフロー、接続リセット、メモリ圧力が含まれます。

詳細なTCP接続状態分析には、状態遷移をトレースするbpftraceスクリプトを書くことができます:

sudo bpftrace -e '
tracepoint:tcp:tcp_set_state {
    printf("pid=%d sport=%d dport=%d oldstate=%d newstate=%d\n",
        pid, args->sport, args->dport, args->oldstate, args->newstate);
}
'

DNS解決レイテンシは、見過ごされがちなアプリケーションレイテンシの原因です。リゾルバレベルでDNSルックアップをトレースできます:

sudo bpftrace -e '
uprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo { @start[tid] = nsecs; }
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo /@start[tid]/ {
    @dns_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
'

I/Oプロファイリング

ストレージI/Oは本番レイテンシの最も一般的な原因の1つであり、eBPFはストレージスタックの複数のレベルでI/Oをトレースするツールを提供します。

biosnoopツールは、タイムスタンプ、レイテンシ、プロセス帰属とともに個々のブロックI/O操作をトレースします:

sudo biosnoop-bpfcc -d sda 10

これは10秒間sdaデバイスへのすべてのブロックI/Oをトレースし、各操作のPID、プロセス名、ディスク、操作タイプ、セクタ、バイト、レイテンシを表示します。特定のディスクで何がI/Oを生成しているかを正確に理解する必要がある場合に最初に使うツールです。

ファイルシステムレベルのトレーシングはより高いレベルのコンテキストを提供します。ext4slowerツールはレイテンシしきい値を超えるext4操作をトレースします:

sudo ext4slower-bpfcc 10

これは読み取り、書き込み、オープン、syncを含む10ミリ秒より遅いすべてのext4操作を出力します。ブロックレベルトレースとファイルシステムメタデータを相関させることなく、遅いファイルシステム操作を即座に特定します。

すべてのファイルシステムタイプにわたる一般的なファイルシステムトレーシングには、fileslowerが同じ目的を果たします:

sudo fileslower-bpfcc 10

書き込みパターンは、アプリケーションがストレージとどのように相互作用するかを理解するために重要です。filetopに相当するbpftraceは、最も頻繁に読み書きされているファイルを示します:

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_write {
    @writes[comm, pid] = count();
}
interval:s:5 { print(@writes); clear(@writes); }
'

データベースのようなfsyncヘビーなワークロードの場合、sync操作のトレースは予期しないフラッシュパターンを明らかにできます:

sudo bpftrace -e '
kprobe:vfs_fsync_range {
    @fsync_latency[comm] = count();
}
kretprobe:vfs_fsync_range {
    printf("fsync completed: comm=%s\n", comm);
}
'

カスタムbpftraceワンライナーの構築

eBPFの真の力は、特定のスタックに合わせたカスタムトレーシングプログラムを書く能力にあります。bpftraceのスクリプティング言語は、シェルスクリプトを書ける人なら誰でもこれをアクセス可能にします。

bpftraceワンライナーの基本構造は、プローブ仕様の後にアクションブロックが続くものです。プローブはkprobe(カーネル関数)、uprobe(ユーザースペース関数)、tracepoint(安定したカーネルイベント)、ソフトウェアイベントにアタッチできます。

プロセスごとのsyscallカウント:

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

プロセスごとの読み取りサイズヒストグラム:

sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ {
    @read_bytes[comm] = hist(args->ret);
}'

完全なコマンドラインでの新しいプロセス作成のトレース:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
    printf("exec: pid=%d ppid=%d %s\n", pid, curtask->real_parent->pid, str(args->filename));
}'

完全なパスでのファイルオープンのトレース:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
    printf("open: pid=%d comm=%s file=%s\n", pid, comm, str(args->filename));
}'

コールスタックごとに集約して、アプリケーションが特定の関数で費やす時間を測定:

sudo bpftrace -e '
uprobe:/opt/myapp/bin/server:DatabaseQuery { @start[tid] = nsecs; }
uretprobe:/opt/myapp/bin/server:DatabaseQuery /@start[tid]/ {
    @query_ns[ustack(5)] = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
'

高頻度イベントの場合、mapを使用してカーネル内で集約し、出力バッファを圧倒しないようにします:

sudo bpftrace -e '
tracepoint:syscalls:sys_exit_read /args->ret > 0/ {
    @total_bytes[comm] = sum(args->ret);
    @total_calls[comm] = count();
}
interval:s:10 {
    printf("\n--- Top readers (10s window) ---\n");
    print(@total_bytes, 10);
    print(@total_calls, 10);
    clear(@total_bytes);
    clear(@total_calls);
}
'

継続的プロファイリングの統合

bpftraceとbcc-toolsによるアドホックプロファイリングはインシデント対応に不可欠ですが、継続的プロファイリングは常時オンのパフォーマンス可視性を提供します。この分野をリードする2つのオープンソースプロジェクトがあります:ParcaとPyroscopeです。

Parcaは、eBPFを使用して、最小のオーバーヘッドでホスト上のすべてのプロセスにわたるスタックトレースを継続的にサンプリングします。ParcaエージェントはKubernetesのDaemonSetまたはベアメタルのsystemdサービスとして実行されます:

sudo parca-agent --remote-store-address=parca-server:7070 \
  --node=production-host-01 \
  --sampling-ratio=1.0 \
  --http-address=:7071

エージェントはすべての実行中プロセスを自動的に検出し、デバッグ情報とBTFデータからシンボルを解決し、プロファイリングデータをParcaサーバーにストリーミングします。サーバーは時系列クエリに最適化されたカラムナフォーマットでプロファイルを保存し、フレームグラフの探索、時間ウィンドウ間のプロファイル比較、リグレッションの特定のためのUIを提供します。

Pyroscopeは同様のアプローチをとりますが、CPU以外の追加プロファイリングモード(メモリアロケーションプロファイリング、ロックコンテンションプロファイリング、Goアプリケーション用のgoroutineプロファイリング)をサポートします:

# pyroscope-agent.yaml
server-address: http://pyroscope-server:4040
log-level: info
targets:
  - service-name: api-server
    spy-name: ebpfspy
    application-name: api-server
  - service-name: worker
    spy-name: ebpfspy
    application-name: worker-pool

両方のツールはダッシュボード作成とアラートのためにGrafanaと統合します。典型的な継続的プロファイリングセットアップには以下が含まれます:

# Grafana data source configuration for Parca
curl -X POST http://grafana:3000/api/datasources \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Parca",
    "type": "parca",
    "url": "http://parca-server:7070",
    "access": "proxy"
  }'

継続的プロファイリングの真の価値は時間とともに現れます。パフォーマンスリグレッションがデプロイされた場合、現在のプロファイルをデプロイ前のベースラインと比較し、どの関数がより多くのCPUを消費しているかを即座に確認できます。これはパフォーマンスデバッグをリアクティブな調査からプロアクティブな検出システムに変換します。

本番でのベストプラクティスと安全性

eBPFの安全性保証は、任意のeBPFプログラムを考えなしに本番システムで実行できることを意味しません。ベリファイアがカーネルクラッシュを防ぎますが、不適切に書かれたeBPFプログラムは依然として過剰なCPU消費、圧倒的な出力の生成、システムパフォーマンスへの干渉を引き起こす可能性があります。

bpftraceとbcc-toolsには常に時間制限を設定してください。すべてのプロファイリングセッションには明示的な持続時間が必要です:

# 良い:明示的な30秒の持続時間
sudo biolatency-bpfcc 30 1

# 危険:手動で停止するまで実行
sudo biolatency-bpfcc

高頻度プローブには注意してください。毎秒数百万回発火する関数にkprobeをアタッチすると、eBPFプログラム自体が些細であっても測定可能なオーバーヘッドが追加されます。本番でプローブをアタッチする前に、頻度を推定してください:

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

関数が毎秒数十万回以上発火する場合、kprobeの代わりにtracepointを使用できるか、処理されるイベント数を減らすフィルタを追加できるか、すべてのイベントをトレースする代わりにサンプリングできるかを検討してください。

実行前にスクリプトを検証するためにbpftraceの--dry-runフラグを使用してください:

sudo bpftrace --dry-run -e 'kprobe:vfs_read { @[comm] = count(); }'

無制限のメモリ増加を防ぐためにmapサイズを制限してください:

sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }' --map-max-entries=10000

本番システムでは、事前承認されたeBPFツールとスクリプトのランブックを確立してください。bcc-toolsコレクションは十分にテストされており、本番での使用に安全です。カスタムbpftraceスクリプトは、本番使用前にチームでレビューし、ステージングでテストする必要があります。

完全なroot権限の代わりに、適切なcapabilityを持つコンテナ内でeBPFツールを実行することを検討してください:

docker run --rm -it --privileged \
  -v /sys/kernel/debug:/sys/kernel/debug:ro \
  -v /sys/kernel/btf:/sys/kernel/btf:ro \
  -v /proc:/proc:ro \
  quay.io/iovisor/bpftrace:latest \
  bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

最後に、プロファイリングワークフローを文書化してください。午前3時にインシデントが発生した場合、bpftraceスクリプトをゼロから書きたくはないでしょう。症状別に整理されたテスト済みプロファイリングスクリプトのリポジトリを維持してください:高CPU、高レイテンシ、メモリ増加、ネットワークエラー、I/O飽和。各スクリプトには何を測定するか、予想されるオーバーヘッド、出力の解釈方法を説明するコメントを含めるべきです。eBPFは本番でスーパーパワーを与えてくれますが、緊急事態が到来する前にその使用を練習していた場合に限ります。