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の調査がはかどります。

2013/02/27

qemu VNC on WebSocket

はじめに

qemu 1.4 では Websocket プロトコル上で VNC を実現する機能が実装されました。
Qemu/Changelog/1.4
従来、ブラウザ(Websocket)でVNCを利用する場合、Websockify などを用いて、Websocket 通信を通常のソケット通信に変換しなければなりませんでした。今回 qemu VNC がWebsocket 対応したことにより、従来のような面倒な変換なしにブラウザ経由で VNC を利用できます。HTML5対応のブラウザであれば、Java プラグインなど無しに気軽に VNC を利用できるようになるのでとても便利です。

関連コミット:
http://git.qemu.org/?p=qemu.git;a=commit;h=7536ee4bc3da7e9b7fdadba5ba6ade63eaace430

使い方

qemu の configure 段階で vnc-ws を有効にします。
$ ./configure --enable-vnc-ws
以下のようなエラーがでるかもしれません。
ERROR
ERROR: User requested feature vnc-ws
ERROR: configure was not able to find it
ERROR
その時は、gnutls-devel パッケージをインストールします。
# yum install gnutls-devel
無事にインストールが済んだら、libvirt XML ファイルを編集しましょう。
コンパイルした qemu を使用するため、emulator タグで qemu の path を変更します。
また、qemu に "-vnc :1,websocket" オプションを渡すためのタグを追加します。ここで、"1:" はディスプレイポート番号です。"0:" だと既存のディスプレイと重なるかもしれないので、ずらしてあります。
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
...
  <devices>
    <emulator>/usr/local/bin/qemu-system-x86_64</emulator>
    ...
  </devices>
  <qemu:commandline>
    <qemu:arg value='-vnc'/>
    <qemu:arg value=':1,websocket'/>
  </qemu:commandline>
</domain>

仮想マシンを起動し、novnc.com にアクセスします。
noVNC.com は HTML5(Canvasなど) と Websocket で実装された、Web ベースの VNC クライアントです。 ブラウザは必ず HTML5 対応の新し目のブラウザを使いましょう。
右上の "Connect" ボタンをクリックし、
Host : 127.0.0.1
Port : 5701
を入力します。ポート番号は、上記で設定したディスプレイ番号に 5700 を足したものになります。今の場合だと、ディスプレイ番号 "1" + 5700 = 5701 です。
接続できると、こんな感じで、画面が表示されます。Chrome の中で Firefox を動かして見ました。

せっかくなので、iphone と Nexus 7 でも試して見ました。
Nexus 7


iphone



自宅の WiMAX環境では、iphone は重くて使い物になりませんでした。Nexus 7 もキーボードがいまいち使いにくかったです。

ともあれ、VNC on Websocket ではブラウザさえあれば、どこからでも VNC で画面転送できるのは便利です。家の仮想マシンを VPN 接続で iphone から操作する、なんてもこともできます。

余談

SPICE の HTML5 クライアントはないのかな?と思ったらありました。
HTML5 - SPICE
qemu が Spice on Websocket に対応していないので、 Websockify が必要です。

2013/02/25

qemu cache mode まとめ

はじめに

qemu の cache mode には "none", "direcsync", "writeback", "writethrough", "unsafe" の5つがあります。名前からでは、cache mode のそれぞれの仕組みがよくわかりません。そのために、どのようなケースでどの cache mode を使えばいいのか、判断に困ってしまいます。
ここでは、raw ディスクイメージについて、それぞれの cache mode の違いをまとめます。(qcow2 など他のディスクイメージフォーマットでは仕組みが若干ことなります)

Cache Mode 性質比較
それぞれの cache mode ではHost page cache や仮想的な Disk write cache を用いるかどうかで性質が変わってきます。表でまとめると以下のようになります。


cache mode Host page cache Disk Write cache NO Flush
directsync



writethrough


none


writeback

unsafe
✓ : on

Host page cache とは、qemu が 仮想マシンイメージファイルを open するときに O_DIRECT フラグをつけるか否かです。"none" と"directsync" では O_DIRECT フラグを"つける" ので、Host page cache を使いません。

Disk Write Cache とは、virtio-blk デバイスの持つ揮発性 cache です。現在では通常のHDDなどは32MB や 64MB のcache を持っていますが、Disk write cache は virtio-blk が持つ仮想的な cache です。qemu の中では Disk Write Cache を使うか否かで flush(fdatasync) のタイミングが異なります。使う場合はOSから flush 要求があった時に flush(fdatasync) しますが、使わない場合は disk write の度に flush(fdatasync)します。qemu が fdatasync を発行することで、Host の Disk にデータが書きだされますが、頻繁に fdatasync すると性能が劣化してしまします。

NO Flush とはqemuが保持している cache を disk へ flush するのを無効化します。無効化することで性能は上がりますが、Host クラッシュ時にゲストでファイル不整合が起こる可能性が増します。

以上を踏まえると、cache mode は以下のように分類できます。
こちらのほうがわかりやすいと思います。

Host page cache あり
Host page cache なし
Write 毎 にFlush
writethrough
directsync
Flush 要求時に Flush
writeback
none
Flush 無効
unsafe


性能比較
それぞれのcache mode のライト性能を、dd で測定しました。
環境は以下となっています。

  • HostOS: Fedora 18 (3.7.9-201.fc18.x86_64)
  • GuestOS : 同上
  • Host FS : ext4 (ordered)
  • Guest FS : 同上
  • Host memory : 4GB
  • Guest memory : 1GB
  • qemu : 1.4
  • qemu aio : threads
  • qemu disk type : raw

測定コマンドは
$ dd if=/dev/zero of=./zero bs=64K count=8K 
もしくは
$ dd if=/dev/zero of=./zero bs=64K count=8K oflag=direct
です。
Cache Mode の性質を比較するために、direct flag をつけた場合でも測定しています。

比較表

通常 write(MB/s)
oflag=direct(MB/s)
HOST
96.5
82.3
directsync
65.8
4.8
writethrough
62.8
4.6
none
88.3
54.2
writeback
80 ~ 400
80 ~ 400
unsafe
80 ~ 400
80 ~ 400

"writeback" と "unsafe" ではブレが非常に大きくなっています。これは Host の Page cache の影響です。 "directsync" と "writethrough" では、oflag=direct 時に著しく性能が落ちています。これは qemu が write 毎に fdatasync を発行するためです。

Cache mode の選定
さて、cache mode には色々あり、性質も大きく異なりますが、結局どの mode を選べば良いのでしょうか。指標としては、"メモリ消費量"、"性能"、"安全性" が挙げられます。

"メモリ消費量" で mode に大小をつけると以下のようになります。
directsync = none < writethrough = write back = unsafe
"directsync" と "none" では Host page cache を使用しないため少なくなります。他の mode では Host page cache が効くため、Guest と Host で二重にキャッシュが効いていることになります。つまり、同じデータがメモリ上に重複して存在してしまします。KSM(Kernel Samepage Merging)の仕組みがあるとは言え、キャッシュ重複はメモリを消費するため、好ましいものではありません。

"性能" での優劣は以下となります。
directsync < writethrough < none < writeback < unsafe
"writeback" と "unsafe" は広大な Host page cache を効かせる分、性能は良くなります。write 毎に fdatasync を発行する "directsync" や "writethrough" は性能は劣ります。

"安全性" での優劣は以下となります。ここで、安全性とは、ファイル整合性がとれるまでの時間が短いものを優とします。
unsafe < writeback < none < writethrough = directsync
fdatasync を頻繁に発行する "writethrough" と "directsync" はファイル整合性を取りやすいです。cache を効かせ、Flush 頻度が少ないものほど、安全性は劣ります。

以上を踏まえて、cache mode を選定します。
要件により、選定する cache mode は異なります。

1. とにかく安全第一。物理環境と同程度の安全性が欲しい。
⇛ directsync
cache mode で一番安全なのは O_DIRECT で fdatasync を頻発する directsync です。そのかわり、性能はいまいちです。

2. メモリ消費量は程程に、性能もある程度欲しい。
⇛ none
"none" は Host page cache を使わないため、メモリ消費もそれほど大きくありません。性能も Host には劣りますが、まずまずです。

3. メモリをどれだけ消費しようが、性能が欲しい。
⇛ writeback
"writeback" は広大な Host page cache を使うため、性能が出ます。反面、メモリ消費量は大きいです。

普通に VM をいじる分には "writeback" で。メモリ が多くない環境では "none" にするのがいいと思います。
ちなみに、自分はいつも "none" にしています。

参考文献
An Updated Overview of the QEMU Storage Stack
QEMU Emulator User Documentation
[Qemu-devel] Is cache=writeback safe yet

2013/02/18

qemu virtio-blk data plane の使い方

はじめに
qemu virtio-blk data planeとは、virtio-blk デバイスごとに、専用のIO threadを作り、QBL(qemu big lock)の外部で動作させることでlock contentionによる性能劣化を抑える試みです。専用のthreadを作り性能を向上させる試みは、ネットワークではすでにvhost-netを用いて行われていました。

関連コミット:
http://git.qemu.org/?p=qemu.git;a=commit;h=392808b49b6aee066d0c1d200e72fc3dc11c9d0f

簡単に仕組みを説明すると、virtio-blk デバイスごとに作られたIO threadはLinux Native AIOシステムコールを使い、非同期にIOを発行します。ゲストからの通知(ioeventfd)、およびゲストへの割り込み通知(irqfd)もこのIO threadが担当します。従来はqemuが行なっていた作業をglobal mutex外へと分離し、専用のthreadに任せることでScalabilityと性能を向上させることができるわけです。

virtio-blk data planeを用いることで、IOPSが14万から60万に上がったという報告もあります。
http://comments.gmane.org/gmane.comp.emulators.qemu/184821

しかし、現状ではvirtio-blk data plane には以下のような制限があります。
  • raw format のみのサポート
  • live migration 非サポート
  • hot unplug 非サポート
  • qemu での IO throttling が効かない
  • Host は linux のみサポート(Linux Native AIO syscallを使うため)

使い方
virtio-blk x-data-plane を使うには、まず条件として以下が必要です。

  • raw image formatでなければならない
  • wce(write cache enable: Guestのwrite cache設定を起動中に on/off 変更する機能) off
  • scsi off

「raw image なんてないよ、qcow2(or 3) しかないよ!」という方は以下のコマンドでraw image を作ってVMを新規作成しまししょう。
# qemu-img convert -O raw qcow.img raw.img
qemu を直接起動しても良いのですが、ここでは利便性のためにlibvirtを経由してqemuを扱うことにします。
上記の条件に合うように、libvirt XML file を作りましょう。
例えば、以下のようにします。

<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
...
  <devices>
    <emulator>/usr/local/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='raw' cache='none' io='native'/>
      <source file='/home/eiichi/vmimg/fraw.img'/>
      <target dev='vda' bus='virtio'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>
    </disk>
    ...
  </devices>
  <qemu:commandline>
    <qemu:arg value='-set'/>
    <qemu:arg value='device.virtio-disk0.config-wce=off'/>
    <qemu:arg value='-set'/>
    <qemu:arg value='device.virtio-disk0.scsi=off'/>
    <qemu:arg value='-set'/>
    <qemu:arg value='device.virtio-disk0.x-data-plane=on'/>
  </qemu:commandline>
</domain>

diskタイプをraw、AIOはLinux Nativeを指定します。
また、qemuの-setオプションを使うことで、x-data-plane=on オプションを有効にします。

評価
以下の環境で、bonnie++による書き込み性能評価を行いました。
  • Host OS: Fedora 18
  • Host Memory : 4G
  • Host FileSystem : btrfs
  • Guest OS: Fedora 18
  • Gues VCPU: 2
  • Guest Memory : 1G
  • Guest FileSystem : btrfs
  • Qemu : qemu v1.3.0-rc2
  • 測定コマンド : bonnie++
Guest data plane off:
Version  1.96       ------Sequential Output------ --Sequential Input- --Random-
Concurrency   1     -Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--
Machine        Size K/sec %CP K/sec %CP K/sec %CP K/sec %CP K/sec %CP  /sec %CP
fraw             2G   875  84 74157  13 21666   6  3128  78 50335   6 263.5  15
Latency             31893us     632ms    6507ms   83291us     299ms     728ms
Version  1.96       ------Sequential Create------ --------Random Create--------
fraw                -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--
              files  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP
                 16 21634  57 +++++ +++ +++++ +++ 22124  60 +++++ +++ +++++ +++
Latency               241us     593us     443us     108us      30us     138us

Guest data plane on:
Version  1.96       ------Sequential Output------ --Sequential Input- --Random-
Concurrency   1     -Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--
Machine        Size K/sec %CP K/sec %CP K/sec %CP K/sec %CP K/sec %CP  /sec %CP
fraw             2G   900  87 61252  12 24206   7  3296  82 91454  12 242.6  15
Latency             54769us    1189ms    5273ms   36272us     275ms     546ms
Version  1.96       ------Sequential Create------ --------Random Create--------
fraw                -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--
              files  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP
                 16  6949  18 +++++ +++ +++++ +++ 19625  52 +++++ +++ 32179  21
Latency              1069us    1450us     448us     112us     449us     173us

残念ながら、自分の環境だと性能に大きな違いは見受けられないようです。。
高性能なストレージを使ったり、qemu_global_mutex contention が大量に発生する環境で測定すると、また違った結果がでてくると思われます。

参考文献

An Updated Overview of the QEMU Storage Stack
Optimizing the QEMU Storage Stack
[Qemu-devel] [PATCH v2 0/8] virtio: virtio-blk data plane
Features/WriteCacheEnable