Linux Observability with BPFを読んだので、メモしておく。 今月、Brendan Gregg氏のBPF本も出るので、 それも読みたいな。
BPFを使うことで、カーネルのイベントをフックして安全にコードを実行できる そのコードがシステムを破壊したりクラッシュさせたりすることが無いよう、BPF側で検証してくれる カーネルモジュールと異なり、BPFプログラムはカーネル再コンパイルの必要がない BPFコードが検証された後、BPFバイトコードは機械命令にJITされる BPFプログラムは、bpf syscall によってBPF VMへとロードされる 2014年前半にAlexei Starovoitov氏がeBPFを導入した 過去のBPFでは2つの32bit registerのみが使えたが、eBPFでは10個の64bit registerまで使える 2014年6月にはeBPFはユーザスペースにも拡張された BPFプログラムタイプ:トレーシングとネットワーキングに大きく分類できる Socket Filter:カーネルに最初に入ったプログラムタイプ。観測目的のみ Kprobe:kprobe ハンドラとしてBPFプログラムを動かす。関数に入るときと出る時が、それぞれSEC(kprobe/sys_exec)とSEC(kretprobe/sys_exec)に対応する Tracepoint:事前にカーネル側で定義されたtracepointに対してBPFプログラムを動かす。/sys/kernel/debug/tracing/eventsで一覧を確認できる XDP:パケット着信時に、早い段階でBPFプログラムを動かす。 Perf:perf eventに対してBPFプログラムを動かす。perfがデータを吐き出す度にBPFプログラムが実行される Cgroup Socket:そのcgroup内のすべてのプロセスにアタッチされる Socket Option:Facebookはデータセンター内の接続において、RTOs(recovery time objectives)の制御に、これを使っている。 Socket Map:BPFでロードバランサを実装するときに使う。CilliumやFacebook Katranはこれを使っている。 BFP Vefirier CVE-2017-16995 のように、過去BFPをバイパスしてカーネルメモリをバイパスできる脆弱性があった DFSで、そのプログラムが終了すること、危険なコードパスが存在しないことを検証する 無限ループを弾くために、すべてのループを禁止している。ループ許可については、本書を書いている時点では、まだ提案の段階。 命令数は4096に制限されている bpf syscall の log_*を設定すれば、検証結果を確認できる BPFプログラムは tail calls によって、他のBPFプログラムを呼び出すことができる 呼び出し時にすべてのコンテキストは失われるので、何らかの方法で情報を共有する必要がある BPFマップ 直接bpf syscall でBPFマップを作成することができる bpf_map_createヘルパ関数を使うのが簡単 bpf_map_update_elem のシグネチャは、カーネル側bpf/bpf_helpers.hとユーザ側tools/lib/bpf/bpf.hで異なるので注意する エラー番号でsteerror(errno)で文字列にすると便利 bpf_map_get_next_keyはそのほかのヘルパ関数と違って、ユーザ側でのみ使えるので注意する array, hash, cgroup storage maps については、スピンロックがあるので、並行アクセス可能 array map は要素数だけ事前にメモリが確保されて、ゼロで初期化される。グローバルな変数確保のために使う? LRUハッシュやLRM(Longest Prefix Match)Trie mapもある Linux 4.4 からマップとBPFプログラムを仮想FSから扱えるよう2つのsyscallが追加された Tracing kprobes/kretprobes:stableでないABIなので、事前にprobe対象の関数シグネチャを調べておく必要がある。Linuxバージョンごとに変わる可能性がある BPFプログラムのcontextはプログラムによって変わる トレーシングポイントAPIはLinuxバージョンごとに互換性がある。/sys/kernel/debug/tracing/eventsで確認できる USDTs(user statically defined tracepoints):ユーザプログラムの静的なトレースポイント BPFTool 開発中が活発なので、Linux src からコンパイルする bpftool featureで何が利用できるか確認できる もしJITが無効ならecho 1 > /proc/sys/net/core/bpf_jit_enableで有効にできる bpftool map show や bpftool prog show でBPFプログラムやBPFマップの一覧を確認できる batch fileをバージョン管理しておくのがオススメ BPFTrace BPFの高レベルDSL awkのような感じでBEGINとENDと実際のトレーシング部分という構成 BPFマップを自動的に作ってくれるので便利 kubectl-trace BPFTraceプログラムをkubernetes jobとして実行する eBPF Exported BPFトレーシング結果をPrometheusに転送。Cloudflareで使われている XDP cotextとして渡されるxdp_buffは、sk_buffを簡単にしたもの。 3つの動作モードがある Native XDP:ドライバから出てすぐのところでBPFプログラムを動かす。git grep -l XDP_SETUP_PROG drive/で、対応するNICか確認する。 Offloaded XDP:git grep -l XDP_SETUP_PROG_HW drivers/で対応するNICを調べる。BPFプログラムをNICへオフロードする。 Generic XDP:開発者向けのテストモード。 XDPはユニットテストができる ユースケース Sysdig では troubleshooting tool を eBPF でOSSとして開発している Flowmill では データセンターネットワークの監視ツールを開発している。CPUオーばヘッドは0.1%~0.25%程度。 # SHELL # BPFプログラムをELFバイナリへコンパイル clang -O2 -target bpf -c bpf_program.c -o bpf_program.o # BPF用仮想FSのマウント mount -t bpf /sys/fs/bpf /sys/fs/bpf # USDTの確認 tplist -l ./hello_usdt # スタックトレースの取得とflamegraphの作成 ./profiler.py `pgrep -nx go` > /tmp/profile.out ./flamegraph.pl /tmp/profile.out > /tmp/flamegraph.svg # kubectl-traceの実行例 kubectl trace run pod/pod_identifier -n application_name -e <<PROGRAM uretprobe:/proc/$container_pid/exe:"main.main" { printf("exit: %d\n", retval) } PROGRAM # XDP BPFプログラムのロード # native mode がダメなら、generic mode で動く。強制することもできる ip link set dev eth0 xdp obj program.o sec mysection // C // bpf syscall でBPFマップを作成 int fd = bpf(BPF_MAP_REATE, &my_map, sizeof(my_map)); // bpf map 一覧取得 int next_key, lookup_key; = -1; while (bpf_map_get_next\key(map_data[0].fd, &lookup_key, &next_key) == 0) { printf("The next key in the map: %d\n", next_key); lookup_key = next_key; } # BCC (python) from bcc import BPF # kprobesの例 bpf_source = """ int do_sys_execve(struct pt_regs *ctx, void filename, void argv, void envp) { char comm[16]; bpf_get_current_comm(&comm, sizeof(comm)); bpf_trace_printk("executing program: %s", comm); return 0; } """ bpf = BPF(text = bpf_source) execve_function = bpf.get_syscall_fnname("execve") bpf.attach_kprobe(event = execve_function, fn_name = "do_sys_execve") bpf.trace_print() # tracepointの例 bpf_source = """ int trace_bpf_prog_load(void ctx) { char comm[16]; bpf_get_current_comm(&comm, sizeof(comm)); bpf_trace_printk("%s is loading a BPF program", comm); return 0; } """ bpf = BPF(text = bpf_source) bpf.attach_tracepoint(tp = "bpf:bpf_prog_load", fn_name = "trace_bpf_prog_load") bpf.trace_print() # uprobesの例 bpf_source = """ int trace_go_main(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); bpf_trace_printk("New hello-bpf process running with PID: %d", pid); } """ bpf = BPF(text = bpf_source) bpf.attach_uprobe(name = "hello-bpf", sym = "main.main", fn_name = "trace_go_main") bpf.trace_print() # USDTの例 from bcc import BPF, USDT bpf_source = """ #include <uapi/linux/ptrace.h> int trace_binary_exec(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); bpf_trace_printk("New hello_usdt process running with PID: %d", pid); } """ usdt = USDT(path = "./hello_usdt") usdt.enable_probe(probe = "probe-main", fn_name = "trace_binary_exec") bpf = BPF(text = bpf_source, usdt = usdt) bpf.trace_print() # BPFTrace のDSL # bpftrace /tmp/examble.bt で実行 BEGIN { printf("starting BPFTrace program\n") } kprobe:do_sys_open { printf("opening file descriptor: %s\n", str(arg1)) @opens[str(arg1)] = count() } END { printf("exiting BPFTrace program\n") }