インフラ・DevOps
eBPFコードのプロファイリング方法
How Do I Profile eBPF Code? (naveensrinivasan.com)
要約
この記事は、eBPFコードのパフォーマンスを測定し、ボトルネックを特定するための実践的なガイドです。ファイルオープン操作を例に、eBPFフックのオーバーヘッドを計測するC言語のテストハーネスの作成方法、`perf`ツールでシンボル解決を有効にするためのカーネル設定、そして`perf record`と`perf report`を用いた計測と分析の手順を解説しています。これにより、eBPFコードがシステムパフォーマンスに与える影響を明確に把握し、最適化箇所を特定することが可能になります。
全文翻訳
eBPFコードのプロファイリング方法
eBPFワークロードを実行したり、eBPFコードを記述したりする場合、そのパフォーマンスへの影響を測定したいと考えます。この記事では、その方法の例を示します。
この例では、OSの最も重要な機能の1つであるファイルオープン操作のパフォーマンスを測定することを目標とします。私たちのコードはeBPFのファイルオープンフックを使用しており、このフックを追加することによるパフォーマンスオーバーヘッドを測定したいと考えていました。
潜在的なボトルネックを特定するために、依存関係を最小限にしたシンプルなC言語のテストハーネスが必要です。これはファイルオープンパフォーマンスを測定するように設計されています。
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <sys/mman.h>
#include <sys/syscall.h>
static inline uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts); /* VDSO, no syscall */
return (uint64_t)ts.tv_sec * 1000000000ull + ts.tv_nsec;
}
int main(int argc, char **argv) {
const char *path = argv[1];
uint64_t n = strtoull(argv[2], NULL, 10);
uint64_t warm = n / 10; /* preallocate + prefault + lock: no page faults in the loop */
uint32_t *d = mmap(NULL, n * sizeof(uint32_t), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0);
mlock(d, n * sizeof(uint32_t));
for (uint64_t i = 0; i < n; i++) d[i] = 0; /* fault everything in */
for (uint64_t i = 0; i < n; i++) {
uint64_t t0 = now_ns();
long fd = syscall(SYS_openat, AT_FDCWD, path, O_RDONLY); /* raw, no libc wrapper */
uint64_t t1 = now_ns();
if (fd >= 0) close(fd);
d[i] = (uint32_t)(t1 - t0);
}
/* dump after the loop only */
for (uint64_t i = warm; i < n; i++) printf("%u\n", d[i]);
return 0;
}
上記のコード例の目標は、シンプルに保ち、同じファイルをウォームキャッシュ条件で再オープンすることです。これにより、無関係なファイルシステムやディスクI/Oの変動を最小限に抑え、p50/p99の特定に役立ちます。コードはlibcのopenat()ラッパーではなくsyscall(SYS_openat, …)を呼び出し、結果の最初の10%はウォームアップ期間として破棄されます。
このテストハーネスは、ファイルをx回オープンするのにかかった時間の結果を生成します。そのため、eBPFフックがアタッチされる前後の測定に使用できます。
セットアップ
eBPFコードをプロファイリングする際、`perf`ツールがシンボルを解決できるようにしたいと考えます。これにより、コードのどこに問題があるかを分析できます。そのためには、これらのコマンドを実行する必要があります。
sudo sysctl -w net.core.bpf_jit_enable=1
sudo sysctl -w net.core.bpf_jit_kallsyms=1
上記のコマンドはJITを有効にし、JITコンパイルされたBPFシンボルを公開するため、`perf report`は未知のアドレスの代わりにプログラム名を表示できます。
シンボルが`perf`ツールに表示されるかどうかを確認するには、eBPFコードを実行し、次のようなコマンドを使用します。
sudo bpftool prog show | rg -A4 ' lsm '
sudo rg 'bpf_prog_[0-9a-f]+_ '/proc/kallsyms | rg 'security|path|file|open'
上記の`rg`コマンドでは、LSMフックを測定しているため、lsmをチェックしています。また、カスタムカーネルバージョンを使用しているため、そのカーネルバージョンの`perf`は標準パスにありません。例では次のようにインストールしています。
PERF=/usr/lib/linux-tools/6.8.0-134-generic/perf
計測
必要なツールがすべてセットアップされたので、最初のステップはeBPFコードを実行せずに測定することです。ここで、上記のCコードが役立ちます。ファイルを/etc/hostnameオープンする際の測定を行い、結果をファイルにパイプしてp50/p99を計算できるようにします。
sudo taskset -c 3 chrt -f 99 ./bench /etc/hostname 100000 > /tmp/samples.txt
`taskset -c 3`は実行をCPU 3に固定し、CPU移行ノイズを低減します。`chrt -f 99`はベンチマークに非常に高いCPU優先度を与えます。これはほとんどすべての通常のプログラムの前に実行され、完了、ブロック、または中断されるまで実行され続けます。Cコードは最初の10%を破棄します。ファイルには90,000のsamples.txtが含まれているはずです。
次に、eBPFコードを実行し、次のようなものを実行します。
sudo $PERF record \
-g \
--call-graph fp \
-e cycles:k \
-F 997 \
-o ~/perf.data \
-- \
taskset -c 3 \
chrt -f 99 \
./bench /etc/hostname 200000 \
> /tmp/samples.txt
`-g`はコールスタックを記録し、`--call-graph fp`はフレームポインタを使用してスタックをアンワインドします。`-e cycles:k`はカーネルモードのみでCPUサイクルをサンプリングします。これには、システムコール、VFS、LSM、およびeBPFの実行が含まれ、ユーザー空間ベンチマーク作業は含まれません。`-F 997`は1秒あたり997サンプルを要求し、丸められていない周波数は周期的なアライメントを避けるのに役立ちます。
上記の実行後、データをソートするためにこのコマンドを実行します。
sudo "$PERF" report -i ~/perf.data --stdio --sort comm,dso,symbol > perf.txt
同じ`perf.data`のフレイムグラフは、Infernoを使用して生成されます。興味のあるスタックは`bpf_lsm_file_open`とその上のすべてです。
以下は`perf.txt`からの例出力です。これは、パフォーマンスのボトルネックが`bpf_lsm_file_open`とそのテールコールに費やされている時間であることを示しています。これはホットパスであることが判明したため、割り当てからCPUサイクルを削るあらゆる試みがシステムのパフォーマンスに大きな影響を与えることになります。
| | | | | | |–90.52%–do_dentry_open
| | | | | | | | | | | | | --89.78%–bpf_lsm_file_open
| | | | | | | | | | | | | --89.30%–0xffffffffc0288c18
| | | | | | | | | | | | | |–87.40%–bpf_prog_b06f413955402a4b_tail_call_security_check
| | | | | | | | | | | | | | | |–77.57%–bpf_prog_934361d723613c1c_enforce_access_policy
| | | | | | | | | | | | | | | | | |–57.87%–bpf_prog_a0f18f4b0b140d77_path_check_callback
| | | | | | | | | | | | | | | | | | | |–29.94%–bpf_probe_read_kernel
| | | | | | | | | | | | | | | | | | | | | |–18.88%–copy_from_kernel_nofault
| | | | | | | | | | | | | | | | | | | | | --4.99%–copy_from_kernel_nofault_allowed
| | | | | | | | | | | | | | | | | | | |–4.88%–htab_map_hash
| | | | | | | | | | | | | | | | | | --2.29%–copy_from_kernel_nofault
この記事は特定の成果ではなく、プロファイリング方法に焦点を当てています。表示されるオーバーヘッドは、フックが実際に行うことに大きく依存するため、ここでは数値を省略し、自分で取得する方法に焦点を当てます。
上記から、時間の使い道を分析し、コードを`perf`チューニングし、eBPF実行時のCコードのp50/p99を調整し始めることができます。
これにより、eBPFコードのパフォーマンスへの影響を明確に特定し、最適化が必要な場所を特定できます。キャッシュしたり、問題の場所に基づいてより良いアルゴリズムを考案したりするほど単純なこともあります。