Ubuntu Weekly Recipe

第692回 sysfsやbpftoolを用いたeBPFの活用

この記事を読むのに必要な時間:およそ 10 分

第688回第690回では,カーネルのトレーシングツールとして注目されているeBPFを活用するためのツールとしてBCCを紹介しました。しかしながら,BCCだけがeBPFを扱えるツールというわけではありません。今回はツールなしに利用できるsysfsや,よりユーザーフレンドリーなトレーシングツールであるbpftoolを紹介します。

Python版BCCの問題点

これまで紹介していたBPF Compiler CollectionBCCのツールはいずれもフロントエンドとしてPythonを使っていました。つまり利用者はまずPythonスクリプトを起動し,その中でeBPFのオブジェクトをコンパイルし,ロードすることでようやくトレースが始まっていたのです。

実行環境でBPFオブジェクトをビルドする必要があるこの方法にはいくつかの問題点が存在します。

  • 実行環境にコンパイラをインストールする必要がある
  • 実行環境にカーネルのヘッダーファイルが必要になる
  • 実行時にコンパイルという重い処理が走る

この問題は日常的に開発に利用しているデスクトップ環境やサーバーならそこまで影響はありません。しかしながらプロダクション用途となると話は別です。セキュティ的にもメンテナンス的にもできる限り余計なものはインストールしたくないでしょう。障害の事前検知手段としてeBPFを使いたいと考えたときに,そのトレースツールの処理の重さが新たな障害のトリガーなってしまっては目も当てられません。サーバーのリソースモニタリングや,ネットワークフレームの処理などにeBPFを使用したいとき,上記のような制約が存在すると導入の障害になるのです※1⁠。

※1
BPFの移植性の問題と今回紹介するCO-REによってどのように解決されるか,執筆時点での状況などはBPF CO-RE (Compile Once – Run Everywhere)が非常によくまとまっています。とても丁寧でわかりやすいまとめなので,もしかしたら誰かが許可を得て,日本語訳を作ってくれているかもしれません。

対応策として,いくつかの方法が考えられます。

  • Clang等でBPFオブジェクトをコンパイルし,別途ロードする
  • sysfsの機能を用いてトレーシングする
  • bpftrace等の別のツールを使う
  • BPF CO-REによりポータブルなバイナリを生成する

Clang等でBPFオブジェクトをコンパイルし,別途ロードする

Clang等を使えばBPFオブジェクトをコンパイルできます。これをさらにカーネルにロードしてしまえば,BCCのバックエンドと同じことを実現できます。しかしながらきちんと動くBPFオブジェクトを作るにはそれなりの知識が必要です。そこが大変だからこそ,Python版のBCCが登場し,広く使われたという経緯が存在します。よって最初の選択肢は,条件次第では採用できるものの,多くのケースにおいては他の選択肢を使ったほうが「楽ができる」でしょう。

sysfsの機能を用いてトレーシングする

2番目の選択肢としてカーネルのsysfsにあるトレース機能を使ってトレーシングを行う方法が考えられます。この方法ならシステムに追加でソフトウェアをインストールしなくても,トレーシングできるというメリットがあります。ただし制約はいくつか存在します。一番大きいのが,トレース結果をtrace_pipeでしか取得できないことでしょう。つまりユーザーランド側はtrace_pipeの出力結果を解析する必要があります。表示するデータの量は変更可能ではあるのですが,あくまでprintfによる出力であるため,渡せるデータの内容やその方法については限定されます。それでも結局のところ,デバッグ目的ならなんらかの出力は必須であるため,簡単に調査する分には問題とはならないでしょう。

bpftrace等の別のツールを使う

より高機能なトレーシングツールとしてbpftraceが存在します。内部的にやっていることはBCCと同じでその場でコンパイルすることになるのですが,カーネルのBTF(BPF Type Format)を活用することでその依存関係を極力減らしています。また独自の言語を採用することで,トレーシングの表現がしやすくなっているのが特徴のひとつです。ちなみに現在はBCCと同じIO Visorプロジェクトで開発が行われています。eBPFを「トレーシング目的」で使うなら,BCCよりもbpftraceを使うほうが簡単かもしれません。独自言語の学習コストはありますが,そこまで難しくはないのですぐに理解できるようになるでしょう。最新版がsnapパッケージとして提供されているため,導入も容易です。

BPF CO-REによりポータブルなバイナリを生成する

最後の方法が現在注目されている,BPF CO-RE(Compile Once – Run Everywhere)を活用する方法です。つまり一度特定の環境でBPFを利用したプログラムバイナリをビルドしておけば,そのバイナリをたとえカーネルのバージョンが異なっていても他の環境で実行できる仕組みです。これにより実行環境にはバイナリだけコピーすれば良いことになります。

BPF CO-REもbpftraceと同じくカーネルのBTFを活用します。BTFは端的に言うとBPFプログラムを動かすために必要な,実行中のカーネルの各種情報を提供する仕組みです。最近のカーネルはこのBTFを大幅に拡張し,BPFプログラムが実行時に自動的にカーネルのバージョン間の差異を自動的に調整できるようになりました。さらにClangはそのBTFを活用したBPFオブジェクトを生成できるようになり,そんなBPFオブジェクトをロードするためのライブラリであるlibbpfは,ロード時にBTFとBPFオブジェクトを適切に結びつけるようになりました。

つまり「BPF CO-RE」とはBPFオブジェクトを再利用可能にするために各種ツールをアップデートした状態をまとめたものであり,BPF CO-REというソフトウェアが存在するというわけではありません。

結局何を使うべきか

このように,BCC以外にも複数の選択肢が存在します。またここでは紹介しなかったツールや,トレーシング以外にeBPFを使うツールも存在します。結局のところ,やりたいことに合わせてツールを変えるのがベストな方法です。

今回例をあげたもの中だと,BPF CO-REは注目株になっています。ただしBPF CO-RE(や最新のBTF)を活用するためには,そこそこ新しいカーネルやツールチェインが必要です。Ubuntuだと最初に動くようになったのが,20.10の頃になります。つまり現時点で最新のLTSであるUbuntu 20.04 LTSでは,BPF CO-REの恩恵は受けられません。また,BPF/BTF自体がまだ活発に機能拡張されているため,本格的に使うならより新しいカーネルやツールチェインを使いたいところです。よってUbuntuの場合は,2022年4月にリリースされる次のLTSであるUbuntu 22.04 LTSぐらいから,プロダクション用途でも使われるようになっていくのではないでしょうか※2⁠。

※2
古いカーネルでもBTFを追加ロードすることでBPF CO-REなオブジェクトをロードできるようにするBPF Hubというプロジェクトもあるようです。

今回はsysfsとbpftraceを使う方法を紹介します。BPF CO-REについては次回以降に解説予定です。

今一度execveをトレースする

まずは最初に第690回で解説した,execve()をトレースするPython版BCC向けのコードexecve.pyを再掲しておきましょう。これはexecsnoopをよりシンプルにしたようなコードです。今回はこれを各種ツールを活用した形に作り直すことにします。

#!/usr/bin/python3
from bcc import BPF

bpf_text="""
#include <linux/sched.h>

struct data_t {
    u32 pid;
    u32 ppid;
    char comm[TASK_COMM_LEN];
    char fname[128];
};
BPF_PERF_OUTPUT(events);

int syscall__execve(struct pt_regs *ctx, const char __user *filename)
{
    struct data_t data = {};
    struct task_struct *task;

    data.pid = bpf_get_current_pid_tgid() >> 32;

    task = (struct task_struct *)bpf_get_current_task();
    data.ppid = task->real_parent->tgid;

    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_user(data.fname, sizeof(data.fname), (void *)filename);

    events.perf_submit(ctx, &data, sizeof(struct data_t));
    return 0;
}
"""

b = BPF(text=bpf_text)
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="syscall__execve")

print("PID      PPID     COMM             FNAME")
def print_event(cpu, data, size):
    event = b["events"].event(data)
    print("{:<8} {:<8} {:16} {}".format(event.pid, event.ppid, event.comm.decode(), event.fname.decode()))

b["events"].open_perf_buffer(print_event)
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

BCC(bpfcc)がインストールされている環境で上記を実行すると,次のような結果が表示されます。

$ sudo apt instal python3-bpfcc
$ sudo python3 execve.py
(中略)
PID      PPID     COMM             FNAME
2822958  5480     tmux: server     /bin/sh
2822960  2822958  sh               /usr/bin/byobu-status
2822959  5480     tmux: server     /bin/sh
2822961  2822960  byobu-status     /usr/bin/sed
2822963  2822959  sh               /usr/bin/byobu-status
2822964  2822960  byobu-status     /usr/bin/tmux

著者プロフィール

柴田充也(しばたみつや)

Ubuntu Japanese Team Member株式会社 創夢所属。数年前にLaunchpad上でStellariumの翻訳をしたことがきっかけで,Ubuntuの翻訳にも関わるようになりました。