RustとBPFでプロファイラを実装しよう!

FUJITA Tomonori
nttlabs
Published in
Apr 13, 2022

プロファイリングは枯れた技術ですが、BPFで実装するのであれば、学ぶべき新技術という気がしてきますよね!実際、PixieParcaなど、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つになります。

  1. 定期的に、実行中の関数、および、その関数に到るまで呼び出された関数の順序を記録する。関数はメモリ上のアドレスとして記録される。
  2. 記録された関数のアドレスを、人間が理解できるように関数名に変換する。

BPFを使ったプロファイリング

BPFは機能1を効率的に実現することができます。CPUクロックに起因して定期的に発生するイベントで、実行中の関数とそれまで呼び出された関数を記録するBPFコードが実行されるようにします。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 __divti3
358210 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では仲間を募集中です。

--

--

FUJITA Tomonori
nttlabs

Janitor at the 34th floor of NTT Tamachi office, had worked on Linux kernel, founded GoBGP, TGT, Ryu, RustyBGP, etc. https://twitter.com/brewaddict