[[eBPF|BPF]]トレーシングを学ぶためのプロセスが、Greggの次の記事で述べられている。 [Learn eBPF Tracing: Tutorial and Examples](https://www.brendangregg.com/blog/2019-01-01/learn-ebpf-tracing.html) 記事によると、初心者、中級者、上級者に分けて、次のようなステップで学んでいくとよいとある。 - 初心者:bccの性能分析ツールを動かす。 - 中級者:bpftraceのツール(スクリプト)を開発する。 - 上級者:bccのツールを開発する、bccやbpftraceに貢献する。 [eBPF Summit 2020](https://ebpf.io/summit-2020/)でLiz Riceにより発表されたビギナーズガイドには、BCCの簡単なツール開発に至るまでの最短の道案内が示されている。[GitHub - lizrice/ebpf-beginners: The beginner's guide to eBPF](https://github.com/lizrice/ebpf-beginners) 今後は、CO-REをサポートすることが推奨されていくことを踏まえると、BCC以外にCO-REに対応したツール開発が必要となるだろう。そこで、著者のBPFアプリケーションの実装経験を踏まえて、CO-REに対応したトレーシングツール開発を目指した、BPFプログラミングのプロセスを紹介する。 ### 0. 何のツールをつくるかを決める まずは、どのようなトレーシングツールをつくるかを決めるところから始まる。既存のツールと重複しないものが望ましいが、最初から新規性かつ有用性を兼ね備えるようなツールの着想に至るのは難しいだろう。 書籍<sup id="a2">[2](#f2)</sup>のPart Ⅱ: Using BPF toolsの各章末にトレーシングに関する練習問題が挙げられており、一部の問題は指示されたツールの開発である。この問題に取り組んでみるのもいいかもしれない。例えば、8.5 "Optional Exercises"のリストの3-7番目はツール開発の問題である。そのうちの4番目がおもしろそうなので、以下に引用しておく。 > 4. Develop a tool to show the ratio of logical file system I/O (via VFS or the file system interface) vs physical I/O (via block tracepoints). ### 1. トレーシング対象の発見 カーネルから何かしらの内部状態を取得したいと考えたときに、カーネルのどの関数やどの変数からトレースすればよいかは自明ではない。 まずは、kprobesとtracepointsでカーネル内のフックポイントのリストを出力し、トレース可能な対象を概観する。kprobesでアタッチ可能な関数のリストは、`/sys/kernel/debug/tracing/available_filter_functions`から読み出せる。 ```shell-session # cat /sys/kernel/debug/tracing/available_filter_functions | grep -e '^tcp_v4' | head tcp_v4_init_seq tcp_v4_init_ts_off tcp_v4_reqsk_destructor tcp_v4_restore_cb tcp_v4_fill_cb tcp_v4_md5_hash_headers tcp_v4_md5_hash_skb tcp_v4_route_req tcp_v4_init_req tcp_v4_init_sock ``` tracepointsのリストはbcc toolsに含まれる[tplist(8)](https://github.com/iovisor/bcc/blob/master/tools/tplist.py)の出力から得られる。システムコールはtracepointsに含まれる。 ```shell-session # tplist | grep tcp: tcp:tcp_retransmit_skb tcp:tcp_send_reset tcp:tcp_receive_reset tcp:tcp_destroy_sock tcp:tcp_rcv_space_adjust tcp:tcp_retransmit_synack tcp:tcp_probe ``` 実際に概観してみると、大量のフックポイントがあることがわかる。ここから望むものを発見することは難しい。しかし、なんらかの負荷想定をもっていれば、その想定からフックポイントのを絞り込める。 実際にカーネルに負荷を発生させながら、その負荷に関連するイベントソースを調べる方法がある。bcc toolsの[profile(8)](https://manpages.debian.org/experimental/bpfcc-tools/profile-bpfcc.8.en.html) では、-pオプションでPIDを指定することにより、動作中のプロセスに紐づくスタックトレースを取得できる。スタックトレースからフックポイントとして使えそうなものを発見できるかもしれない。その他のスタックトレースや関数の呼び出し回数を出力するツールは、[funccount(2)]((https://manpages.debian.org/unstable/bpfcc-tools/funccount-bpfcc.8.en.html) メモリであれば[memleak(8)](https://manpages.debian.org/unstable/bpfcc-tools/memleak-bpfcc.8.en.html)、ファイルシステムであれば[xfsdist(8)]()、[ext4dist(8)]()、ディスクI/Oであれば、[biostacks(8)]()、ネットワークの上位層のソケット層では、[sockstat(8)]()がある。bcc tools以外では、ネットワークの下位層のパケットに対しては、[@YutaroHayakawa](https://twitter.com/YutaroHayakawa)さん作の[ipftrace2](https://github.com/YutaroHayakawa/ipftrace2)も有用である。ipftrace2はカーネル内のパケットのフローを関数単位で追跡できる。 フックポイントに見当をつけたのちに、そのフックポイントの詳細を調べる。まず、tplist(8)により、フックポイントの引数の名前と型を確認する。 ```shell-session # tplist -v syscalls:sys_enter_read syscalls:sys_enter_read int __syscall_nr; unsigned int fd; char * buf; size_t count; ``` 次に、[argdist(8)]()により引数の値と返り値の分散を確認できる。フックポイントの通過頻度が小さければ、[trace(8)]()で個々のイベントを出力することもできる。最後に、bpftraceを使用してフックポイントに対して簡単に処理を書いてみることもできる。[bpftraceのリファレンスガイド](https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md)にあるように、さまざまなユーティリティ関数が揃っている。 ### 2. BCCによるプロトタイピング bccリポジトリ内の性能分析ツールが非推奨になったとはいえ、BCCはプロトタイピングに有用だ。BCCであれば、BPFプログラムとフロントエンドプログラムの両方を1枚のスクリプト内に収められるため、試行錯誤を速められる。例えば、BPFプログラムはPythonの文字列として記述されるため、フロントエンドへの入力に応じて、文字列処理で簡単にBPFプログラムを動的生成できる。mapへのアクセスも、libbpfを直接使うより簡単に書ける。 BCCの機能は、[BCCのリファレンスガイド](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md)に整理されている。 著者はいきなり最終ステップであるlibbpf + CO-REから書き始めたが、一旦BCCでプロトタイプを作成したのちに、libbpf + CO-REで実装すればよかったと後悔した。 ### 3. libbpf + CO-RE Nakryikoによる[Building BPF applications with libbpf-bootstrap](https://nakryiko.com/posts/libbpf-bootstrap/)の記事にlibbpfベースのBPFアプリケーションの構築方法がまとめられている。同時に、[libbpf + Cに移植されたbcc toolsのソースコード](https://github.com/iovisor/bcc/tree/master/libbpf-tools)が具体例として参考になる。これらのリソースがなければ、著者は実装がおぼつかなかっただろう。ただし、Nakryikoの記事は古いバージョンのlibbpfを基に書かれているため、[libbpf 1.0](https://github.com/libbpf/libbpf/wiki/Libbpf%3A-the-road-to-v1.0)以降では一部のAPIの仕様が変更されていることに留意しなければならない。 BPFは開発が活発なため、カーネルの細かなバージョンごとに利用可能な機能に差異がある。[BPFの機能とカーネルバージョンとの対応表](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md)があるため、サポートするカーネルバージョンを決めてからどの機能を利用するかを見当するとよい。 余談だが、CO-REの機構を使わずに、異なるカーネルバージョンに対応する方法もなくはない。[weaveworks/tcptracer-bpf](https://github.com/weaveworks/tcptracer-bpf)では、既知のパラメータ(既知のIPアドレスやポートなど)で一連のTCP接続を作成し、それらのパラメータがカーネルのstruct sock構造体のフィールドオフセットを検出している。[datadog-agent](https://github.com/DataDog/datadog-agent)でも ### Go言語によるBPFプログラミング Prometheusに代表されるように、Goで書かれたObservabilityツールは多数存在する。GoでBPFのフロントエンドを書きたいというニーズもあるだろう。 GoでBPFのフロントエンドを書くには、以下のライブラリのいずれかを使うことになる。フロントエンドのBPFライブラリに最低限必要な処理は、(1)BPFバイトコードとmapのカーネルへのロードと、(2)mapの操作である。 - [iovisor/gobpf](https://github.com/iovisor/gobpf): BCCのGoラッパー - [dropbox/goebpf](https://github.com/dropbox/goebpf): libbpfを使わず自前でbpfシステムコールを呼ぶ - [cilium/ebpf](https://github.com/cilium/ebpf): Pure Go - [DataDog/ebpf](https://github.com/DataDog/ebpf): cilium/ebpfからforkされ、BPFオブジェクトのライフサイクル管理マネージャーが追加されている。 - [aquasecurity/libbpfgo](https://github.com/aquasecurity/libbpfgo): 元はセキュリティランタイムの[Tracee](https://aquasecurity.github.io/tracee/latest)用のlibbpfのGoラッパー。 - libbpf + cgo bindings カーネルが提供するBPFの最新の機能を使いたければ、カーネルのアップストリームでメンテされているlibbpfを使う。Goからはcgoを使用してlibbpfのAPIを呼び出す。libbpfをGoのバイナリに含めるには、libbpfを静的リンクさせる。具体的には、[libbpfの静的ライブラリファイル(.a)をCGO_LDFLAGSで指定してビルド](https://github.com/yuuki/go-conntracer-bpf/blob/e36514323db7b9b84abdced2ba0710ac5468f8d0/Makefile#L89-L94)する。libbpfはlibelfとlibzに依存するため、これらのパッケージがインストールされていない環境を想定するなら、libelfとlibzも自前でビルドしてバイナリに含める。 libbpf APIを自前で呼び出すのが手間であれば、aquasecurity/libbpfgoを使う。ただし、libbpfの全てのAPIがラッピングされているわけではないため、使いたいAPIがサポートされているかを確認しなければならない。 Pure Goのライブラリが使いたければ、cilium/ebpfかDataDog/ebpfを使う。ただし、執筆時点では、[CO-REに対応しきれていないなどの課題](https://github.com/cilium/ebpf/issues/114)がある。 Go + BPFについては、次の記事にも整理されている。 [Getting Started with eBPF and Go | networkop](https://networkop.co.uk/post/2021-03-ebpf-intro/) また、[[XDP]]にフォーカスしたときのGoライブラリの選択については、[@takemioIO](https://twitter.com/takemioIO)さんによる次の記事が参考になるだろう。 [Go+XDPな開発を始めるときに参考になる記事/janog LT フォローアップ - お腹.ヘッタ。](https://takeio.hatenablog.com/entry/2021/01/26/180129) ### Rust言語によるBPFプログラミング システムソフトウェア用のプログラミング言語としてRustが人気である。RustでBPFプログラミングをしたいという人は多いだろう。著者はRustのプログラミング経験はほとんどないため、既存のリソースを簡単に紹介するにとどめておく。 [libbpf/libbpf-rs](https://github.com/libbpf/libbpf-rs)はlibbpfのRustラッパーである。libbpfには依存するが、libbpfの最新の機能が使いやすい。 [aya-rs/aya](https://github.com/aya-rs/aya)はRustでフロントエンドプログラムを書くための最近のBPFライブラリだ。ayaにより、libbpfにもbccにも依存せずに、libcのみの依存で、CO-REに対応したバイナリを生成できる。 [foniod/redbpf](https://github.com/foniod/redbpf)は、フロントエンドではなく、BPFプログラムをRustで書くためのツールとライブラリである。 その他、RustによるBPFトレーシングについて、id:udzura:detail さんの次のスライドが参考になる。[Rustで作るLinuxトレーサ / libbpf-core-with-rust - Speaker Deck](https://speakerdeck.com/udzura/libbpf-core-with-rust) ### BPFプログラミングの留意事項 著者が気づいた範囲でのBPFプログラミングの留意事項を紹介する。 **カーネル・ユーザ間並行性** すでに述べたように、BPFアプリケーションはカーネルとユーザ空間の2種類のプログラムがmapやring bufferなどのカーネル内のデータ構造を経由して、一方向または双方向にデータを共有する。そのため、カーネルとユーザのそれぞれのプログラムで並行して処理が行われる。トレーシングでは、カーネルはMAPにデータを更新し、ユーザがMAPの読み終わったデータを削除することもあるため、書き込み競合が発生する可能性がある。[BPF_LOOKUP_AND_DELETE_BATCHなどのアトミックなAPIを使用して回避できる](https://github.com/iovisor/bcc/blob/629d40a34dd766ed1e962a6aff713a9c4e7e61bd/libbpf-tools/syscount.c#L199-L200)。 **カーネルスレッド間並行性** カーネルでは複数のスレッドが協調して動作しており、スレッド間で並行処理が行われる。kprobeとtracepointでアタッチされたBPFプログラムが、異なるスレッドから呼び出されることを考慮する必要がある。 (TODO) **ユーザ空間のコンテキストの追跡** ユーザ空間では、一連の処理を行うときに、複数のシステムコールを連続して呼び出すことがある。システムコールあたり1つのBPFこのコンテキストを使ってBPFプログラムを書く場合は、コンテキストを追跡する必要がある。 例えば、ユーザ空間のプロセスがネットワークサーバとして動作するときに、サーバはsocket(2)でソケットを作成し、ソケットとIPアドレスをbind(2)して、listen(2)によりソケットを待ち受け状態にするといった王道の手順がある。これらは別々のシステムコールとして発行されるため、カーネル空間のBPFプログラムはシステムコールごとにアタッチされるため、ユーザ空間の手順を この例の場合では最初のsocket(2)のソケットIDをmapに格納しておき、覚えておいて、 MAPのキーにカーネルのスレッドIDを含めないと、スレッドIDごとに。 これは、DBMSを使ったWebアプリケーションのプログラミングで、ユーザなどのオブジェクトのIDで紐付けて一連の処理を書くことと類似している。