Linuxアドベントカレンダー2020 8日目の記事です。前日はmomomo_rimotoさんの多段sshとchroot環境。そしていろいろなコマンドを使いたいでした。
今日はみんな大好きなeBPFにも関係して、特にUSDTという機能に関するお話をします。
USDT、それは
USDT(User Statically-Defined Tracing) とは、要するに、ユーザランドにあるプログラムに対して、静的にトレースのための窓口(プローブ)を埋め込む仕組みです。トレースが無効の際はパフォーマンスが落ちないような工夫がされています。
詳細な説明は id:mrtc0 の記事の方がわかりやすいので、簡単な説明に留めますが...
カーネル側をトレースする際に、動的な関数などのプローブをkprobe、静的にカーネルに組み込まれたプローブをtracepointと呼びますが、ユーザランドでは動的な関数などのプローブをuprobe、静的なものをUSDTと呼ぶ、という風に対応しています。
試しにlibcのUSDTに関する情報を見てみます。libc.soのELFバイナリには .note.stapsdt
とうNOTEセグメントがあり、どのようなUSDTがどの位置にあるかという情報が記されています。
$ readelf -n /usr/lib/x86_64-linux-gnu/libc-2.31.so | head -40 Displaying notes found in: .note.gnu.property Owner Data size Description GNU 0x00000010 NT_GNU_PROPERTY_TYPE_0 Properties: x86 feature: IBT, SHSTK ... Displaying notes found in: .note.stapsdt Owner Data size Description stapsdt 0x0000003a NT_STAPSDT (SystemTap probe descriptors) Provider: libc Name: setjmp Location: 0x0000000000045d75, Base: 0x00000000001c1698, Semaphore: 0x0000000000000000 Arguments: 8@%rdi -4@%esi 8@%rax stapsdt 0x0000003b NT_STAPSDT (SystemTap probe descriptors) Provider: libc Name: longjmp Location: 0x0000000000045f21, Base: 0x00000000001c1698, Semaphore: 0x0000000000000000 Arguments: 8@%rdi -4@%esi 8@%rdx stapsdt 0x00000042 NT_STAPSDT (SystemTap probe descriptors) Provider: libc Name: longjmp_target Location: 0x0000000000045f3d, Base: 0x00000000001c1698, Semaphore: 0x0000000000000000 Arguments: 8@%rdi -4@%eax 8@%rdx stapsdt 0x0000003b NT_STAPSDT (SystemTap probe descriptors) Provider: libc Name: longjmp Location: 0x0000000000045fb7, Base: 0x00000000001c1698, Semaphore: 0x0000000000000000 Arguments: 8@%rdi -4@%esi 8@%rdx stapsdt 0x00000042 NT_STAPSDT (SystemTap probe descriptors) Provider: libc ...
USDTを有効にしたバイナリを作る
USDTを有効にしたプログラムを作成する場合、以下のような DTRACE_PROBE[N]
マクロをソースコードに宣言する必要があります。
// 2引数のプローブの例
DTRACE_PROBE2(progname, usdtname, longval, strval);
このマクロはBSD系のプログラムで利用できるマクロと共通のフォーマットになっているため、多くのプログラムでは、configureの際に --enable-dtrace
などdtraceを有効にするフラグを設定すると、LinuxではUSDTが有効のバイナリが作成されます。
以下はRubyの例です。
$ sudo apt install systemtap-sdt-dev # 事前に入れる $ RUBY_CONFIGURE_OPTS='--enable-dtrace' rbenv install 2.7.2 Downloading ruby-2.7.2.tar.bz2... -> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.2.tar.bz2 Installing ruby-2.7.2... Installed ruby-2.7.2 to /home/vagrant/.rbenv/versions/2.7.2 $ rbenv global 2.7.2
readelfで確認すると確かに .note.stapsdt
が定義されていました*1。
$ ldd `rbenv which ruby` linux-vdso.so.1 (0x00007ffefaddc000) libruby.so.2.7 => /home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7 (0x00007f12abe01000) $ readelf -n /home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7 ... Displaying notes found in: .note.stapsdt Owner Data size Description stapsdt 0x00000043 NT_STAPSDT (SystemTap probe descriptors) Provider: ruby Name: array__create Location: 0x0000000000037a08, Base: 0x000000000035ce42, Semaphore: 0x00000000003b7cd8 Arguments: -4@$0 8@%rax -4@4(%rsp) stapsdt 0x00000045 NT_STAPSDT (SystemTap probe descriptors) Provider: ruby Name: array__create Location: 0x0000000000039c58, Base: 0x000000000035ce42, Semaphore: 0x00000000003b7cd8 Arguments: -8@%rbx 8@%rax -4@4(%rsp) stapsdt 0x00000045 NT_STAPSDT (SystemTap probe descriptors) Provider: ruby Name: array__create Location: 0x000000000003b558, Base: 0x000000000035ce42, Semaphore: 0x00000000003b7cd8 Arguments: -8@%rbp 8@%rax -4@4(%rsp) stapsdt 0x00000045 NT_STAPSDT (SystemTap probe descriptors) Provider: ruby Name: array__create Location: 0x000000000003c618, Base: 0x000000000035ce42, Semaphore: 0x00000000003b7cd8 Arguments: -8@%rbx 8@%rax -4@4(%rsp) ...
USDTにアタッチする
簡単にアタッチしてみましょう。bpftraceをインストールします*2。今回はUbuntu Focalの標準で提供されている、aptで入れられるもので十分でしょう。
$ sudo apt install bpftrace
動作確認のため bpftrace -l
を動かします。
$ sudo bpftrace -l 'usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7' usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:array__create usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:raise usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:gc__sweep__begin usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:gc__sweep__end usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:gc__mark__begin usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:gc__mark__end usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:hash__create usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:load__return usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:load__entry usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:find__require__return usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:require__entry usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:find__require__entry usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:require__return usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:object__create usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:parse__end usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:parse__begin usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:string__create usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:symbol__create usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:method__cache__clear usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:cmethod__entry usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:cmethod__return usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:method__return usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7:ruby:method__entry
さて、トレースを試します。今回、Rubyが起動直後からSinatraのアプリをセットアップするまでに、どれだけのファイルをrequireするか可視化します。
下準備として、以下のような __block.rb
という名前のファイルを作っておきます。
puts $$
gets
Sinatraのアプリのコードはこれくらいで(事前に gem install sinatra
でgemを入れておきましょう)。
require 'sinatra' get '/' do "Hola!\n" end
__block.rb
を -r
オプションで事前に読み込ませて、起動時にPIDを出力しつつ一旦止まるようにします。
$ ruby -r./__block.rb test.rb 53958
別のターミナルでbpftraceを起動し、このPIDにアタッチします。なお、 BPFTRACE_STRLEN
をいう環境変数を渡さないとrequireしたファイル名が64バイトで切り詰められてしまいます。
$ sudo env BPFTRACE_STRLEN=128 \ > bpftrace \ > -p 53958 \ > -e 'usdt:/home/vagrant/.rbenv/versions/2.7.2/lib/libruby.so.2.7.2:ruby:require__entry { > printf("[%d]%s - %s required\n", pid, comm, str(arg0)); > }' Attaching 1 probe...
アタッチしたら、Sinatra側のプロセスでリターンキーを押し起動を再開します。
$ ruby -r./__block.rb test.rb 53958 [2020-12-07 01:53:09] INFO WEBrick 1.6.0 [2020-12-07 01:53:09] INFO ruby 2.7.2 (2020-10-01) [x86_64-linux] == Sinatra (v2.1.0) has taken the stage on 4567 for development with backup from WEBrick [2020-12-07 01:53:09] INFO WEBrick::HTTPServer#start: pid=53958 port=4567
すると、bpftraceでは以下のようにrequireしたファイルの一覧が表示されます。
[53958]ruby - sinatra required [53958]ruby - sinatra required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sinatra-2.1.0/lib/sinatra/main.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sinatra-2.1.0/lib/sinatra/base.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/version required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/tilt-2.0.10/lib/tilt.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/tilt-2.0.10/lib/tilt/mapping.rb required [53958]ruby - monitor required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/tilt-2.0.10/lib/tilt/dummy.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/tilt-2.0.10/lib/tilt/template.rb required [53958]ruby - thread required ... [53958]ruby - logger required [53958]ruby - uri required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection/http_origin.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection/ip_spoofing.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection/json_csrf.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection/path_traversal.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection/xss_header.rb required [53958]ruby - /home/vagrant/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-protection-2.1.0/lib/rack/protection.rb required
USDTを用いたトレースツールを作る
今度は、作られたオブジェクトの統計情報を表示するためのツールを作成します。せっかくなのでRubyを用いた rbbcc を使いましょう*3。
インストールはomnibusで固めたall-in-oneなパッケージを用いることができます。
$ wget https://github.com/udzura/omnibus-rbbcc/releases/download/0.6.4/rbbcc_0.6.4_focal-1_amd64.deb $ sudo apt install ./rbbcc_0.6.4_focal-1_amd64.deb $ rbbcc --version RbBCC: version 0.6.4 Using ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
さて、以下のようなRubyのスクリプトを書き、 objcount.rb
として保存します(長いです)。
require 'rbbcc' include RbBCC pid = ARGV[0]&.to_i || raise("Usage: #{$0} PID") prog = <<BPF #include <uapi/linux/ptrace.h> struct key_t { char klass[64]; }; struct leaf_t{ u64 count; }; BPF_HASH(store, struct key_t, struct leaf_t); int countup_create_object(struct pt_regs *ctx) { struct key_t key = {0}; struct leaf_t initial = {0}, *data_; bpf_usdt_readarg_p(1, ctx, &key.klass, sizeof(key.klass)); data_ = store.lookup_or_try_init(&key, &initial); if(data_) { struct leaf_t data = *data_; data.count++; store.update(&key, &data); } return 0; }; BPF u = USDT.new(pid: pid.to_i) u.enable_probe(probe: "object__create", fn_name: "countup_create_object") b = BCC.new(text: prog, usdt_contexts: [u]) block = true Signal.trap(:INT) { puts "\nCollected."; block = false} puts "Collecting events... Hit Ctrl-C to finish" while block end puts "%-32s %5s" % %W(KLASS_NAME COUNT) res = {} b["store"].items.sort_by{|k, v| -v.count }.each do |k, v| # integrate some dups res[k.klass] ||= 0 res[k.klass] += v.count end res.sort_by{|k, v| -v }.each do |klass, count| puts "%-32s %5d" % [klass, count] end
動かします、先ほど同様 __block.rb
で一度停止させる形でsinatraを起動しました。
$ ruby -r./__block.rb test.rb 120811
別のターミナルで objcount.rb
を起動します。
$ sudo rbbcc ./objcount.rb 120811 Collecting events... Hit Ctrl-C to finish
Sinatraのターミナルでリターンを押し、起動し切ってからrbbccのターミナルでCtrl-Cを押すと、起動までに作成されたRubyレベルのオブジェクトが集計され、表示されます。
Collecting events... Hit Ctrl-C to finish ^C Collected. KLASS_NAME COUNT String 3755 Range 768 Gem::StubSpecification 173 File 173 Gem::StubSpecification::StubLine 173 Class 145 Gem::Version 92 Gem::Requirement 80 Regexp 76 Array 46 Gem::Dependency 31 Object 26 #<Class:0x0000564a75ebefb8> 21 #<Class:0x0000564a760a4be8> 20 #<Class:0x0000564a75ea2c28> 19 #<Class:0x0000564a75ebe158> 19 #<Class:0x0000564a75ea2e58> 19 #<Class:0x0000564a760a4f08> 18 Mustermann::AST::Node::Char 15 #<Class:0x0000564a76089e10> 15 Hash 11 Mustermann::AST::Transformer::ExpressionTransform::Operator 8 Thread::Mutex 8 ...
例えば、意図せず特定のクラスのインスタンスが大量に作られていないか、などの事象が確認できるかもしれません。
また、USDT + eBPFの組み合わせであれば本番環境のアプリケーションに、パフォーマンスの劣化をほぼ気にせずにアタッチができます*4。RubyのTracePointクラスのような他の方法よりもその点は有利と言えます。
まとめ
このように、USDTを経由すれば、eBPFなどを用いてアプリケーションレベルのトレースやデバッグが可能になります。
なお、USDT自体はeBPF固有のプローブではありません。systemtap、ftraceやperfなどでも使えます。例えばブレンダン氏の下記の記事などを参照してください。
明日は Dolphin_0809 さんの記事です!
今回の環境のバージョンなど
Ubuntu
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.1 LTS Release: 20.04 Codename: focal
kernel
$ uname -a Linux ubuntu2004.localdomain 5.8.0-29-generic #31~20.04.1-Ubuntu SMP Fri Nov 6 16:10:42 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
glibc
$ sudo apt show libc6 Package: libc6 Version: 2.31-0ubuntu9.1
Ruby(tracee)
$ ruby -v ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
rbbcc
$ rbbcc --version RbBCC: version 0.6.4 Using ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
*1:どういうプローブがあるかはRDocがあります。 https://docs.ruby-lang.org/en/2.4.0/dtrace_probes_rdoc.html これはRuby のバージョンが微妙に古いですが、内容は同じです。
*2:bpftraceに関しては2日目の記事もぜひ
*3:TODO: Python版BCCでの実装例も置いておく。あまり変わりませんが。
*4:単純にアタッチ有効/無効で 10000 回のアクセスを比較したベンチでは、有効時に平均レスポンスタイムが1.013 [ms]、無効時は0.862 [ms]でした。本番アプリケーションではI/OなどCPUレベルでの劣化と無関係なボトルネックが増えるため、より微小な差異になるでしょう。