2013/03/24

ftrace marker と uprobe-based event tracer オーバヘッド比較

はじめに

ユーザ空間のプログラムのトレース情報を取得する方法には、さまざまなものがありますが、ここでは "ftrace marker" と "uprobe-based event tracer" を取り上げ、トレースオーバヘッドを測定してみます。
"ftrace marker" とは ftrace trace_marker ファイルにトレース情報を書き込むことで、ftrace でトレース情報を取得する機能です。Kernel のトレース情報とユーザプログラムのトレース情報を照らしあわせたい時等に便利です。
使い方は簡単で、tracing ディレクトリの trace_marker ファイルに write するだけです。

# echo test > /sys/kernel/debug/tracing/ftrace_marker
# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 1/1   #P:4
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
           <...>-23108 [003] ...1 34500.124363: tracing_mark_write: test

"uprobe-based event tracer" とは kprobe-based event tracer のユーザ空間版で、kprobe のように動的にプローブを差し込むことができる機能です。以下でも若干解説しますが、詳しい使い方については、ドキュメントをご覧ください。

比較方法
"ftrace marker" と "uprobe-based event tracer" をそれぞれ使って、トレースポイントを 1000回踏むのにかかる時間を測定します。
測定プログラムは以下を用意しました。
下の3つの関数をそれぞれ 1000回ずつ呼び出し、かかった時間を測定する簡単なものです。
do_nothing()  : 関数呼び出しのオーバヘッドを考慮するための何もしない関数
marker_write() : ftrace marker に write する関数
uprobe_test() :  uprobe を差し込む対象の関数

probench.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/time.h>

int marker_fd;

long time_usec(void) {
    struct timeval tv;

    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000000 + tv.tv_usec;
}

void probe_bench(void (*func)()) {
    long start_us, end_us, diff_us;
    int i, loop_max;

    loop_max = 1000;
    start_us = time_usec();
    for (i = 0; i < loop_max; i++) {
        func();
    }
    end_us = time_usec();
    diff_us = end_us - start_us;
    printf("Elapsed time(us): %ld\n", diff_us);
    printf("per trace point(us): %f\n", (double) diff_us / loop_max);
    printf("\n");
}

void do_nothing(void) {
}

void marker_write(void) {
    write(marker_fd, "test\n", 5);
}

void uprobe_test(void) {
}


int main(int argc, char *argv)
{
    char path[256];
    int trace_fd;

    strcpy(path, "/sys/kernel/debug/tracing/tracing_on");
    trace_fd = open(path, O_WRONLY);
    if (trace_fd >= 0)
        write(trace_fd, "1", 1);

    strcpy(path, "/sys/kernel/debug/tracing/trace_marker");
    marker_fd = open(path, O_WRONLY);

    printf("-- nothing --\n");
    probe_bench(do_nothing);

    printf("-- ftrace marker --\n");
    probe_bench(marker_write);

    printf("-- uprobe --\n");
    probe_bench(uprobe_test);

    close(marker_fd);
}
Makefile
probench: probench.c
 gcc -g -o $@ $< -O2

clean:
 rm probench

上記のプログラムを make したあと、uprobe を差し込む場所を指定します。
まず、ELF を読んでプログラムヘッダからテキストセグメント開始アドレスを探しましょう。
# readelf -e probench | less
...
プログラムヘッダ:
  タイプ        オフセット          仮想Addr           物理Addr
            ファイルサイズ        メモリサイズ         フラグ 整列
...
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000be4 0x0000000000000be4  R E    200000
テキストセグメント開始アドレスは0x400000とわかりました。
次に、uprobe_test 関数のアドレスは以下のコマンドで取得できます。
 # readelf -s probench | grep uprobe_test
    59: 0000000000400830     2 FUNC    GLOBAL DEFAULT   13 uprobe_test
アドレスは0x400830とわかりました。
uprobe_test にプローブを差し込みましょう。uprobe_test 関数のアドレス0x400830 から テキストセグメント開始アドレス 0x400000を引き算した 0x830 をuprobe_events に書き込みます。書き込み後はイベントをオンにしてやります。
# echo 'p:test /path/to/probench:0x830' > /sys/kerenel/debug/tracing/uprobe_events
# echo 1 > /sys/kerenel/debug/tracing/events/uprobes/test/enable
以上で準備がととのいました。ベンチマークプログラム : probench を実行しましょう。
# ./probench       
-- nothing --
Elapsed time(us): 8
per trace point(us): 0.008000

-- ftrace marker --
Elapsed time(us): 1742
per trace point(us): 1.742000

-- uprobe --
Elapsed time(us): 4255
per trace point(us): 4.255000

結果は、
ftrace marker : 1.7us/TracePoint
uprobe : 4.3us/TracePoint
でした。関数呼び出しのオーバヘッドは無視できるようです。
uprobe は ftrace marker と比べて、若干オーバヘッドが嵩むようです。

余談
uprobe-based event tracer ではなく、systemtap を使った場合でも測って見ました。
以下のようなスクリプトを実行したあとで probench を走らせます。
#!/usr/bin/stap

probe begin {
 printf("start\n")
}

probe process("/home/eiichi/git/probench/probench").function("uprobe_test") {
}

# ./probench
-- nothing --
Elapsed time(us): 8
per trace point(us): 0.008000

-- ftrace marker --
Elapsed time(us): 1766
per trace point(us): 1.766000

-- uprobe --
Elapsed time(us): 3994
per trace point(us): 3.994000
systemtap の場合は 4.0us/TracePoint でした。uprobe-based event tracer と変わらないようです。

まとめ
"ftrace marker" と "uprobe-based event tracer" のオーバヘッドを比較すると、前者より後者のほうが2倍以上大きいことがわかりました。トレースオーバヘッドを気にする かつ ユーザプログラムを改変できる場合は ftrace marker を使ったほうがいいでしょう。

参考情報
kernel/Documentation/trace/ftrace.txt
kernel/Documentation/trace/kprobetracer.txt
kernel/Documentation/trace/uprobetracer.txt

2013/03/11

Netperf でネットワークレイテンシ測定

Netperf は iperf と並ぶ多機能ネットワークベンチマークソフトです。 Netperf を使って latency(RTT: Round Trip Time) を測定する方法についてまとめました。

omni-test による latency 測定
最近の netperf は "omni-test" による測定を行うのが一般的のようです。
omni test で latency を測定しましょう。
ここで、latency とは送信サーバと受信サーバでパケットが往復するのにかかる時間(RTT)のことを言うとします。 まずは受信側で受信準備します。
# netserver
送信側で UDP で RTT を測定するコマンドを発行します。
# netperf -t omni -H localhost -- -d rr -T UDP -k MIN_LATENCY,MEAN_LATENCY,P90_LATENCY,P99_LATENCY,MAX_LATENCY,STDDEV_LATENCY
OMNI Send|Recv TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to localhost.localdomain () port 0 AF_INET
MIN_LATENCY=11
MEAN_LATENCY=44.34
P90_LATENCY=50
P99_LATENCY=74
MAX_LATENCY=3079
STDDEV_LATENCY=13.49
latency の単位は micro second です。 オプションについて簡単に説明します。
  -t : テストモード指定。ここでは omni テスト。
  -H : 受信サーバのホスト名またはIP addressを指定。
  -- : 以降が omni テストのためのオプションになります。
  -d : direction 指定。rr は request/response = 往復時間測定。
  -T : プロトコルを指定。ここでは UDP。他には TCP, SCTP も指定可能。
  -k : omni test 特有の出力項目セレクタ。
出力は レイテンシの最小値、平均値、90%分位点、99%分位点、最大値、標準偏差、などが出力可能です。

-k オプションで omni test で出力する項目を指定する項目は非常にたくさんあります
-k all とすると、すべて出力されるようになります。測定ログとして残すには十分でしょう。
# netperf -t omni -H localhost -- -d rr -T UDP -k all           
OMNI Send|Recv TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to localhost.localdomain () port 0 AF_INET
SOCKET_TYPE=Stream
PROTOCOL=UDP
DIRECTION=Send|Recv
ELAPSED_TIME=10.00
THROUGHPUT=22333.41
THROUGHPUT_UNITS=Trans/s
...
UUID=46412b12-8a4a-11e2-985e-e91ceb3a7ad7
MIN_LATENCY=11
MAX_LATENCY=2478
P50_LATENCY=44
P90_LATENCY=52
P99_LATENCY=77
MEAN_LATENCY=43.90
STDDEV_LATENCY=15.74
LOCAL_SOCKET_PRIO=-1
REMOTE_SOCKET_PRIO=-1
LOCAL_SOCKET_TOS=0xffffffff
REMOTE_SOCKET_TOS=0xffffffff
LOCAL_CONG_CONTROL=
REMOTE_CONG_CONTROL=
LOCAL_FILL_FILE=
REMOTE_FILL_FILE=
COMMAND_LINE="netperf -t omni -H localhost -- -d rr -T UDP -k all"

ヒストグラム出力

レイテンシの測定では、分位点などの要約統計量だけでなくヒストグラムが欲しいことがあります。
netperf でヒストグラムを出力する際は、コンパイルする際に、configure に --enable-histogram オプションを渡します。
# ./configure --enable-histogram
測定の際には -v 2 オプションを付け、 verbosity を 1 より大きくします。
# netperf -t omni -H localhost -v 2 -- -d rr -T UDP -k MIN_LATENCY,MEAN_LATENCY,P90_LATENCY,P99_LATENCY,MAX_LATENCY,STDDEV_LATENCY
OMNI Send|Recv TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to localhost.localdomain () port 0 AF_INET : histogram
MIN_LATENCY=11
MEAN_LATENCY=44.87
P90_LATENCY=46
P99_LATENCY=71
MAX_LATENCY=1447
STDDEV_LATENCY=7.58

Histogram of request/response times
UNIT_USEC     :    0:    0:    0:    0:    0:    0:    0:    0:    0:    0
TEN_USEC      :    0: 3031: 1094:  413: 197318: 9043: 4774: 1942:  441:  135
HUNDRED_USEC  :    0:  163:    4:    2:    3:    0:    1:    0:    0:    0
UNIT_MSEC     :    0:    1:    0:    0:    0:    0:    0:    0:    0:    0
TEN_MSEC      :    0:    0:    0:    0:    0:    0:    0:    0:    0:    0
HUNDRED_MSEC  :    0:    0:    0:    0:    0:    0:    0:    0:    0:    0
UNIT_SEC      :    0:    0:    0:    0:    0:    0:    0:    0:    0:    0
TEN_SEC       :    0:    0:    0:    0:    0:    0:    0:    0:    0:    0
>100_SECS: 0
HIST_TOTAL:      218365

余談

上記の測定には、対象として自身(localhost)を指定していました。
レイテンシの最大値: 1.4 ms という数字は、ping コマンドで RTT を測定した場合に比べて大きいです。これは、Kernel 内でパケットが折り返す ping(ICMP) とは違い、ユーザ空間までのパケット往復時間で図っているためです。 パケットがユーザ空間まで上がる分、スケジュールの関係で netperf や netserver プロセスの実行が遅れてしまうことがあるからです。

 参考文献
Care and Feeding of Netperf

2013/03/09

Qemu ゲストからの virtio-blk cache 設定確認

Qemu/KVM を用いた VPS(IaaS) を使っていると、Qemu virtio-blk Disk の cache  設定がどうなっているのか気になることがあります。実は、ゲストから以下のコマンドで cache 設定を確認できます。
(virtioXの数字Xはlspciコマンドなどで確認してください。)
[eiichi@f1 vda]$ cat /sys/bus/virtio/devices/virtio1/block/vda/cache_type
write back
"write through" ならば Disk Write Cache なし
(Qemu cache mode は "directsync", "writethrough" のどちらか)、
"write back" ならば Disk Write Cache あり
("none", "writeback", "unsafe" のどれか)です。
Qemu の cache mode について詳しくは、拙稿 Qemu cache mode まとめをご覧ください。

ゲストの kernel バージョンによっては上記の cache_type が用意されていないこともあります。そのときは以下のコマンドで virtio-blk デバイスの feature bits を参照することで確認できます。左から10番目が disk cache の有無を示す bit です。 1 なら有り、0なら無しです。
[eiichi@f1]~% cat /sys/bus/virtio/devices/virtio1/features
0010101101110000000000000000110000000000000000000000000000000000
この例では左から10番目が1なので disk cache "有り"です。
ちなみに、自分が借りている、"さくらのVPS"での feature bits は以下でした。
0010101001100000000000000000110000000000000000000000000000000000
さくらのVPSも disk cache 有りのようです。
自分のローカル環境との違いは SCSI コマンドサポートの有無と、disk cache の動的変更の可否です。
他の Feature bits に関する情報は qemu の hw/virtio-blk.h または Linux カーネルの virtio-blk デバイスドライバ drivers/block/virtio_blk.c から手に入ります。
 /* Feature bits */ 
 #define VIRTIO_BLK_F_BARRIER    0       /* Does host support barriers? */
 #define VIRTIO_BLK_F_SIZE_MAX   1       /* Indicates maximum segment size */
 #define VIRTIO_BLK_F_SEG_MAX    2       /* Indicates maximum # of segments */
 #define VIRTIO_BLK_F_GEOMETRY   4       /* Indicates support of legacy geometry */
 #define VIRTIO_BLK_F_RO         5       /* Disk is read-only */
 #define VIRTIO_BLK_F_BLK_SIZE   6       /* Block size of disk is available*/
 #define VIRTIO_BLK_F_SCSI       7       /* Supports scsi command passthru */
 /* #define VIRTIO_BLK_F_IDENTIFY   8       ATA IDENTIFY supported, DEPRECATED */
 #define VIRTIO_BLK_F_WCE        9       /* write cache enabled */
 #define VIRTIO_BLK_F_TOPOLOGY   10      /* Topology information is available */
 #define VIRTIO_BLK_F_CONFIG_WCE 11      /* write cache configurable */
 
 #define VIRTIO_BLK_ID_BYTES     20      /* ID string length */
新しく Qemu/KVM で構築された VPS などを借りてみたときは virtio-blk cache 設定を確認されてはいかがでしょうか。

systemtap で KVM ゲストとホストのTSC値を一致させる

はじめに

KVM でゲストを作成する際、KVMはご丁寧にもゲスト作成時の TSC をゼロから数えるためにホストのTSCからのズレ(オフセット)を用意します。ゲスト内で RDTSC 命令が発行されると、ホストのTSCにゲストのTSC offset を足したものを取得します。
ところで、ゲストからみたTSC値がホストからみたTSC値と異なると、ゲストの動作記録とQemuやホストOSの動作記録を突き合わせるときに、ゲストのTSC offsetを考慮しなければならず面倒です。そこで、systemtap を使ってカーネル無改造で手軽に TSC offset をゼロにしようと言うわけです。TSC offset が 0 になれば、ゲストとホストから見える TSC値は一致します。

TSC offset ゼロ化

KVM は ioctl KVM_CREATE_VCPU が発行されると、Virtual CPU を作成します。この際、現時点でのTSC値を取得し、その負値を tsc_offset にします。(vmx_write_tsc_offset@arch/x86/kvm/vmx.c)
vmx_write_tsc_offst 関数に systemtap の probe を差し込んで、guru モードで $tsc_offset = 0に書き換えてやればいいわけです。
systemtap の '-L' オプションで、probe を差し込む箇所を確認します。
[root@edge2]/home/eiichi/stap# stap -L 'module("kvm_intel").statement("vmx_write_tsc_offset")'
module("kvm_intel").statement("vmx_write_tsc_offset@arch/x86/kvm/vmx.c:1872")
さらに local 変数を表示させます。
[root@edge2]/home/eiichi/stap# stap -L 'module("kvm_intel").statement("vmx_write_tsc_offset@arch/x86/kvm/vmx.c:1872")'   
module("kvm_intel").statement("vmx_write_tsc_offset@arch/x86/kvm/vmx.c:1873") $vcpu:struct kvm_vcpu* $offset:u64
$offset で tsc offset が取得できます。
以下のような stap script を用意します。
[root@edge2]/home/eiichi/stap# cat tsc_offset_zero.stp 
#!/usr/bin/stap -g

probe begin { printf("start\n") }

probe module("kvm_intel").statement("vmx_write_tsc_offset@arch/x86/kvm/vmx.c:1872"){
 $offset = 0
 printf("VCPU : %x tsc offset changed to 0\n", $vcpu)
 print_backtrace()
}
実行するときには、stap -g オプションをつけましょう。'-g' オプションは guru モードといって、カーネル内変数を上書きできる素敵なモードです。
上のスクリプトを実行し、start が表示されたらゲストを起動します。
動作例はこんな感じ:
[root@edge2]/home/eiichi/stap# stap -g -d kernel -d kvm tsc_offset_zero.stp
start
VCPU : ffff88004ee23dd0 tsc offset changed to 0
 0xffffffffa022f9ad : vmx_write_tsc_offset+0xd/0x50 [kvm_intel]
 0xffffffffa01badcd : kvm_write_tsc+0x18d/0x2c0 [kvm]
 0xffffffffa0232c26 : vmx_create_vcpu+0x386/0x810 [kvm_intel]
 0xffffffffa01c43b7 : kvm_arch_vcpu_create+0x47/0x70 [kvm]
 0xffffffffa01aeb0e : kvm_vm_ioctl+0x2ae/0x5b0 [kvm]
 0xffffffff811a6529 : do_vfs_ioctl+0x99/0x580 [kernel]
 0xffffffff811a6aa1 : sys_ioctl+0x91/0xb0 [kernel]
 0xffffffff8163f9d9 : system_call_fastpath+0x16/0x1b [kernel]
VCPU : ffff88004ee23dd0 tsc offset changed to 0
 0xffffffffa022f9ad : vmx_write_tsc_offset+0xd/0x50 [kvm_intel]
 0xffffffffa01badcd : kvm_write_tsc+0x18d/0x2c0 [kvm]
 0xffffffffa0231f29 : vmx_set_msr+0x179/0x2a0 [kvm_intel]
 0xffffffffa01b4f29 : do_set_msr+0x19/0x20 [kvm]
 0xffffffffa01bcf60 : msr_io+0xc0/0x150 [kvm]
 0xffffffffa01c039b : kvm_arch_vcpu_ioctl+0x59b/0xf50 [kvm]
 0xffffffffa01acea8 : kvm_vcpu_ioctl+0x118/0x610 [kvm]
 0xffffffff811a6529 : do_vfs_ioctl+0x99/0x580 [kernel]
 0xffffffff811a6aa1 : sys_ioctl+0x91/0xb0 [kernel]
 0xffffffff8163f9d9 : system_call_fastpath+0x16/0x1b [kernel]

これでゲスト内で RDTSC によって取得する TSC値はホストと同じものになります。ゲストとホストでトレースを突き合わせる場合は、例えば systemtap なら get_cycles() でタイムスタンプを取得してゲストホストそれぞれでトレースデータを保存し、あとでマージします。
一度ゲストが起動してしまえば、ゲスト内で TSC書き換えが起こらない限り、上記の stap script を停止してしまっても大丈夫です。

Systemtap を使えば、ゲストOS-Qemu-ホストOS にまたがる動作記録を一貫して取得できるので大変便利です。Qemu/KVMの調査がはかどります。