ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

RustでBPF CO-RE - とりあえずビルドしてみるの巻

2020年は色々やったんですが、不甲斐なさも残りました。2021年も頑張ります(1行で去年の総括と今年の抱負)。

で、RustとBPF CO-RE、2つのsota(2020年末に覚えた言葉の一つ) をブログに書いて気炎を上げていきたい。

(はじめに: 半分自分メモのつもりなんです! という言い訳をしておきます。認識や用語など間違いがあれば突っ込んで...)

BPF CO-RE、コレってなんですか

itiskj.hatenablog.com

上記記事に書いてある通り(さらに言えば Why We Switched from BCC to libbpf for Linux BPF Performance Analysis | PingCAP の通り)、BCCのプロダクション利用には、コンパイラやヘッダファイルなどたくさんの依存、実行時にコンパイルをすることによるオーバヘッドなど多くの問題があったが*1、そう言った実行時にコンパイルしないといけない問題や、環境依存が大きい問題は BTF 、 BPF CO-RE と、libbpf経由での利用などで解消する見込みなのでそっちを掘って行ったほうがいいという話がある。

ぼくもRuby版BCCポートを開発していたので、BCCのdeprecated化には遺憾ではあるが、これがsotaってことなんですね...。

で、libbpfでのツール実装サンプルはbccリポジトリに、すでにある。お馴染み execsnoop などが一通り移植完了している模様。

github.com

はい、Cなんです。ワンバイナリ*2にしたいのでそうなのでしょう...。

複雑なツールをすべてCで作ることは、単純なメモリや文字列の扱い一つとっても難しい場面が多く、保守性も下がるだろう。

BPFプログラム部分はC言語と言ってもDSLのようなもので、複雑なことはしないにしても、できればBPFを利用する側では他の言語の力を借りたい。

libbpf-rs の利用

Rustの libbpf_rs クレートを用いると、RustでBPFツールを作成できる。しかも、基本的にCO-REなバイナリを作れる。

github.com

まずはexamplesにあるやつについて、環境を作って試してみる。

Ubuntu Groovy環境の用意

期待を大きくしておいてなんだけど、そもそも、現状BTF + BPF CO-REなバイナリを動かすのは大変である。 CONFIG_DEBUG_INFO_BTF が有効になったカーネルを作るのがそもそも結構大変で、それが有効になったカーネルには /sys/kernel/btf/vmlinux というファイルがsysfsに生えている。

$ ls -l /sys/kernel/btf/vmlinux 
-r--r--r-- 1 root root 3565534 Jan  5 09:28 /sys/kernel/btf/vmlinux

これが、UbuntuでいうとGroovy(20.10)以降の標準Linuxカーネルではデフォルトで有効になっており、いきなり使えて便利なので、今回はこの環境を使う。読者の皆様はVagrantなどで適宜試されたい。なお、Groovyでは、CのBPFプログラムのビルドで便利な libbpf-dev ヘッダもパッケージになってすぐに入れられる。

ということでUbuntu Groovyで必要なパッケージを入れます。

$ sudo apt install libelf-dev libgcc-s1 clang libbpf-dev

## Rustもセットアップしちゃう
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ . $HOME/.cargo/env

runqslower を動かしてみる

libbps_rs をチェックアウト。

$ git clone https://github.com/libbpf/libbpf-rs.git
$ cd libbpf-rs/examples/runqslower

libbpf-cargo を入れて、まずはbpfプログラムのみをビルドしてみる。

$ cargo install libbpf-cargo
$ cargo libbpf build

target配下に runqslower.bpf.oコンパイルされている。

$ ls -l ../../target/bpf/
total 932
-rw-rw-r-- 1 vagrant vagrant 951944 Jan  6 09:49 runqslower.bpf.o

また、BPFプログラムにアクセスするコードのskelも自動生成してくれる。

$ cargo libbpf gen
Warning: unrecognized map: .maps
Warning: unrecognized map: license
$ ls -l src/bpf/
total 2692
-rw-rw-r-- 1 vagrant vagrant     192 Jan  6 09:50 mod.rs
-rw-rw-r-- 1 vagrant vagrant    2281 Jan  6 09:48 runqslower.bpf.c
-rw-rw-r-- 1 vagrant vagrant     234 Jan  6 09:48 runqslower.h
-rw-rw-r-- 1 vagrant vagrant    6680 Jan  6 09:50 runqslower.skel.rs
lrwxrwxrwx 1 vagrant vagrant      13 Jan  6 09:48 vmlinux.h -> vmlinux_505.h
-rw-rw-r-- 1 vagrant vagrant 2734479 Jan  6 09:48 vmlinux_505.h

mod.rsrunqslower.skel.rs がそれで、このファイルが定義する構造体にパラメータを渡し、ロードしてアタッチする。以下は抜粋。

mod bpf;                                                                                                                                     
use bpf::*;                                                                                                                                  

let mut skel_builder = RunqslowerSkelBuilder::default();
let mut open_skel = skel_builder.open()?;
open_skel.rodata().min_us = opts.latency;
//...
let mut skel = open_skel.load()?;
skel.attach()?;
let perf = PerfBufferBuilder::new(skel.maps().events())
        .sample_cb(handle_event)
        .lost_cb(handle_lost_events)
        .build()?;

loop { perf.poll(Duration::from_millis(100))?; }

skelがあれば cargo build でプログラム本体もビルドできる。

$ ls -l ../../target/debug/
total 17856
drwxrwxr-x 28 vagrant vagrant     4096 Jan  6 09:53 build
drwxrwxr-x  2 vagrant vagrant    20480 Jan  6 09:54 deps
drwxrwxr-x  2 vagrant vagrant     4096 Jan  6 09:53 examples
drwxrwxr-x  4 vagrant vagrant     4096 Jan  6 09:54 incremental
-rwxrwxr-x  2 vagrant vagrant 18245232 Jan  6 09:54 runqslower
-rw-rw-r--  1 vagrant vagrant      773 Jan  6 09:54 runqslower.d

実行。少し負荷をかけてみれば色々出てきて、ちゃんと遅いrun queueを検知できていそう。

$ sudo ../../target/debug/runqslower
Tracing run queue latency higher than 10000 us
TIME     COMM             TID     LAT(us)
09:55:13 kworker/11:0     38523   12390
09:56:42 http             71282   36409
09:57:05 kworker/6:1      48057   14452
09:58:59 dockerd          73130   34074
09:58:59 dockerd          73129   15471
09:58:59 dockerd          73131   48768
09:58:59 kworker/u30:2    71779   12502
09:58:59 containerd       71511   11318
09:58:59 dockerd          73251   88511
09:58:59 containerd-shim  76464   17829
09:58:59 containerd-shim  75362   17918
09:58:59 containerd-shim  79165   17978
09:58:59 sshd             2470    21413
09:58:59 ksoftirqd/9      66      38959
09:58:59 containerd-shim  77094   10517

バイナリの素性

このバイナリは、リンクはこういう感じで、

$ ldd ../../target/debug/runqslower
        linux-vdso.so.1 (0x00007ffd75af9000)
        libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x00007fb5944fe000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fb5944e1000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fb5944c6000)
        librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fb5944bb000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb594499000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb594493000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb5942a7000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fb5948ac000)

起動した際には、 /sys/kernel/btf/vmlinux のみを読み取り、カーネルヘッダなどを改めて利用していないことがわかる。sysfsのファイルにだけ依存するため、コンテナにポン置きしても動かすことが容易であろうと考えられる*3

$ sudo strace -y -e file ../../target/debug/runqslower
execve("../../target/debug/runqslower", ["../../target/debug/runqslower"], 0x7ffe8c882688 /* 13 vars */) = 0
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3</etc/ld.so.cache>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libelf.so.1", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libelf-0.181.so>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libz.so.1", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libz.so.1.2.11>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libgcc_s.so.1>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/librt.so.1", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/librt-2.32.so>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libpthread-2.32.so>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libdl-2.32.so>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libc-2.32.so>
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3</proc/84098/maps>
access("/sys/kernel/btf/vmlinux", R_OK) = 0
openat(AT_FDCWD, "/sys/kernel/btf/vmlinux", O_RDONLY) = 3</sys/kernel/btf/vmlinux>
openat(AT_FDCWD, "/sys/devices/system/cpu/possible", O_RDONLY) = 5</sys/devices/system/cpu/possible>
Tracing run queue latency higher than 10000 us
TIME     COMM             TID     LAT(us)       
openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY) = 14</sys/devices/system/cpu/online>
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 29</usr/share/zoneinfo/Etc/UTC>
10:01:53 systemd-journal  478     55310
...

次は、Cのlibbpf toolsをRustに移植してみたりしたい。


追記: id:y_uuki さんの質問、ご指摘を踏まえいくつか注釈をつけています @ 2021/01/06 19:47

*1:実際動かすのでやっと、という環境が多かったのではないかと愚考

*2:追記: ここでは、コンパイル済みBPFプログラムと利用側プログラムが一体になっている、ぐらいの意味。静的リンクなどは考慮しません

*3:追記: なお、この辺りの特徴はC実装のlibbpf-toolsも同様ではある。Rustで利用側プログラムを実装するメリットとしては、Rust自体の安全性や生産性が大きいと思う