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 showbpftool prog show でBPFプログラムやBPFマップの一覧を確認できる
    • batch fileをバージョン管理しておくのがオススメ
  • BPFTrace
    • BPFの高レベルDSL
    • awkのような感じでBEGINENDと実際のトレーシング部分という構成
    • 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")
}