RustとBPFでプロファイラを実装しよう!
プロファイリングは枯れた技術ですが、BPFで実装するのであれば、学ぶべき新技術という気がしてきますよね!実際、Pixie、Parcaなど、BPFを使った第三世代プロファイラの開発が進んでいます(世代数は雰囲気)。しかし、Rustの実装は見つからなかったので、RustとBPFで実装してみました。
CPUプロファイリングの基本
CPUプロファイリングの典型的な目的は、頻繁に実行される関数を見つけることです。プロファイラは、定期的に、対象のプロセスやスレッドで、実行されている関数を記録します。
10000: main(); find_item(); calc();
5000: main(); parse_binary(); scan();
2000: main(); verify(); calc();
上記の例では、1行目に、calc関数が10,000回記録されています。プロファイラは、実行されていたcalc関数だけでなく、main関数がfind_item関数を、find_item関数がcalc関数を呼び出したという、呼び出された関数の順序を記録します。そのおかげで、3行目も、calc関数が記録されていますが、find_item関数がcalc関数を一番多く呼び出していることが分かり、find_item関数の実行回数の削減など、最適化のヒントが得られます。
プロファイラの機能は、大きく分けると、下記の2つになります。
- 定期的に、実行中の関数、および、その関数に到るまで呼び出された関数の順序を記録する。関数はメモリ上のアドレスとして記録される。
- 記録された関数のアドレスを、人間が理解できるように関数名に変換する。
BPFを使ったプロファイリング
BPFは機能1を効率的に実現することができます。CPUクロックに起因して定期的に発生するイベントで、実行中の関数とそれまで呼び出された関数を記録するBPFコードが実行されるようにします。BPFは、それらの情報を得る関数を提供しており、この機能は簡単に実装することができます。
上記が実装したプロファイラのBPFコードですが、12行目のbpf_get_stack関数を実行すると、実行中の関数及びそれまで呼び出された関数の順序を、関数アドレスの配列として得ることができます。得られたデータを、BPF ring buffer機能で、ユーザスペースに送信します。
BPFを使ってプロファイリング機能を初めて(たぶん)実装したBCC、その後継のbpftrace、Pixie、Parcaも、今回の実装同様に、bpf_get_stack関数と類似の関数を使っています。
Rustで実装したユーザスペースのコードは、ring bufferからデータを取り出し、実行されているバイナリに含まれている情報を使って、関数アドレスから関数名に変換します。今回は、addr2lineというRustライブラリを使いました。
では、早速実行してみましょう。
# ./target/debug/profiler ~/git/foo/target/release/foo358210 2022-04-13 11:37:40.550452735 UTC
5648d14d4453 __divti3358210 2022-04-13 11:37:40.560551849 UTC
5648d149fd50 foo::f0
関数が1つしか表示されず、呼び出された関数の順序が解析できていないようです。main関数はなぜ表示されないのでしょうか。
呼び出された関数の解析
BPFが提供する関数は、呼び出された関数が解析できていないようなので、その方法を確認してみましょう(x86_64アーキテクチャを対象とします)。
関数は呼び出されるたびに、変数など関数に関する情報(フレーム)を、コールスタックと呼ばれるメモリ領域に保存します。実行中の関数の最新のフレームから、フレームを順番にたどることで、呼び出された関数の順序を知ることができます。
BPFが提供する関数は、フレーム内に保存されている、フレームポインタと呼ばれる、一つ前のフレームのアドレスを使って、フレームを順番にたどります。問題は、言語、アーキテクチャ、コンパイルオプションなどによりますが、性能向上などの理由から、フレームポインタが使われないケースがあることです。Rustは、デフォルトでフレームポインタを使いません。下記の環境変数を設定することで、フレームポインタを使うバイナリが作られます。
RUSTFLAGS=-Cforce-frame-pointers=yes
フレームポインタを有効にしたバイナリは、正しくプロファイリングできました。
# ./target/debug/profiler ~/git/foo/target/release/foo358370 2022-04-13 11:59:30.022516876 UTC
555dc1f4d5a8 foo::f0
555dc1f4d33e foo::exec
555dc1f4d25f foo::main
555dc1f4df2e core::ops::function::FnOnce::call_once
555dc1f4cf01 std::sys_common::backtrace::__rust_begin_short_backtrace
555dc1f4ce54 std::rt::lang_start::{{closure}}
555dc1f61501 std::rt::lang_start_internal
まとめ
標準のプロファイラperfは、フレームポインタを使わないコールスタック解析もサポートしていますが、あえて、RustやBPFを追求したいというエンジニアのみなさま、NTTでは仲間を募集中です。