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")
}