BCC/BPFでシステムコールとかをいい感じにトレースする

この記事は KMC Advent Calendar 2017 の7日目の記事です。 昨日の記事は id:dnek さんの Unityで漢字パズルアプリを作ったよ(Android/iOS) でした。

シンプルだけどこだわって作っている感じがしてすごくよいですね。 (自分の作ったゲームはゲーム性へのこだわりが足りなくて微妙なのしかないので)

さて、今日はインフラ的なレイヤーの話をします。

自由にメトリクスを取りたい!!

パフォーマンスの改善や障害対応を行うために、「fsync のレイテンシの分布が知りたい」「dentry cache のヒット率が知りたい」「IOオペレーションのサイズの分布が知りたい」といった、OS の低レイヤーの情報を集めたくなることがあります。 top, vmstat, sar などのコマンドを使ってメトリクスを得るのが普通ですが、OS が予め用意したメトリクスしか見ることができず、必ずしも知りたい情報が得られるとは限りません。

例えば、dentry cache は /proc/sys/fs/dentry-state を見るとどれぐらい使われているかは分かるんですが、ヒット率が何%ぐらいかは全然わかりません。

こういうときに自分で自由に kernel 内の情報を収集できれば便利です。

そんなあなたに BCC

最近の Linux では kprobes と BPF を使ってカーネル内関数をトレースすることができます。 BCC は、kprobes と BPF を wrap して PythonC言語で便利にトレースプログラムを書けるようにしたものです。

BCC の雰囲気をつかむためには Examples 眺めてみるのがおすすめです。 「そんな情報取れるの!?」ってレベルで色々トレースしています。

例えば、biosnoop はブロックデバイスIOのレイテンシや、そのIOを発生させたプロセスのPIDなどを表示するプログラムです。 動かしてみるとこんな感じになります:

$ sudo /usr/share/bcc/tools/biosnoop
[sudo] password for nojima:
TIME(s)        COMM           PID    DISK    T  SECTOR    BYTES   LAT(ms)
0.000000000    prometheus     33872  sda     W  120713576 12288      0.48
0.002180000    jbd2/sda2-8    362    sda     W  60254856  524288     1.65
0.002844000    jbd2/sda2-8    362    sda     W  60255880  299008     2.31
0.041397000    jbd2/sda2-8    362    sda     W  60256464  4096       0.29
1.804266000    ?              0              R  -1        8          0.15
2.638785000    kworker/u480:1 55782  sda     W  13633792  4096       2.34
2.638831000    kworker/u480:1 55782  sda     W  1050648   8192       2.40
2.638857000    kworker/u480:1 55782  sda     W  1059752   106496     2.42
2.638892000    kworker/u480:1 55782  sda     W  22022984  16384      2.45
2.638914000    kworker/u480:1 55782  sda     W  22022952  4096       2.47
2.638945000    kworker/u480:1 55782  sda     W  22023120  12288      2.49
2.638990000    kworker/u480:1 55782  sda     W  22023160  16384      2.54

prometheus が何か書き込んでいることが見て取れます。

また、ext4distext4 へのオペレーションのレイテンシの分布を出力するプログラムです。 手元のVMで走らせてみると↓のようになりました。 この環境では fsync のレイテンシには2つの山があり、速い方だと 0.1ms~0.5ミリ秒程度、遅い方だと 30ms~130ms 程度だということがわかります。

$ sudo /usr/share/bcc/tools/ext4dist
Tracing ext4 operation latency... Hit Ctrl-C to end.
(中略)
operation = 'fsync'
     usecs               : count     distribution
         0 -> 1          : 1        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 0        |                                        |
       128 -> 255        : 177      |*******                                 |
       256 -> 511        : 98       |****                                    |
       512 -> 1023       : 1        |                                        |
      1024 -> 2047       : 1        |                                        |
      2048 -> 4095       : 2        |                                        |
      4096 -> 8191       : 5        |                                        |
      8192 -> 16383      : 3        |                                        |
     16384 -> 32767      : 20       |                                        |
     32768 -> 65535      : 870      |************************************    |
     65536 -> 131071     : 947      |****************************************|
    131072 -> 262143     : 60       |**                                      |
    262144 -> 524287     : 5        |                                        |
    524288 -> 1048575    : 3        |                                        |

他にも色々な例があるので、眺めてみると面白いと思います。

正直、example だけで十分な場合もかなり多いとは思いますが、BCC のメリットは何と言っても自分でカーネルの内部にハンドラをアタッチして自由に情報を取れることです。 この記事では、BCCで簡単なトレーシングプログラムの書き方を紹介します。

インストール方法

公式の INSTALL.md を参照してください。

Hello World in BCC

BPF で sync コマンドを打つたびに Hello, World! と表示するプログラムを書いてみます。 まず、以下の内容で hello.py を作成します。

from bcc import BPF

BPF_PROGRAM = r'''
#include <uapi/linux/ptrace.h>

int kprobe__sys_sync(struct pt_regs *ctx) {
    bpf_trace_printk("Hello, World!\n");
    return 0;
}
'''

bpf = BPF(text=BPF_PROGRAM)
bpf.trace_print()

次に、hello.py を root ユーザーで実行し、別の端末で sync コマンドを打ちます。 すると sync するたびに Hello, World! が出力されることが確認できると思います。 こんな感じ:

$ sudo python hello.py
           <...>-141094 [001] d... 5759646.157609: : Hello, World!
           <...>-141099 [000] d... 5759647.607684: : Hello, World!
           <...>-141104 [001] d... 5759648.419520: : Hello, World!

kprobe__関数名 という名前の関数を定義すると、その名前のカーネル関数にハンドラを仕掛けることができます。 上の例では kprobe__sys_sync という名前の関数を定義しているため、sys_sync が呼ばれる直前にこの関数が kprobe によって呼び出されます。

また、kretprobe__関数名 という名前の関数を定義すると、その関数から戻るタイミングにハンドラを仕掛けることができます。 次の例で使います。

また、Python 側から bpf.attach_kprobe(), bpf.attach_kretprobe() を読んでハンドラを設定する方法もあります。 詳しくはリファレンスを参照してください。

sync の時間を測る

次にもう少し実用的(?)な例を見てみましょう。次のプログラムは sys_sync の時間を計測し、print するものです。

from bcc import BPF

BPF_PROGRAM = r'''
#include <uapi/linux/ptrace.h>

BPF_HASH(start_at, u32, u64);

int kprobe__sys_sync(struct pt_regs *ctx) {
    u32 pid = (u32)bpf_get_current_pid_tgid();

    u64 ts = bpf_ktime_get_ns();
    start_at.update(&pid, &ts);

    return 0;
}

int kretprobe__sys_sync(struct pt_regs *ctx) {
    u32 pid = (u32)bpf_get_current_pid_tgid();

    u64 *p = start_at.lookup(&pid);
    if (!p)
        return 0;
    u64 ts_start = *p;
    u64 ts_end = bpf_ktime_get_ns();
    u64 delta = ts_end - ts_start;

    bpf_trace_printk("sync called: delta=%u\n", delta);

    start_at.delete(&pid);

    return 0;
}
'''

bpf = BPF(text=BPF_PROGRAM)
bpf.trace_print()

実行するとこんな感じになります。(別の端末で sync を打って下さい)

$ sudo python sync.py
           <...>-140474 [000] d... 5757530.462204: : sync called: delta=228816861
           <...>-140479 [000] d... 5757531.890676: : sync called: delta=58180443
           <...>-140484 [000] d... 5757532.781428: : sync called: delta=50627725
           <...>-140489 [000] d... 5757534.292865: : sync called: delta=58540044
  • 実行時間を計測するには、関数が呼ばれたタイミングと戻るタイミングでそれぞれタイムスタンプを取得し、その差を計算する必要があります。 そのため、kprobe__sys_synckretprobe__sys_sync を定義して、両方のタイミングでフックが呼ばれるようにしています。
  • 関数が呼ばれたタイミングで取得したタイムスタンプを戻るタイミングで呼ばれるハンドラに引き渡すためにハッシュ表を使っています。
    • ハッシュ表は BPF_HASH マクロを使って定義します。第一引数が変数名、第二引数が key の型、第三引数が value の型です。
  • bpf_get_current_pid_tgid() は PID と TGID を両方返す関数です。下位32ビットが PID で、上位32ビットが TGID です。
    • ややこしいんですが、ユーザーランドでよく PID と呼ばれている ID はここで言う TGID で、Thread ID と呼ばれているものがここでいう PID です。
  • bpf_ktime_get_ns() は現在のタイムスタンプを返します。

read のレイテンシの分布を出力する

以下のプログラムは2秒毎にファイルへの read のレイテンシの分布を出力します。

from bcc import BPF
import time

BPF_PROGRAM = r'''
#include <uapi/linux/ptrace.h>

BPF_HASH(start_at, u32, u64);
BPF_HISTOGRAM(dist, u32);

int kprobe__generic_file_read_iter(struct pt_regs *ctx) {
    u32 pid = (u32)bpf_get_current_pid_tgid();

    u64 ts = bpf_ktime_get_ns();
    start_at.update(&pid, &ts);

    return 0;
}

int kretprobe__generic_file_read_iter(struct pt_regs *ctx) {
    u32 pid = (u32)bpf_get_current_pid_tgid();

    u64 *p = start_at.lookup(&pid);
    if (!p)
        return 0;
    u64 ts_start = *p;
    u64 ts_end = bpf_ktime_get_ns();
    u64 delta = ts_end - ts_start;

    u32 key = bpf_log2l(delta);
    dist.increment(key);

    start_at.delete(&pid);
    return 0;
}
'''

bpf = BPF(text=BPF_PROGRAM)
while True:
    time.sleep(2)
    table = bpf['dist']
    table.print_log2_hist()
    table.clear()

実行するとこんな感じになります。

$ sudo python read.py
     value               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 0        |                                        |
       128 -> 255        : 0        |                                        |
       256 -> 511        : 0        |                                        |
       512 -> 1023       : 6        |****************************************|
      1024 -> 2047       : 1        |******                                  |
      2048 -> 4095       : 6        |****************************************|
      4096 -> 8191       : 1        |******                                  |
      8192 -> 16383      : 2        |*************                           |
     value               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 0        |                                        |
       128 -> 255        : 0        |                                        |
       256 -> 511        : 0        |                                        |
       512 -> 1023       : 0        |                                        |
      1024 -> 2047       : 0        |                                        |
      2048 -> 4095       : 0        |                                        |
      4096 -> 8191       : 0        |                                        |
      8192 -> 16383      : 1        |****************************************|

BPF_HISTOGRAM(dist, u32); というのがキモで、分布を保存するためのテーブルを作成し、レイテンシをカウントしています。

Python側からは bpf['テーブル名'] でこのテーブルにアクセスできます。 print_log2_hist() は分布をいい感じに print してくれる便利メソッドです。

テーブルには Python のディクショナリみたいにアクセスすることもできます。 例えば、以下の例はテーブルの内容をタブ区切りで一行で出力しています。

# 分布をタブ区切りで出力する
table = bpf['dist']
for k, v in sorted(table.items(), key=lambda x: x[0].value):
    sys.stdout.write("\t" + str(2 ** k.value) + ":" + str(v.value))
sys.stdout.write("\n")

まとめ

BCC で基本的なトレースを行うプログラムを紹介しました。 もっと知りたい人は公式の example を読んだり、reference guide を読みましょう。

さて、KMC Advent Calendar 2017 の次は aokabit さんです!!どんな記事か楽しみですね!!