2013/12/03

blktrace で block IO の分布を可視化する

はじめに

blktrace は block IO の発行をトレースする有名なツールです。blktrace で集めたトレースデータを解析する btt には、ディスク上での block IO の分布を可視化する bno_plot.py というツールが付属しています。bno_plot.py を使うと、gnuplot を用いて下記のような図を生成できます。


図では、x軸が時間、y軸が Block Number、z軸が Block per IO になっています。これから、いつ頃に、どのセクタに、どのくらいのIOが発行されたかがわかります。

使い方

bno_plot.py の使い方をご説明します。
blktrace と gnuplot をインストールします。
# yum -y install blktrace gnuplot

blktrace でトレースを収集します。
# blktrace -w 30 -d /dev/sda -o sda

収集したトレース情報を、btt で解析します。解析したデータをファイルに出力します。
# btt -i sda.blktrace.2 -B sda_cpu2
サフィックスが r.dat, w.dat, c.dat になっているファイルが生成されます。それぞれ、Read, Write, Read and Write データに対応します。

解析データを bno_plot.py に与え、gnuplot で図を描画します。
# bno_plot.py sda_cpu2_8,0_c.dat
これで、上記の画像のような図が得られます。

Block IO の分布を可視化すると、どのように負荷がかかっているのかわかりやすいです。
また、@akiradeveloper 氏が開発している、dm-writeboost のように、ランダムライトをシーケンシャルライトに変換する特殊なディスクキャッシュについて、IO分布を可視化すると特徴が見て取れて面白いかもしれません。

2013/12/6 追記:
早速 @akiradeveloper 氏がdm-writeboost の block IO 可視化をしてくれました!ありがとうございます。
テストステ論: (writeboost report) writeboostのI/Oを可視化した
予想通りdmへのIOがランダムでも、その下のデバイスへのIOはシーケンシャルになっている様子が見て取れます。

2013/11/27

Erlang/OTP の systemtap トレース機能の使い方

はじめに

Erlang/OTP には systemtap と連携したトレース機能が備わっています。Erlang/OTP で systemtap 連携を有効にすると、systemtap から Erlang のトレースポイントを扱うことができます。トレースポイントを使うことで、BEAM 仮想マシンの挙動をより正確に把握したり、性能解析しやすくなります。

使い方

Erlang/OTP の ビルド

systemtap 連携を使うには、Erlang/OTP を configure --with-dynamic-trace=systemtap でビルドする必要があります。
# git clone git://github.com/erlang/otp.git
# cd otp
# ./otp_build autoconf
# ./configure --with-dynamic-trace=systemtap
# make

Erlang Shell を起動して [systemtap] と表示されていれば、systemtap が有効になっていることが確認できます。
# ./bin/erl
Erlang R16B03 (erts-5.10.4) [source-fb0006c] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [systemtap]

Eshell V5.10.4  (abort with ^G)

stap スクリプト起動方法

まず、stap -L コマンドで トレースポイントを確認しましょう。予め、beam バイナリファイルへの PATH を通しておいてください。
# PATH=/home/eiichi/git/otp/bin/x86_64-unknown-linux-gnu/:$PATH stap -L 'process("beam.smp").mark("*")' 
process("beam.smp").mark("aio_pool__add") $arg1:long $arg2:long
process("beam.smp").mark("aio_pool__get") $arg1:long $arg2:long
process("beam.smp").mark("bif__entry") $arg1:long $arg2:long
process("beam.smp").mark("bif__return") $arg1:long $arg2:long
process("beam.smp").mark("copy__object") $arg1:long $arg2:long
process("beam.smp").mark("copy__struct") $arg1:long
process("beam.smp").mark("dist__monitor") $arg1:long $arg2:long $arg3:long $arg4:long $arg5:long
process("beam.smp").mark("dist__output") $arg1:long $arg2:long $arg3:long $arg4:long
process("beam.smp").mark("dist__outputv") $arg1:long $arg2:long $arg3:long $arg4:long
process("beam.smp").mark("dist__port_busy") $arg1:long $arg2:long $arg3:long $arg4:long
...

約60個のトレースポイント(user-probe 系を除く)が確認できます。Linux Kernel のトレースポイントは 約 1200個、Qemu のトレースポイントが 約 900個なのと比較すると、若干少ないですね。

これらトレースポイントのうち、gc_major_start を例にトレースポイントの使い方を説明します。

まず、下記のような stap スクリプトを用意しましょう。(otp/lib/runtime_tools/examples より抜粋)
# cat garbage-collection.systemtap 
probe process("beam.smp").mark("gc_major-start")
{
    printf("GC major start pid %s need %d words\n", user_string($arg1), $arg2);
}

systemtap を有効にした Erlang Shell を起動し、下記のコマンドでsystemtap スクリプトを起動します。
# PATH=/home/eiichi/git/otp/bin/x86_64-unknown-linux-gnu/:$PATH stap garbage-collection.systemtap                    
Erlang Shell で適当に > "aaaaaaaaaa". などとコマンドを入力すると、stap スクリプトで下記のような出力が得られます。
GC major start pid <0 .33.0=""> need 9 words

GCの他にも、Message の送受信、プロセスの spawn、スケジュールなど、興味深く有用なトレースポイントがありあますので、お試しください。
systemtap スクリプトの例は lib/runtime_tools/example にあります。

dyntrace(user-probe)

Erlang/OTP で systemtap 連携を有効にすると、dyntrace モジュールを使って、 Erlang コードから動的にsystemtap スクリプトに情報を出力することができます。
Erlang/OTP に同伴されている、lib/runtime_tools/example/user-probe.systemtap を例にとって dyntrace モジュールの使いかたを説明します。

まず、user-probe.systemtap は beam 向けになっていますので、beam.smp 向けに直しましょう。user-probe.systemtap を開き、process('beam') となっているところを process('beam.smp') に書き換えます。
# vim user-probe.systemtap

書き換えたら、Erlang Shell を起動し、user-probe.systemtap を起動します。(beam に PATHを通しておいてください。)
# stap user-proeb.systemtap

この状態で、Erlang Shell にて、以下のように入力します。
2> dyntrace:p(1, 2, 3, 4, "a", "b", "c").
true
3> dyntrace:put_tag("test").
undefined
4> dyntrace:p(1, 2, 3, 4, "a", "b", "c").
true

すると、stap スクリプトでは、以下のような出力が得られます。
<0 .33.0="">  1 2 3 4 'a' 'b' 'c' 'c'
<0 .33.0=""> test 1 2 3 4 'a' 'b' 'c' 'c'

dyntrace:p/nで各種情報を出力します。dyntrace:put_tag/1 でトレース出力のプレフィックスを設定することができます。
詳しくはErlang User's Guide: dyntrace をご覧ください。

参考文献

Dtrace and Erlang: a new beginning
Erlang User's Guide: Systemtap and Erlang/OTP
Erlang User's Guide: dyntrace
Systemtap and Erlang: a tutorial
runtime_tools/src/dyntrace.erl

2013/11/22

OSS プロジェクト間の関連性を可視化してみました

はじめに


以前、OSS の開発活動を可視化する WEB サービス を作っていた時、 OSS プロジェクト相互の開発者の乗り入れ状況が気になりました。例えば 仮想マシンを管理するライブラリ libvirt と仮想マシンエミュレータ qemu では相当の共通開発者がいることが予想されます。また qemu と Linux Kernel も関係が深いはずです。
これらの関係を可視化することで、OSS プロジェクト同士の関連性を読み取ってみましょう。関連性を探ることで、OSS 開発者のプロジェクト間の移動や、潮流まで理解できるかもしれません。

デモ

と、いうわけで、Chord Diagram を用いて OSS プロジェクト間の関連性を可視化してみました。対象としたOSSプロジェクトは、自分の趣味で選んだ以下のものです。
"couchdb  libvirt  mongo    node  ocaml  perl     postgresql  redis  swift      virt-manager
cpython  linux    neutron  nova  otp    php-src  qemu        riak   systemtap"

下がその画像になります。

画像だけではいまいちわかりにくいので、インタラクティブなデモを用意しました。
こちらです : OSS Relationship Visualization
コードは GitHub で公開しています : Etsukata/ossrel
カーソルを各プロジェクトの弧に乗せると、そのプロジェクトと他プロジェクトの関連性のみが表示されます。
観察してみると、以下のことが読み取れました。

・仮想化(qemu, libvirt, virt-manager, OpenStack) は互いに関連性が強い
・Linux Kernel と qemu は関連性が強い
・OpenStack(nova, swift, neutron) 同士は関連性が強い
・Erlang/OTP と riak, couchdb は関連性が強い
・データベース(mongo, redis, riak, ...) と 仮想化(qemu, libvirt, ...) は関連性が薄い
・Ocaml とその他のOSS は関連性が薄い

などなど。
あらかた予想通りであることがわかりましたが、予想していなかったことがありました。

・ほとんどのプロジェクト同士は、関連性がある

複数のプロジェクトに貢献するのは、幅広い興味と知識が要求されるため、難しいことです。しかし、世の中にはそれが出来る人が多数いるんだなぁ、と感心させられました。

実装


実装について、簡単に記載します。
処理の流れは以下のようになっています。
1. 対象となるOSSの git commit logから author name を抜き出す
2. OSS プロジェクト同士(A, B)の関連性を以下の式で計算する
関連性 = sqrt(sum_{共通開発者} min(プロジェクトAへのコミット数, プロジェクトBへのコミット数))
平方根をとっているのは、Chord Diagram の見た目を整えるためです。
3. 関連性から隣接行列を生成する
4. 隣接行列を D3js に渡し、Chord Diagram で表示する。


ここでは、自分が選択したプロジェクト間のみ対象に可視化しましたが、可視化対象は自由に選択できますossrel を clone し、repos ディレクトリに対象となる git レポジトリを置き、run.sh を走らせるだけで、可視化できます。やり方は README.md に記載しました。
関連性が気になる OSS があったら、ぜひ可視化してみてください。

2013/10/31

iostat -x の出力を Linux Kernel ソースコードから理解する

はじめに


iostat は IO の出力速度や待ち時間の計測によく使われるコマンドです。"-x" オプションをつけると、平均待ち時間(await)やリクエストキュー長(avgqu-sz)、サービスタイム(svctm)などの詳細な情報を出力することができ、とても便利です。データベースをはじめとし、各種アプリケーションのパフォーマンスを計測するための重要な指標となります。
今回は、これらの出力結果について、より詳細かつ正確な意味を、Linux Kernelのソースコードを読んで理解しましょう。かなり長くなってしまったので、意味を把握したい方は下の方の "iostat -x 出力結果まとめ" をご覧ください。

iostatの挙動


まず、iostatの挙動を調べます。iostatは、read_sysfs_file_stat()で指定したインターバルごとに /proc/diskstats の情報を読み取り、compute_ext_disk_stats()で各種統計情報の計算を行い、結果をwrite_ext_stat()で出力します。read_sysfs_file_stat()、compute_ext_disk_stats()、write_ext_stat()の一部を下記に示します。

read_sysfs_file_stat():
int read_sysfs_file_stat(int curr, char *filename, char *dev_name)
{
...
    i = fscanf(fp, "%lu %lu %lu %lu %lu %lu %lu %u %u %u %u",
           &rd_ios, &rd_merges_or_rd_sec, &rd_sec_or_wr_ios, &rd_ticks_or_wr_sec,
           &wr_ios, &wr_merges, &wr_sec, &wr_ticks, &ios_pgr, &tot_ticks, &rq_ticks);

    if (i == 11) {
        /* Device or partition */
        sdev.rd_ios     = rd_ios;
        sdev.rd_merges  = rd_merges_or_rd_sec;
        sdev.rd_sectors = rd_sec_or_wr_ios;
        sdev.rd_ticks   = (unsigned int) rd_ticks_or_wr_sec;
        sdev.wr_ios     = wr_ios;
        sdev.wr_merges  = wr_merges;
        sdev.wr_sectors = wr_sec;
        sdev.wr_ticks   = wr_ticks;
        sdev.ios_pgr    = ios_pgr;
        sdev.tot_ticks  = tot_ticks;
        sdev.rq_ticks   = rq_ticks;
    }
...
}

read_sysfs_file_stat() では、/proc/diskstatsの各種フィールドを記録しています。これらパラメータが、カーネル内でどのような意味を持つかを詳細に理解するのが本資料の目的です。

compute_ext_disk_stat():
/*
 ***************************************************************************
 * Compute "extended" device statistics (service time, etc.).
 *
 * IN:
 * @sdc     Structure with current device statistics.
 * @sdp     Structure with previous device statistics.
 * @itv     Interval of time in jiffies.
 *
 * OUT:
 * @xds     Structure with extended statistics.
 ***************************************************************************
*/
void compute_ext_disk_stats(struct stats_disk *sdc, struct stats_disk *sdp,
                unsigned long long itv, struct ext_disk_stats *xds)
{
    double tput
        = ((double) (sdc->nr_ios - sdp->nr_ios)) * HZ / itv;
    
    xds->util  = S_VALUE(sdp->tot_ticks, sdc->tot_ticks, itv);
    xds->svctm = tput ? xds->util / tput : 0.0;
    /*
     * Kernel gives ticks already in milliseconds for all platforms
     * => no need for further scaling.
     */
    xds->await = (sdc->nr_ios - sdp->nr_ios) ?
        ((sdc->rd_ticks - sdp->rd_ticks) + (sdc->wr_ticks - sdp->wr_ticks)) /
        ((double) (sdc->nr_ios - sdp->nr_ios)) : 0.0;
    xds->arqsz = (sdc->nr_ios - sdp->nr_ios) ?
        ((sdc->rd_sect - sdp->rd_sect) + (sdc->wr_sect - sdp->wr_sect)) /
        ((double) (sdc->nr_ios - sdp->nr_ios)) : 0.0;
}

util, svctm, await, avgrq-sz パラメータについて、各種計算を行っています。

write_ext_stat():
/*
 ***************************************************************************
 * Display extended stats, read from /proc/{diskstats,partitions} or /sys.
 *
 * IN:
 * @curr    Index in array for current sample statistics.
 * @itv     Interval of time.
 * @fctr    Conversion factor.
 * @shi     Structures describing the devices and partitions.
 * @ioi     Current sample statistics.
 * @ioj     Previous sample statistics.
 ***************************************************************************
 */
void write_ext_stat(int curr, unsigned long long itv, int fctr,
            struct io_hdr_stats *shi, struct io_stats *ioi,
            struct io_stats *ioj)
{
...

    /*       rrq/s wrq/s   r/s   w/s  rsec  wsec  rqsz  qusz await r_await w_await svctm %util */
    printf(" %8.2f %8.2f %7.2f %7.2f %8.2f %8.2f %8.2f %8.2f %7.2f %7.2f %7.2f %6.2f %6.2f\n",
           S_VALUE(ioj->rd_merges, ioi->rd_merges, itv),
           S_VALUE(ioj->wr_merges, ioi->wr_merges, itv),
           S_VALUE(ioj->rd_ios, ioi->rd_ios, itv),
           S_VALUE(ioj->wr_ios, ioi->wr_ios, itv),
           ll_s_value(ioj->rd_sectors, ioi->rd_sectors, itv) / fctr,
           ll_s_value(ioj->wr_sectors, ioi->wr_sectors, itv) / fctr,
           xds.arqsz,
           S_VALUE(ioj->rq_ticks, ioi->rq_ticks, itv) / 1000.0,
           xds.await,
           r_await,
           w_await,
           /* The ticks output is biased to output 1000 ticks per second */
           xds.svctm,
           /*
            * Again: Ticks in milliseconds.
        * In the case of a device group (option -g), shi->used is the number of
        * devices in the group. Else shi->used equals 1.
        */
           shi->used ? xds.util / 10.0 / (double) shi->used
                     : xds.util / 10.0);    /* shi->used should never be null here */
...
}

compute_ext_disk_stat() で計算した結果と併せて、各種情報を出力しています。S_VALUEは差分をとってインターバルで割るマクロです。

/proc/diskstats 詳細



さて、iostat コマンドの挙動は把握できました。ポイントとなるのは、"/proc/diskstats では どのような情報を出力しているのか" です。これらを正確に理解するには、カーネルのソースコードを読む必要がでてきます。

まず、ソースコードを読む前にドキュメントを調べましょう。カーネルソースディレクトリ以下に/Documentation/iostats.txtがあります。以下に一部を抜粋します。

Documentaition/iostast.txt:
Field  1 -- # of reads completed
    This is the total number of reads completed successfully.
Field  2 -- # of reads merged, field 6 -- # of writes merged
    Reads and writes which are adjacent to each other may be merged for 
    efficiency.  Thus two 4K reads may become one 8K read before it is
    ultimately handed to the disk, and so it will be counted (and queued)
    as only one I/O.  This field lets you know how often this was done.
Field  3 -- # of sectors read
    This is the total number of sectors read successfully.
Field  4 -- # of milliseconds spent reading
    This is the total number of milliseconds spent by all reads (as 
    measured from __make_request() to end_that_request_last()).
Field  5 -- # of writes completed
    This is the total number of writes completed successfully.
Field  6 -- # of writes merged
    See the description of field 2.
Field  7 -- # of sectors written
    This is the total number of sectors written successfully.
Field  8 -- # of milliseconds spent writing
    This is the total number of milliseconds spent by all writes (as 
    measured from __make_request() to end_that_request_last()).
Field  9 -- # of I/Os currently in progress
    The only field that should go to zero. Incremented as requests are 
    given to appropriate struct request_queue and decremented as they finish.
Field 10 -- # of milliseconds spent doing I/Os
    This field increases so long as field 9 is nonzero.
Field 11 -- weighted # of milliseconds spent doing I/Os
    This field is incremented at each I/O start, I/O completion, I/O 
    merge, or read of these stats by the number of I/Os in progress
    (field 9) times the number of milliseconds spent doing I/O since the 
    last update of this field.  This can provide an easy measure of both
    I/O completion time and the backlog that may be accumulating.

iostat.txtに加え、Documentation/block/stat.txtにも有用な情報があります。
Name            units         description
----            -----         -----------
read I/Os       requests      number of read I/Os processed
read merges     requests      number of read I/Os merged with in-queue I/O
read sectors    sectors       number of sectors read
read ticks      milliseconds  total wait time for read requests
write I/Os      requests      number of write I/Os processed
write merges    requests      number of write I/Os merged with in-queue I/O
write sectors   sectors       number of sectors written
write ticks     milliseconds  total wait time for write requests
in_flight       requests      number of I/Os currently in flight
io_ticks        milliseconds  total time this block device has been active
time_in_queue   milliseconds  total wait time for all requests
Documentationを読むとフィールドの意味が大まかに理解できます。
ではソースコードを読んでいきます。/proc/diskstats を read した時に呼ばれるのは diskstats_show() です。

diskstats_show():
tatic int diskstats_show(struct seq_file *seqf, void *v)
{
    struct gendisk *gp = v;
    struct disk_part_iter piter;
    struct hd_struct *hd;
    char buf[BDEVNAME_SIZE];
    int cpu;
...
    while ((hd = disk_part_iter_next(&piter))) {
        cpu = part_stat_lock();
        part_round_stats(cpu, hd);
        part_stat_unlock();
        seq_printf(seqf, "%4d %7d %s %lu %lu %lu "
               "%u %lu %lu %lu %u %u %u %u\n",
               MAJOR(part_devt(hd)), MINOR(part_devt(hd)),
               disk_name(gp, hd->partno, buf),
               part_stat_read(hd, ios[READ]),
               part_stat_read(hd, merges[READ]),
               part_stat_read(hd, sectors[READ]),
               jiffies_to_msecs(part_stat_read(hd, ticks[READ])),
               part_stat_read(hd, ios[WRITE]),
               part_stat_read(hd, merges[WRITE]),
               part_stat_read(hd, sectors[WRITE]),
               jiffies_to_msecs(part_stat_read(hd, ticks[WRITE])),
               part_in_flight(hd),
               jiffies_to_msecs(part_stat_read(hd, io_ticks)),
               jiffies_to_msecs(part_stat_read(hd, time_in_queue))
            );
    }
...
}
part_stat_readマクロで hd_struct から各種情報を読み取っていることがわかります。
キーとなる構造体は disk_stats です。下記には、// 以下に対応する iostat -x のフィールドを追記してあります。

disk_stats:
struct disk_stats {
    unsigned long sectors[2];   /* READs and WRITEs */  // rsec/s wsec/s avgrq-sz
    unsigned long ios[2];  // r/s w/s
    unsigned long merges[2]; // rrqm/s wrqm/s
    unsigned long ticks[2];  // await r_wait w_wait
    unsigned long io_ticks; // %util svctm
    unsigned long time_in_queue; // avgqu-sz
};

disk_statsのメンバが更新されるタイミングを追っていきましょう。

hd_struct の各メンバが更新されるのは、blk_account_io_completion() および、 blk_account_io_done() 、drive_stat_acct() です。blk_account_io_completion() と blk_account_io_done() は、リクエスト完了時に呼ばれるblk_end_bidi_request() から呼ばれます。

コールグラフはざっと以下のような感じです。
blk_end_bidi_request()
-> blk_update_bidi_request()
  -> blk_update_request()
    -> blk_account_io_completion()
-> blk_finish_request()
  -> blk_account_io_done()

blk_account_io_completion():
static void blk_account_io_completion(struct request *req, unsigned int bytes)
{
    if (blk_do_io_stat(req)) {
        const int rw = rq_data_dir(req);
        struct hd_struct *part;
        int cpu;

        cpu = part_stat_lock();
        part = req->part;
        part_stat_add(cpu, part, sectors[rw], bytes >> 9);
        part_stat_unlock();
    }
}
blk_account_io_completion() では、IOが完了したバイト数をsectors に加算しています。

blk_account_io_done():
static void blk_account_io_done(struct request *req)
{
    /*
     * Account IO completion.  flush_rq isn't accounted as a
     * normal IO on queueing nor completion.  Accounting the
     * containing request is enough.
     */
    if (blk_do_io_stat(req) && !(req->cmd_flags & REQ_FLUSH_SEQ)) {
        unsigned long duration = jiffies - req->start_time;
        const int rw = rq_data_dir(req);
        struct hd_struct *part;
        int cpu;

        cpu = part_stat_lock();
        part = req->part;

        part_stat_inc(cpu, part, ios[rw]);
        part_stat_add(cpu, part, ticks[rw], duration);
        part_round_stats(cpu, part);
        part_dec_in_flight(part, rw);

        hd_struct_put(part);
        part_stat_unlock();
    }
}

blk_account_io_done() では、IO回数を表す ios をインクリメントし、 await の計算に使われる ticks に duration を加算しています。さらに実行中のリクエスト数(=リクエストキューの長さ)を part_dec_in_flight()でデクリメントしています。
durationについては、後ほど詳述します。

part_round_stats() では、その延長で、全てのリクエストがキューにいた時間の積算値を表すtime_in_queue と、デバイスがIOリクエストを発行して、未完了のIOが存在する時間を表す io_ticks を更新しています。io_ticksには前回のリクエスト完了から、今回のリクエスト完了までを加算し、訂正(@yohei-aさんのエントリ:「iostat はどのように %util を算出しているか(3)」を読んで間違いに気づきました。ありがとうございます!) io_ticksにはIO 発行中の時間(part_in_flightが0より大きい時間)を加算し、time_in_queueにはそれに実行中のリクエスト数を掛けたものを加算しているのがわかります。
static void part_round_stats_single(int cpu, struct hd_struct *part,
                    unsigned long now) 
{
    if (now == part->stamp)
        return;

    if (part_in_flight(part)) {
        __part_stat_add(cpu, part, time_in_queue,
                part_in_flight(part) * (now - part->stamp));
        __part_stat_add(cpu, part, io_ticks, (now - part->stamp));
    }    
    part->stamp = now; 
}

blk_account_io_done() で ticks に duration を加えています。duration は submit_bio の延長で呼ばれる blk_rq_init()でカウントが開始されます。
コールスタックは以下のような感じ。

submit_bio()
-> generic_make_request()
  -> blk_queue_bio()
    -> get_request()
      -> blk_rq_init()

blk_rq_init():
void blk_rq_init(struct request_queue *q, struct request *rq) 
{
...
    rq->start_time = jiffies;
...
}

最後に、hd_struct の merges を更新する drive_stat_acct() についてです。これは、リクエストをつくる際に、bioをリクエストにマージしようと試みる関数
bio_attempt_back_merge() または、リクエストを作成する blk_queue_bio() で呼ばれます。マージできたら merges カウントをインクリメントし、できなかったら実行中のリクエスト数をインクリメントし、io_ticksカウント用のstampを初期化(part_round_stat())します。
コールスタックは以下のような感じ。

submit_bio()
-> generic_make_request()
  -> blk_queue_bio()
    -> bio_attempt_back_merge()
       -> drive_stat_acct()
または
submit_bio()
-> generic_make_request()
  -> blk_queue_bio()
     -> drive_stat_acct()

drive_stat_acct():
static void drive_stat_acct(struct request *rq, int new_io)
{
    struct hd_struct *part;
...
    if (!new_io) {
        part = rq->part;
        part_stat_inc(cpu, part, merges[rw]);
    } else {
...
        part_round_stats(cpu, part);
        part_inc_in_flight(part, rw); 
...
}

以上でiostat -x の出力を読み解くカーネル内の情報は手にはいりました。

iostat -x 出力結果まとめ


カーネルのソースコードを読んで得た情報から、 iostat -x の出力フィールドそれぞれについて、詳細な意味をまとめます。

rrqm/s   wrqm/s :
一秒間にマージされたリード/ライト・リクエストの数。リクエストを作るgeneric_make_request()の延長で、bioをリクエストにマージしようと試み(bio_attempt_back_merge())、成功したらカウントされる。

r/s     w/s:
一秒間に発行したリード/ライト・リクエストの数。リクエストが完了した際に呼ばれるblk_end_bidi_request()の延長でカウント(blk_account_io_done())している。

rkB/s    wkB/s:
一秒間に発行したリード/ライト・リクエストが読み書きに成功したkB数。リクエストが完了した際に呼ばれるblk_end_bidi_request()の延長でカウント(blk_account_io_completion()) している。

avgrq-sz:
発行が完了したリクエストの平均サイズ。

avgqu-sz:
平均リクエストキューの長さの指標となる数値。正確にリクエストキューの長さというわけではなく、IOリクエストが作成されてから、発行が完了されるまでの総待ち時間値(time_in_queue)の平均。

await:
リード・ライトリクエストが作成(get_request())されてから、リクエスト完了(blk_finish_request())までにかかる時間の平均値。

r_await w_await:
それぞれ、awaitのリード版・ライト版。

svctm:
一回のIO発行にかかる時間(リクエストがキューに積まれている時間を含まない)の平均値。awaitとは異なり、IOスケジューラでリクエストがqueueに積まれている時間は含まれない。この数値によりデバイスがIOを発行してから完了にかかるまでの時間がわかる。

%util:
IO発行にかかる時間(リクエストがキューに積まれている時間を含まない)の、インターバルに占める割合(%)

以上、長くなりましたが、(若干端折りつつ)iostat -x の出力を Kernel ソースコードから追ってみました。ソースコードを追うと、それぞれの出力結果の意味をより正確につかむことができます。また、avgqu-szが正確にリクエストキューの長さの平均を表すものではなく、その指標になる数値(総待ち時間の平均)であることなど、発見がありました。

参考情報


Documentation/block/stat.txt
Documentation/iostats.txt
GitHub: sysstat/sysstat

2013/10/29

トレースデータを可視化する d3js_trace をつくりました

はじめに

ソフトウェアの性能解析をしている時、どこにCPUボトルネックがあるのか知りたくなることがあります。そういった場合、プロファイリングツールを使ってスタックトレースを採取し、CPUを消費している場所や割合に関する統計情報を解析するのが有効です。しかし、一般的に採取するデータ量は膨大になってしまい、欲しい情報を解析するのはなかなか骨の折れる作業です。
そこで、今回はトレース情報を可視化するツール:d3js_trace を作ってみました。d3js_trace は、perf で取得したスタックトレース情報を、JavaScript ライブラリ : D3js を用いて可視化します。可視化により、人間が解析しやすい形で表現することで、より容易にトレースデータを解析できるようになります。

コードについては GitHub に公開しています: Etsukata/d3js_trace



以下に掲載した画像は、perf でシステム全体のプロファイリングをとったものです。コマンドは "perf record -a -g fp sleep 3 "。どこでどのくらいCPUを消費したかが、階層を組んだ放射型のグラフにより表現されています。
画像をみるだけよりは、インタラクティブなデモをご覧いただけるほうが、理解しやすいと思います⇛デモ
図にカーソルを合わせると、CPU使用割合とコールスタックが表示されます。

使い方

d3js_trace は上記デモのような解析ページを生成します。その使い方をご紹介します。

まず git clone します。
# git clone https://github.com/Etsukata/d3js_trace.git
# cd d3js_trace

perf でスタックトレースを収集します。今回は例として、-a でシステム全体を対象に収集しています。
# perf record -g fp -a sleep 3

perf script コマンドでテキストデータで出力し、それを python スクリプトで d3js で読む JSON 形式に変換します。
# perf script | ./d3trace.py > trace_json.js

WEBブラウザで置いてある index.html を開きます。
# firefox index.html

すると、デモのようなページが表示されます。

TODO

d3js_trace は作ってみたばかりなので、色々と改良したいことがあります。

  • 色付け
  • ズーム可能にする
  • d3jsの他のExampleを使えるようにする
  • ftrace のトレース情報などを利用し、レイテンシトレースを可視化できるようにする

...などなど

Thanks to

d3js_trace は Brendan Gregg さんの FlameGraph を参考につくりました。また、データ可視化部には d3js の Sequence Sunburst を使っています。
素晴らしいツールを開発された方々に感謝します。

2013/10/12

Virsh で Qemu/KVM Live Block Migration

はじめに

仮想環境での Live Migration というと、仮想マシン移行元と移行先ホストでディスクを共有した上で行うのが一般的です。Live Block Migration は、共有ディスクが無い場合でも、仮想ストレージを移行させることにより Live Migration を実現する技術です。VMWare においては、Storage vMotion と呼ばれています。今回は、Qemu/KVM 環境において virsh を使った Live Block Migration の使い方をご紹介します。検証環境は Fedora 19です。

Live Block Migration には、仮想マシンの仮想ストレージすべてをコピーする Full モードと、Backing File との差分のみコピーする Incremental モードがあります。下記でそれぞれを紹介します。

Live Block Migration の使い方(Full編)

Live Block Migration を行う前に、前準備として移行先において仮想ストレージのスタブを準備する必要があります。(libvirt-devel に事前にスタブを作成するパッチが投稿されているため、この作業は必要なくなるかもしれません。パッチ: Pre-create storage on live migration)
まず、移行元ディスクの容量を調べます。ここではQcow2フォーマットを用いています。
[@src] # qemu-img info f19.img
image: f19.img
file format: qcow2
virtual size: 49G (52428800000 bytes)
disk size: 4.9G
cluster_size: 65536

移行先にて、スタブを作成します。移行元と同じパスに、移行元のディスク容量と同じ容量のスタブを作成します。
[@dst] # qemu-img create -f qcow2 f19.img 52428800000

移行元にて、virsh コマンドでマイグレーションを開始します。通常の Live Migrationでのオプションに加え、--copy-storage-all をつけることにより、Block Migration になります。
[@src] # virsh migrate --live --verbose --copy-storage-all f19 qemu+ssh://dst-ip/system
デフォルトでは port : 45192 を使うので、開けておきましょう。
すべてのストレージをコピーするため、マイクレーションには結構(数分)時間がかかります。
マイグレーションが完了したら、移行先で仮想マシンが稼働していることを確認しましょう。

Live Block Migration の使い方(Incremental編)

仮想ストレージ全てをコピーする Full モードは、かなり時間がかかってしまうという問題があります。Qemu/KVM には、事前にベースとなる仮想ストレージ(backing)を作成し、それとの差分のみを記録する、スナップショット機能があります。この機能との組みあせで Live Block Migration を行うと、backing との差分のみがコピーされるため、マイグレーション時間を短縮できます。

スナップショットの作成:
仮想マシンが稼働していない状態で行います。
[@src] # qemu-img create -f qcow2 -b base.img migrate-inc.img
-b でベースとなる backing file を指定し、migrate-inc.img を作成しました。
移行先でも同じコマンドでスタブを作成しておきます。
[@dst] # qemu-img create -f qcow2 -b base.img migrate-inc.img

移行元で、仮想ストレージに migrate-inc.img を指定した仮想マシンを作成し、起動しておきます。(説明略)

virsh コマンドで Incremental モードでの Live Block Migration を行います。
[@src] # virsh migrate --live --verbose --copy-storage-inc vm-name qemu+ssh://dst-ip/system
Fullモードとは違い、--copy-storage-inc オプションを使います。
backing との差分のみをコピーするので、Fullと比較して短い時間で完了します。

付録

Qemu の Live Block Migration はQemu 0.12 の頃から QMP コマンドの migrate -b が使われていましたが、新しめの Qemu(1.3 以降)では nbd と drive-mirror コマンドが使われるようになっています。参考:Qemu Wiki: Features/Virt Storage Migration
libvirt では Migration API がバージョン3まであり、Qemu が対応している場合は、新しい方法(nbd + drive-mirror)でマイグレーションし、対応していない場合は以前の方法にフォールバックするようになっています。参考:libvirt : virDomainMigrate3
nbd と drive-mirror による Live Block Migration については、以下のパッチに詳しい説明があります。
[libvirt] [PATCH v3 00/12 ] Rework Storage Migration

参考文献

Qemu Wiki: Features-Old/LiveBlockMigration
Qemu Wiki: Features/Virt Storage Migration
Qemu Wiki: Features-Done/ImageStreamingAPI
[libvirt] [PATCH v3 00/12 ] Rework Storage Migration
libvirt : virDomainMigrate3

2013/09/28

Qemu/KVM で CPU Hotplug を使う

はじめに

Hotplug とはマシンを停止せずにCPU、メモリなどのデバイスを追加する技術です。CPU Hotplug を仮想環境で用いると、仮想マシンを停止することなく仮想CPUを追加し、処理能力を強化することができます。これにより、仮想マシンの無停止スケールアップを実現できます。
Qemuはversion1.5よりCPU Hotplug機能をサポートしています。今回はQemuでのCPU Hotplugの使い方についてご紹介します。
検証環境はFedora19です。

Qemu のコンパイル

Hotplugのサポートは1.5以降です。Qemuのversionが1.5未満の場合は最新のQemuをコンパイルしましょう。
# git clone git://git.qemu.org/qemu.git
# cd qemu
# ./configure --target-list=x86_64-softmmu
# make
# make install

CPU Hotplugの使い方は、複数あります。以下では、QMPを用いる方法とlibvirt(virsh, virt-manager)経由でCPU Hotplugする方法を記載します。

共通の前提

CPU Hotplug機能を使うためには、あらかじめ、Qemu を起動する時のパラメータ: maxcpus を2以上にしておく必要があります。CPU Hotplug可能な数の上限は maxcpus となります。例えば、
qemu ... -smp 1,maxcpus=4
といった具合です。virsh では  vcpu タグの要素が maxcpus に対応し、current属性の値が -smp X の Xに相当します。

QMPでのCPU Hotplug

QMP(Qemu Monitor Protocol)でQemuと通信してCPU Hotplugを実施します。
QMPの使い方については下記のブログがとても詳しいです。
Multiple ways to access Qemu Monitor Protocol(QMP)
QMPで下記のコマンドを送信します。
> {"execute":"cpu-add", "arguments" : { "id" : 1 } }
arguments の id が Hotplug 対象の 仮想CPU です。この値は、0以上、maxcpus未満の整数をしていします。
あとはゲスト内でCPUをonlineにします。
# echo 1 >  /sys/devices/system/cpu/cpu1/online
/proc/cpuinfoなどで、Hotplugされたことを確認しましょう。

virsh での CPU Hotplug

libvirt が Qemu CPU Hotplug をサポートしているのは version 1.0.6.5 からですが、Fedora19 の libvirt 1.0.5.5 ではサポートされているので、それを使います。
関連コミット:
qemu: Implement new QMP command for cpu hotplug

注意: CPU Hotplugを使うには チップセットエミュレータのversionが1.5以上でないといけません。virsh edit で
<os>
  <type arch="x86_64" machine="pc-1.2">hvm</type>
</os>

<os>
  <type arch="x86_64" machine="pc-1.5">hvm</type>
</os>
に変更してください。
virsh setvcpus コマンドで仮想CPUをHotplugします。仮想マシンの名前は hotplug としています。
# virsh vcpucount hotplug
maximum      config         4
maximum      live           4
current      config         1
current      live           1
# virsh setvcpus hotplug
# virsh vcpucount hotplug
maximum      config         4
maximum      live           4
current      config         1
current      live           2
あとはQMPでの場合と同様に、ゲスト内でHotplugされたCPUをonlineにするだけです。

virt-manager での CPU Hotplug

注意: virsh での CPU Hotplug と同様に、チップセットエミュレータのversionが1.5以上であることを確認しましょう。同様に、 libvirt の version についても確認しましょう。
virt-manager での CPU Hotplugは実は簡単で、下記仮想マシンの詳細管理画面で、CPUの"現在の割り当て"部分をポチポチして"適用"ボタンを押すだけです。便利だなぁ。
後は、Hotplug されたCPUをゲスト内でonlineにしましょう。

Qemu guest agent との連携

Hotplug されたCPUをいちいちゲスト内で online にするの、めんどくさいですね。そんなときは Qemu guest agent と連携してホストから CPU を online にしましょう。ゲストにQemu 1.5 以降の guest agent をインストールして起動したあと、ホストから "guest-set-vcpus" コマンドで guest agent 経由で CPU を online にできます。

guest agent の設定の仕方については下記の記事が詳しいです。
lost and found(for me?) : Fedora 19 KVM : qemu-guest-agent

virsh で CPU を Hotplug したあと、guest agent 経由で online にします。
# virsh qemu-agent-command hotplug '{"execute":"guest-get-vcpus"}'
{"return":[{"online":true,"can-offline":false,"logical-id":0},{"online":true,"can-offline":true,"logical-id":1},{"online":true,"can-offline":true,"logical-id":2}]}
# virsh setvcpus hotplug 2
# virsh qemu-agent-command hotplug '{"execute":"guest-get-vcpus"}'
{"return":[{"online":true,"can-offline":false,"logical-id":0},{"online":false,"can-offline":true,"logical-id":1}]}
# virsh qemu-agent-command hotplug '{"execute":"guest-set-vcpus", "arguments" : { "vcpus" : [{"online":true,"can-offline":false,"logical-id":0},{"online":true,"can-offline":true,"logical-id":1}] }}'
{"return":2}

[root@edge2 qemu]# virsh qemu-agent-command hotplug '{"execute":"guest-get-vcpus"}'
{"return":[{"online":true,"can-offline":false,"logical-id":0},{"online":true,"can-offline":true,"logical-id":1}]}

/proc/cpuinfoでちゃんとonlineになってることを確認したらOKです。

参考文献

Qemu : qga/qapi-schema.json
Multiple ways to access Qemu Monitor Protocol(QMP)
lost and found(for me?) : Fedora 19 KVM : qemu-guest-agent

2013/08/29

FedoraでLXCを使う

はじめに

LXCはLinux上で複数の仮想的なLinuxを動作させることのできるOSレベルの仮想化技術です。一言で言えば、chroot に cgroups でのリソース管理を追加して強化したようなものです。Qemu/KVMのようなエミュレーションを行う仮想化よりもオーバヘッドが少なく、軽量です。Heroku のような PaaS 業者は、LXCの利点を生かし高集約なサービスを提供しています。さらにはJoe's Web Hostingのように、VPSをLXCで提供しているサービスすらあります。しかし、LXCではQemu/KVMとは違い、ホストとゲストで異なるOSを動作させることができません。仮想化による性能劣化が低い一方、柔軟性は一歩劣ると言えます。
今回は Fedora 19 で手軽に LXC を使う方法をまとめました。(Fedora18でも可能ですが、virt-manager が安定しないので、Fedora19以降がお勧めです)

環境構築

LXCでコンテナを構築する方法は大きく2つ有ります。一つは LXC 公式ツールキットを使う方法です。これは、最近はやりのdockerでも使われている方法なのですが、Fedora との相性がいまいちです。もう一つは、Qemu/KVMの管理でもおなじみのlibvirtを使う方法です。libvirtを使うと、仮想ネットワークの管理(DHCP, NAT)を含む仮想マシンの操作を、Qemu/KVMと同じ感覚で扱えるので、Qemu/KVMに慣れた方にはとてもおすすめです。今回はlibvirtでLXCを扱う方法について説明します。

root 環境のインストール:
yum で installroot を指定することで、init を実行する rootfs を構築できます。debootstrap と同じようなものです。
# yum -y --releasever=19 --nogpg --installroot /home/eiichi/lxc/base install systemd passwd yum fedora-release vim-minimal openssh-server procps-ng iproute dhclient
# chroot /home/eiichi/lxc/base /bin/passwd root

LXCは標準で/dev/pts/0 を使うので、securettyに追記して root でログインできるようにしておきます。
# echo "pts/0" >> /home/eiichi/lxc/base/etc/securetty

libvirtで使う:
virt-manager 経由で使うのが便利です。
ホストマシンに接続した後、新規仮想マシンの作成ボタンを押し、"コンテナーの種類"で"オペレーティングシステムコンテナ"を選択します。"既存のOSルートディレクトリを指定してください"ダイアログで、root環境を指定します。上記の例では、"/home/eiichi/lxc/base"となります(画像参照)。

あとは流れに沿って進めば、コンテナが起動します。

ネットワーク設定:
無事ログインできたら、ネットワーク設定を確認します。下記コマンドで veth ネットワークデバイスがみえるはずです。
-bash-4.2# ip -d l
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
13: eth0:  mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:16:3e:0b:ea:1c brd ff:ff:ff:ff:ff:ff
    veth 
さらに、libvirtのdefaultネットワークを用いているのであれば、dhcpでipをもらえるはずです。
# dhclient eth0
これで、NATで外部に接続することが出来るようになります。
全て完了しました。とても簡単です。

おまけ: btrfs での運用
上記では一つのコンテナを作る際の手順について説明しましたが、複数のコンテナを作る場合はどうでしょう。rootfs をコピーして使うのでは、各コンテナでrootfsの内容がほとんど変わらないのでディスク容量の無駄です。こんな時は、COW(Copy on Write)なbtrfsのsnapshotを使って無駄を省きましょう。内容としては、dockerがUnion FS(AUFS)を用いてやっていることとほぼ同等です。

btrfsの準備:
普段btrfsを使っていない場合は、loopback device で btrfs を利用する準備をしましょう。
# dd if=/dev/zero of=./btrfs.img bs=1M count=10k
# losetup /dev/loop0 btrfs.img
# mkfs.btrfs /dev/loop0
# mount -t btrfs /dev/loop0 ./mnt
これで./mnt で btrfs が使えるようになりました。base となるコンテナとして、subvolumeを切り、そこに rootfs を構築します。
# cd mnt
# btrfs subvolume create base
# yum -y --releasever=19 --nogpg --installroot /home/eiichi/lxc/mnt/base install systemd passwd yum fedora-release vim-minimal openssh-server procps-ng iproute dhclient
コンテナを新しく作る際には、base subvolume の snapshot を作成して構築します。
# btrfs subvolume snapshot base f19-1
あとは、作ったsnapshot directoryをvirt-managerでOSコンテナとして登録すればO.K.です。

まとめ
Fedoraにおいて、libvirtを使ってLXCを使う方法について説明しました。LXCは軽量な仮想化技術であり、とても簡単に管理できます。また、btrfs の snapshot と組み合わせることで、非常に効率的な運用が可能になります。

参考文献
Daniel P. Berrangé: Running a full Fedora OS inside a libvirt LXC guest
Stefan Hajnoczi: Thoughts on Linux Containers (LXC)
btrfs wiki: btrfs(command)

2013/08/03

SystemTap 埋め込みC関数のAPI変更について

はじめに

SystemTapはスクリプト内にC言語の関数を埋め込む機能を備えています。カーネル内の変数について詳しく調査したり、変数の内容を変更したりする際に埋め込みC関数がとても便利です。SystemTap 1.8 で埋め込みC関数内でのローカル変数アクセス方法が変更になりましたので、まとめておきます。さらに詳しい情報はSystemTap の NEWS に記載されています。

従来API(1.7以前)

従来、ローカル変数にアクセスする際、"THIS->var" 、"THIS->__retvalue" を用いていました。例えば以下のような感じです。
function add_one:long (val:long) %{
        THIS->__retvalue = THIS->val + 1;
%}

 新API(1.8以後)

新APIでは "THIS->var" ,"THIS->__retvalue" の代わりにマクロ "STAP_ARG_var", "STAP_RETVALUE" を用います。
function add_one:long (val:long) %{
        STAP_RETVALUE = STAP_ARG_val + 1;
%}
APIが変更された理由は、tapset によりインクルードされたヘッダとの変数名の衝突を防ぐためです。詳しくはSources Bugzilla – Bug 10299をご覧ください。

移行方法

1.7以前のAPIで書かれたstpスクリプトを1.8以後のSystemTapで実行すると、下記のようなエラーが起きるため、SystemTap のバージョンを1.8以後に移行する際には、なんらかの対処が必要になります。
/tmp/stapHdE3nB/stap_1f8c58b66994d073c51471dcf3f703ba_1070_src.c: In function 'function_add_one':
/tmp/stapHdE3nB/stap_1f8c58b66994d073c51471dcf3f703ba_1070_src.c:112:25: error: 'struct function_add_one_locals' has no member named 'val'
make[1]: *** [/tmp/stapHdE3nB/stap_1f8c58b66994d073c51471dcf3f703ba_1070_src.o] Error 1
make: *** [_module_/tmp/stapHdE3nB] Error 2
WARNING: kbuild exited with status: 2
Pass 4: compilation failed.  [man error::pass4]

移行方法1.  --compatible=1.7 オプションの利用

systemtap を実行する際に、--compatible=1.7 オプションをつけることで、スクリプトを変更せずに済みます。

移行方法2. /* unmangled */ pragma の利用

systemtap スクリプトの埋め込みC関数に /* unmangled */ プラグマを付与することで、従来APIと新APIを混在させることができます。
function add_one:long (val:long) %{ /* unmangled */ 
        THIS->__retvalue = THIS->val + 1;
%}

余談

この件、実はTwitter上で埋め込みC関数APIの変更を嘆いていた時に、SystemTap 主要開発者の Frank Ch. Eigler さん(@fche)から教えていただきました。 Frank さん、どうもありがとうございました。

参考文献

SystemTap Language Reference: 3.5 Embedded C
systemtap/NEWS



2013/07/21

VirtFS で Qemu ゲストホスト間ファイル共有

はじめに

Qemu/KVM 環境において、ホストゲスト間でのファイル共有ができると、とても便利です。例えば、開発中の Linux Kernel をテストする時には、ホストのコンパイル済み Kernel ソースディレクトリをゲストでマウントし、Kernel のインストールができると捗ります。ファイル共有方法には NFS、CIFS、SSHFS などがありますが、Qemu にはより効率的な "VirtFS" という仕組みがあります。
VirtFS は、ゲストの Linux マシンと virtio-9p デバイスを通じてファイル共有する仕組みです。ゲストホスト間で共有するリングバッファへの読み書きでデータをやり取りするため、他のネットワークファイルシステムなどより効率が良いのです。
今回は virt-manager での VirtFS を使ったファイル共有設定方法についてご紹介します。
Fedora 18で検証しています。(Fedora 15以上であれば Qemu が対応しています。)

virt-manager でのホストゲスト間ファイル共有設定

仮想マシン詳細を開き、「ハードウェアを追加」で 「FileSystem」を選択します。

ファイルシステム・パススルーの各項目を設定していきます。
上記の設定のように設定し、ゲストを起動しましょう。

「ターゲットパス」に指定したワードが、ゲスト上で 9pfs をマウントする際のマウントタグになります。

ゲストでのマウント
マウントタグの確認
# cat /sys/bus/virtio/drivers/9pnet_virtio/virtio<n>/mount_tag
source_tag
マウント
# mkdir source
# mount -t 9p -o trans=virtio source_tag ./source/
これでゲストから /path/to/source_dir がみえるようになりました。

注意

上記の設定では、ゲストからの共有ディレクトリへの書き込みが Permisson Denied になってしまいます。これは、qemu 起動ユーザが共有ファイルへのアクセス権を持っていない時に生じます。書き込みできるようにするためには、qemu を root で起動する必要があります。

qemu を root で起動するための libvirt 設定
/etc/libvirtd/qemu.conf
user = "root"
group = "root"
をコメントアウトし、libvirtd を再起動。

参考文献

Documentation/filesystems/9p.txt
libvirt: Domain XML format
Qemu Wiki : 9psetup
VirtFS LPC 2010
KVM日記 : Rootfs over Virtfsでゲストを起動する

2013/07/18

virsh で 仮想マシンのスナップショットを取る

はじめに

仮想マシン上で頻繁に環境構築・破壊を繰り返す場合、仮想マシンのスナップショットを利用し、素早くディスク状態をもとに戻せると便利です。libvirt, Qemu/KVM は仮想マシンのスナップショット機能を実装しており、とても有用です。今回は virsh コマンドでのスナップショットの扱い方をご紹介します。検証環境は Fedora 18です。

スナップショットの種類

libvirt, Qemu が実装している仮想マシンスナップショットの種類には、以下の2種類がありあます。

- 1. 内部スナップショット
- 2. 外部スナップショット

1. 内部スナップショットは仮想マシンのスナップショットを一つの qcow2 ファイルで管理する方式です。スナップショット取得中は仮想マシンは一時停止状態になります。仮想マシンのディスクのスナップショットのみならず、RAM 状態やデバイス状態などの仮想マシン状態も保存できます。

2. 外部スナップショットは仮想マシンのスナップショットを外部の qcow2 ファイルで管理します。なんと、仮想マシンを停止することなくスナップショットを取得できます。仮想マシンディスク以外の仮想マシン状態を保存することは、今のところできません。また、今のところ、仮想マシン停止中にはスナップショットを取ることができません。
現状動作が安定しておらず、非常に実験的な機能です。

以下、仮想マシンの名前を vm1 として、virsh コマンドの使い方を説明します。

内部スナップショット

内部スナップショットの作成
# virsh snapshot-create-as vm1 snap1 "snap1 description"
ドメインのスナップショット snap1 が作成されました
内部スナップショットは仮想マシン稼働中でもスナップショットを作成できます(ただし、安定していません)。作成している間は、仮想マシンは一時停止状態になります。ストレージ性能や仮想ディスク容量にもよりますが、作成時間は数分かかります。

内部スナップショット確認
# virsh snapshot-list vm1
 名前               作成時間              状態
------------------------------------------------------------
 snap1                2013-07-18 16:43:11 +0900 running

内部スナップショット復元
# virsh snapshot-revert vm1 snap1
スナップショットの復元についても、仮想マシン稼働中に実行可能です。ただし、復元中、仮想マシンは一時停止状態になります。

内部スナップショット情報の取得
指定のスナップショット情報を取得する際のコマンドは以下です。
# virsh snapshot-info vm1 snap1
名前:         snap1
ドメイン:   vm1
カレント:   はい (yes)
状態:         running
親:            -
子:            0
子孫:         0
メタデータ: はい (yes)

スナップショット復元後は下記コマンドで現時点でどのスナップショットを使用しているか確認できます。
# virsh snapshot-info vm1 --current
名前:         snap1
ドメイン:   vm1
カレント:   はい (yes)
状態:         running
親:            -
子:            0
子孫:         0
メタデータ: はい (yes)

スナップショット XML ファイルのダンプ
仮想マシンに関する設定情報(XML ファイル)を含んでいます。下記コマンドで設定情報を出力できます。
# virsh snapshot-dumpxml vm1 snap1

スナップショットの削除
# virsh snapshot-delete vm1 snap1


外部スナップショット

外部スナップショット作成
# virsh snapshot-create-as vm1 disksnap1 "disksnap1 description" --disk-only --atomic
ドメインのスナップショット disksnap1 が作成されました
外部スナップショットは仮想マシン実行中のみ取得可能です。内部スナップショットとは異なり、仮想マシンを停止(一時停止)することなく取得可能(Live Snapshot)です。つまり、仮想マシン無停止での Live Backup が可能です。
外部スナップショット作成後はディスクスナップショットイメージが作成され、current snapshot が作成したスナップショットになります。
# virsh snapshot-info vm1 --current
名前:         disksnap1
ドメイン:   vm1
カレント:   はい (yes)
状態:         disk-snapshot
親:            -
子:            0
子孫:         0
メタデータ: はい (yes)

外部スナップショット確認
# virsh snapshot-list vm1
 名前               作成時間              状態
------------------------------------------------------------
 disksnap1            2013-07-18 17:39:44 +0900 disk-snapshot
 snap1                2013-07-18 16:43:11 +0900 running

外部スナップショットが作成されると、仮想マシンイメージファイルを格納してあるディレクトリ(デフォルトでは /var/lib/libvirt/images)にスナップショットファイル(vm1.disksnap1)が新たに作成されます。
仮想マシンは新たに作成されたスナップショットファイルを使用するようになります。
# virsh domblklist vm1
ターゲット ソース
------------------------------------------------
vda        /home/eiichi/vmimg/vm1.disksnap1
hdc        -

外部スナップショット復元
外部スナップショットの復元は、virsh edit で仮想マシン設定 XML ファイルを開き、disk タグの source タグのfile 属性を復元したいディスクスナップショットに指定します。現状では ディスクスナップショットへの snapshot-revert は対応していないようです。
# virsh snapshot-revert vm1 disksnap2

エラー: サポートされない設定: 外部ディスクスナップショットへの復元はまだサポートされていません

注意
外部スナップショットはまだまだ開発段階の機能です。無停止でスナップショットが取れますが、動作が安定しないのが難点です。また、内部スナップショット機能についても、仮想マシン起動中のスナップショット取得はやはり安定して動作しないことがあります。
安定した動作を希望する場合、一番安全な、"仮想マシン停止時"  の "内部スナップショット" をおすすめします。

参考文献
fedoraproject : Features/Virt Live Snapshots
QEMU wiki : Features/Snapshot
libvirt : Snapshot XML Format
kashyapc fedorapeople : snapshot handout

python-requests の conflict で devstack が失敗する件

昨日(2013/7/17)、devstack による openstack のインストールを試したところ、以下のようなエラーが出て失敗してしましました。

2013-07-17 23:08:34 Processing dependencies for nova==2013.1.3.a39.g378c045
2013-07-17 23:08:34 error: Installed distribution requests 1.2.3 conflicts with requirement requests>=1.1,<1.2.3

python-requests の version conflict のようです。
結論から言うと、python-cinderclient の requirements.txt を下記のように変更することで回避できます。
[eiichi@q01]~/openstack/python-cinderclient% diff -u requirements.txt.org requirements.txt
--- requirements.txt.org2013-07-18 10:40:48.857523886 +0900
+++ requirements.txt2013-07-18 10:16:19.484897288 +0900
@@ -2,6 +2,6 @@
 pbr>=0.5.16,<0.6
 argparse
 prettytable>=0.6,<0.8
-requests>=1.1,<1.2.3
+requests>=1.1,<=1.2.3
 simplejson>=2.0.9
 six

参考情報

調べてみると、install された python-requests のバージョンが、python-cinderclinet の requirement と conflict しているようです。
% cat python-cinderclient/requirements.txt | grep requests
requests>=1.1,<1.2.3
requests のバージョンを cap するようになったのは 7/14 の下記のコミットからです。
Update to latest openstack/requirements

このコミットが入るようになった経緯は下記のバグが原因です。
BUG # 1200214 Relax OpenStack upper capping of client versions

python-requests のバージョンを CAP する理由が特に無いようにみえるため、おそらく上記の回避策で問題無い、と思います。問題があったら、すいません。

追記(2013/8/3)

devstack への下記コミットにより、本問題は修正されました。
Use unique build dir for pip installs

2013/07/16

Linux で VXLAN を使う

はじめに

VXLAN は VMware、Cisco、Redhat などが推進している VLAN に替わるネットワーク論理分割のための規格です。従来、IaaSなどのクラウド環境において、マルチテナントを実現するためには 802.1Q VLAN を用いるのが一般的な解決策でしたが、この VLAN には VLAN ID が 12bit しかないため、最大 4096 セグメントの分離しかできない、という問題があります。
VXLAN はこの問題を解決します。VLAN ID に対応する VNI(VXLAN Network Identifier) に 24bit を設け、 1,677万セグメントの論理分割を実現します。

VXLAN 類似の技術には Microsoft、Intel、Dell などが推進している NVGRE(Network Virtualization using Generic Routing Encapsulation) があります。実装の進み具合で判断すると、やはり VXLAN のほうが勢いがあるため、今後 L2 over L3 を実現するネットワーク論理分割の主流は VXLAN になる、と個人的には思っています。

今回はこの VXLAN の Linux での使い方をご紹介します。

環境

Qemu/KVM を利用した仮想環境で実施しています。
下記のような簡単な環境です。
|VM A(192.168.10.2/24)| --- | vbr | --- |VM B(192.168.10.3/24)|
VM A、VM B の二台が物理マシン上に作った仮想ブリッジに接続されています。
VM A、VM B の間に VXLAN で 仮想ネットワークを構築します。

使い方

VXLAN の実装はユーザ空間版もあるのですが、ここでは Linux Kernel での実装を使います。前準備として、Linux Kernel のバージョンが 3.7 以上である必要があります。
関連コミット:
vxlan: virtual extensible lan

iproute2 コマンドスイートのバージョンが低いと、VXLAN がサポートされていません。その場合は最新版をソースからコンパイルして入れましょう。VM A および VM B で実施します。
# git clone git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/iproute2.git
# cd iproute2
# ./configure
# make
# make install
# ip link help
(snip)
TYPE := { vlan | veth | vcan | dummy | ifb | macvlan | can |
          bridge | ipoib | ip6tnl | ipip | sit | vxlan }
TYPE に vxlan が含まれていれば、OKです。
もし、iproute2 のコンパイル時に "db_185.h がない" というエラーがでた場合は libdb-devel をインストルしましょう。
# yum install libdb-devel

さて、VXLAN を張る作業に入ります。
VM A、 B 上で下記のコマンドを入力します。
# ip link add vxlan0 type vxlan id 42 group 239.1.1.1 dev eth0
VXLAN は多くのトンネリング技術とことなり、1対Nでのトンネリングを行います。そのため、マルチキャストアドレスを指定します。
# ip -d link show vxlan0
4: vxlan0:  mtu 1450 qdisc noop state DOWN mode DEFAULT
    link/ether ba:ea:4d:a8:72:82 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 42 group 239.1.1.1 dev eth1 port 32768 61000 ageing 300
# ip link set up vxlan0
# ip maddr
1:  lo
(snip)
2:  eth0
(snip)
    inet  239.1.1.1
3:  vxlan0
(snip)
ip maddr で 239.1.1.1 が表示されていれば、適切にアドレス設定できていると確認できます。

VXLAN デバイスに アドレスを振って、疎通確認をします。
On VM A
ip a add 192.168.42.2/24 dev vxlan0

On VM B
ip a add 192.168.42.3/24 dev vxlan0

疎通確認
On VM A
ping 192.168.42.3
あなたの予想に反せずに pong が返っているでしょうか。pong が返らないようであれば、VM A,B および物理マシンの firewall 設定を確認してみてください。

下記に ping を送った際の wireshark 通信ダンプ結果を載せておきます。

最初に UDP Multicast で 239.1.1.1 に送信され、VM A の VXLAN トンネル終端がMAC アドレス学習後は Unicast で通信していることがわかります。

fdb は下記コマンドで確認できます。
On VM A
# bridge fdb show dev vxlan0
9e:03:cd:ab:b2:91 dst 192.168.10.3 self

参考文献

Documentation/networking/vxlan.txt
VXLAN: A Framework for Overlaying Virtualized Layer 2 Networks over Layer 3 Networks draft-mahalingam-dutt-dcops-vxlan-04
IPA : VXLAN/NVGREによるネットワーク分離

2013/07/15

iproute2 コマンドでルーティングテーブル/アドレス設定の保存/復元

はじめに

iproute2 はLinuxでネットワーク関係の設定を変更するためのコマンドスイートです。もはや非推奨となった ifconfig の代替として利用が推奨されており、ifconfig コマンドではでは行えない設定も可能です。iproute2 コマンドスイートの中には、ルーティングやアドレス設定を行う ip、トラフィック制御を行う tc、ネットワーク統計情報を取得する lnstat, ifstat コマンドなどが含まれます。iproute2 には非常に多くの機能が含まれているのですが、linux-net に併せて開発がとても早く、ドキュメントの整備が追いついていないのが難点です。

今回は ip コマンドを用いてルーティングテーブルおよび、アドレス設定の保存/復元を行う方法についてご紹介します。仮想ネットワークの構築を行うとき等に、ネットワーク設定を素早く手軽に復元できるこれらの機能が有用です。

設定の保存

設定のルーティングテーブル/アドレス設定の保存はそれぞれ ip route/ ip addr コマンドにより行います。
設定はバイナリ形式です。
ルーティングテーブルの保存:
% ip route save > iproute.conf.bin

アドレス設定の保存:
% ip addr save > ipaddr.conf.bin

設定の確認

設定はバイナリ形式なので、人手での確認は showdump オプションを使います。
% ip addr showdump < ipaddr.conf.bin
if1:
    inet 127.0.0.1/8 scope host lo
if2:
    inet 192.168.122.132/24 brd 192.168.122.255 scope global eth0
if2:
    inet 192.168.122.133/24 scope global secondary eth0
if1:
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
if2:
    inet6 fe80::5054:ff:fe30:28ac/64 scope link 
       valid_lft forever preferred_lft forever
% ip route showdump < iproute.conf.bin 
default via 192.168.122.1 dev if2  proto static 
192.168.122.0/24 dev if2  proto kernel  scope link  src 192.168.122.132

設定の復元

restoreコマンドで復元します。
% ip addr restore < ipaddr.conf.bin
% ip route restore < iproute.conf.bin

余談

iproute2 はドキュメント整備が不足していると述べましたが、これはかなりのつらみを感じます。今回もコマンドの使い方を学ぶためにソースコードとコミットログを参照しました。
コミットログによると、今回紹介した ip addr save/restore や ip route save/restore はcheckpoint-restartでも用いられているようです。
Add ip route save/restore
iproute: Add ability to save, restore and show the interfaces' addresses (resend)

参考文献

iproute2 - official
kernel/git/shemminger/iproute2.git
git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/iproute2.git

2013/06/23

OSSの開発活動を可視化するWebサービスをつくりました

Linux Foundation の報告書 "Linux Kernel Development: How Fast it is Going, Who is Doing It, What They are Doing, and Who is Sponsoring It" には Linux Kernle 開発の統計情報が数多く含まれており、開発の規模やスピード感が読み取れます。こういった統計情報は Linux Kernel のみならず、他の多くの OSS 開発プロジェクトにおいても有用です。今回作成した Web サービス "OSS Development Statistics" は git レポジトリのログから、月ごとのコミット数やディベロッパ数、コミット数ランキング、ホットな開発キーワードを表示します。

現在登録してあるプロジェクトは、自分が独断で選んだ以下のものです。要望があれば他にも追加いたします。@Etsukataまでお気軽にどうぞ。git レポジトリであれば簡単に追加できます。
  • libvirt 
  • linux 
  • mongo 
  • node 
  • openstack_nova
  • openstack_quantum 
  • openstack_swift 
  • qemu 
  • riak
現在、一日に一回 git pull してデータベースを更新しています。

サービスの作成にあたっては、MEAN Stack を使用しました。
MEAN Stack とは M : Mongodb, E : Express, A : Angular JS, N : Node.js を指し示します。クライアントサイド、サーバサイドともにJavaScriptで記述できる生産性の高いフレームワーク群です。MEAN Stackについては、mongodb blog に寄稿されている記事: The MEAN Stack: MongoDB, ExpressJS, AngularJS and Node.js が参考になります。

類似のことをするソフトウェアとしては、LWN.net の Jonathan Corbet 氏が主に開発している git-dm があります。Corbet 氏は 新しい Linux Kernel がリリースされる度に開発の統計情報をまとめた記事を投稿しますが、git-dmはその記事を書くために用いられているようです。
Linux Kernel 3.9 リリース時の記事:

今回本サービスを使ってみて、OpenStack-Nova の開発規模が QEMU を上回っていることに驚きました。OpenStack 全体では Linux Kernel の開発規模に迫りつつあるかもしれません。
今後は OSS プロジェクト相互の開発者の乗り入れ状況や、プロジェクト同士の開発規模、開発加速度の比較をしたいと考えています。

参考情報

2013/06/15

Mongodb の Aggregation Framework で月ごとに集計をとる

はじめに

mongodb ので Date Object を含むデータを集計する際に、月ごとで集計をとる方法について説明します。ここでは mongodb version 2.2 で導入された aggregation framework を用います。aggregation framework は非常に柔軟なフレームワークで、SQL に迫るほど強力な集計機能を備えています。map-reduce ほど表現力が豊かではありませんが、ほとんどの集計処理をより手軽に行えます。

早速 aggregation framework を用いて、月ごとに集計をとって見ましょう。

サンプル

まず、mongodb を起動し、mongo shell からサンプルデータを入力しておきます。
> db.revenue.insert({ revenue : 10 , date : ISODate("2013-06-20") })
> db.revenue.insert({ revenue : 5 , date : ISODate("2013-06-30") })
> db.revenue.insert({ revenue : 4 , date : ISODate("2013-05-30") })
> db.revenue.insert({ revenue : 6 , date : ISODate("2013-05-20") })
> db.revenue.insert({ revenue : 3 , date : ISODate("2013-05-10") })
> db.revenue.insert({ revenue : 1 , date : ISODate("2013-04-10") })
> db.revenue.insert({ revenue : 5 , date : ISODate("2013-04-20") })
> db.revenue.insert({ revenue : 2 , date : ISODate("2013-04-30") })
> db.revenue.find()
{ "_id" : ObjectId("51bbbf89d6ac2be7cae104b8"), "revenue" : 20, "date" : ISODate("2013-06-10T00:00:00Z") }
{ "_id" : ObjectId("51bbbfb5d6ac2be7cae104b9"), "revenue" : 10, "date" : ISODate("2013-06-20T00:00:00Z") }
{ "_id" : ObjectId("51bbbfc3d6ac2be7cae104ba"), "revenue" : 5, "date" : ISODate("2013-06-30T00:00:00Z") }
{ "_id" : ObjectId("51bbbfd9d6ac2be7cae104bb"), "revenue" : 4, "date" : ISODate("2013-05-30T00:00:00Z") }
{ "_id" : ObjectId("51bbbfe6d6ac2be7cae104bc"), "revenue" : 6, "date" : ISODate("2013-05-20T00:00:00Z") }
{ "_id" : ObjectId("51bbbfedd6ac2be7cae104bd"), "revenue" : 3, "date" : ISODate("2013-05-10T00:00:00Z") }
{ "_id" : ObjectId("51bbbff7d6ac2be7cae104be"), "revenue" : 1, "date" : ISODate("2013-04-10T00:00:00Z") }
{ "_id" : ObjectId("51bbc000d6ac2be7cae104bf"), "revenue" : 5, "date" : ISODate("2013-04-20T00:00:00Z") }
{ "_id" : ObjectId("51bbc008d6ac2be7cae104c0"), "revenue" : 2, "date" : ISODate("2013-04-30T00:00:00Z") }

これらのデータについて、月ごとの revenue を集計してみましょう。
今回使用する aggreagation framework のオペレータは 以下です。
$project : document を集計しやすくするために整形する(SQL での SELECT に相当)
$month : Date Object から月を示す数値を返す
$year : Date Object から年を示す数値を返す
$group : document を集計するためにグルーピングを行う(SQL での GROUP BY に相当
$sum : 数値を合計する

オペレータ一覧は下記ページにあります。
Aggregation Framework Reference

クエリは以下のようになります。
> db.revenue.aggregate(
  { $project : {
    ym : { 
      year : { $year : "$date" }, 
      month : { $month : "$date" }
    },
    revenue : 1}
  }, 
  { $group : {
    _id : "$ym" , 
    revenue_per_month : { $sum : "$revenue" }
    }
  })

{
 "result" : [
  {
   "_id" : {
    "year" : 2013,
    "month" : 5
   },
   "revenue_per_month" : 13
  },
  {
   "_id" : {
    "year" : 2013,
    "month" : 4
   },
   "revenue_per_month" : 8
  },
  {
   "_id" : {
    "year" : 2013,
    "month" : 6
   },
   "revenue_per_month" : 35
  }
 ],
 "ok" : 1
}

上記クエリについてすこし説明を加えておくと、まず、$project オペレータで Date Objectから年と月を取り出し、集計に使う revenue を抽出しておきます。$project オペレータに続いて、pipeline で $group オペレータを作用させ、実際に集計を行います。

上記で用いたオペレータ以外にも、$sort や $match など便利なオペレータがたくさんありますので、ぜひ試してみてください。

参考文献

MongoDB : Aggregation Framework Reference
SQL to Aggregation Framework Mapping Chart
MongoDBでゆるふわDB体験

2013/05/16

CVE-2013-2094(perfバグによるLinux権限昇格の脆弱性)まとめ

NIST により2013/5/14 に、 RHEL6.1 - 6.4 をはじめとする Linux ディストリビューションに、perf のバグをついて権限昇格される脆弱性があることがアナウンスされました。
Vulnerability Summary for CVE-2013-2094

影響範囲が大きいと思いますので、情報をまとめておきました。

Exploit Code:semtex.c
手元のCentOS 6.4 で試したところ、rootを取ることが出きました。

Does CVE-2013-2094 affect Red Hat Enterprise Linux and Red Hat Enterprise MRG?
解決策として、Systemtap スクリプトを guru モードで動作させ、動的にパッチを当てる手法が掲載されています。

RedHat Bugzilla CVE-2013-2094
書き込み「Petr Matousek 2013-05-14 19:36:43 EDT」において、上記のexploit:semtex.cでroot権限昇格についての詳しい説明があります。ありがたや。

IT media「Linuxに権限昇格の脆弱性、エクスプロイトも出まわる」

原因となったコミット

修正コミット

2013/05/09

Qemu のトレース新機能 "ftrace backend" 紹介

はじめに

Qemu のトレース新機能 "ftace backend" は Linux 標準のトレース機構 ftrace を使って Qemu と Linux Kernel(KVM) のトレース情報を併せて取得する機能です。Qemu で KVM を使う場合は、ユーザ空間(Qemu)とカーネル空間(Kernel)を頻繁に遷移するため、両空間のトレース情報を併せて取得できると、デバッグや性能解析がよりはかどります。

ftrace backend の実装には ftrace marker が用いられています。ftrace marker は debugfs の marker file への書き込みを ftrace のリングバッファに送る機能です。Qemu ftrace backend は、Qemu のトレース情報出力先を marker file にすることで実現しています。

関連コミット:
trace: Add ftrace tracing backend

2013年5月3日に Qemu Mainline にマージされました。おそらく Qemu 1.5 で使用できるはずです。Author の名前が自分と酷似していますが。。。

使い方

ftrace backend を使うためには、まず configure 時に trace backend として "ftrace" を指定する必要があります。
# ./configure --trace-backend=ftrace

このままでも ftrace backend はトレース情報を debugfs の trace file に記録できますが、今回は KVM を使うのでKVM 関係のトレースイベント情報も併せて取得するよう設定します。
# echo 1 > /sys/kernel/debug/tracing/events/kvm/enable

Qemu 起動時に、取得対象の Qemu trace イベントを指定します。
Qemu のトレースイベント一覧は Qemu ソースコードの trace-events ファイルに記載されています。また、 Qemu monitor から "info trace-events" コマンドによっても取得できます。
ここでは、すべてのイベントを取得するような設定にします。
% cat /home/eiichi/events 
*
Qemu を起動します。ftrace を使うため、必ず root 権限で起動しましょう。
# ./qemu-system-x86_64 -enable-kvm -trace events=/home/eiichi/events

得られるトレース出力は以下のようになります。
 # less /sys/kernel/debug/tracing/trace
snip...
 qemu-system-x86-23226 [002] d... 116142.685922: kvm_entry: vcpu 0
 qemu-system-x86-23226 [002] d... 116142.685923: kvm_exit: reason IO_INSTRUCTION rip 0xc45b info 700040 0
 qemu-system-x86-23226 [002] .... 116142.685924: kvm_pio: pio_write at 0x70 size 1 count 1
 qemu-system-x86-23226 [002] .... 116142.685925: kvm_userspace_exit: reason KVM_EXIT_IO (2)
 qemu-system-x86-23226 [002] ...1 116142.685943: tracing_mark_write: cpu_set_apic_base 00000000fee00900
 qemu-system-x86-23226 [002] ...1 116142.685946: tracing_mark_write: kvm_run_exit cpu_index 0, reason 2
 qemu-system-x86-23226 [002] ...1 116142.685947: tracing_mark_write: cpu_out addr 0x70 value 143
 qemu-system-x86-23226 [002] ...1 116142.685951: tracing_mark_write: kvm_vcpu_ioctl cpu_index 0, type 44672, arg (nil)
 qemu-system-x86-23226 [002] d... 116142.685954: kvm_entry: vcpu 0
snip...
tracing_mark_write と書かれているのが、Qemu(ユーザ空間)のトレース情報です。ここでは、ゲストCPUのIO port write命令を受けて VM_EXIT(reason KVM_EXIT_IO) したのちユーザ空間でエミュレーションが行われているのがわかります。

libvirt で使う場合

libvirt で ftrace backend を使う場合は、libvirtd の設定ファイルを以下のように変更し、Qemu を起動するユーザを root にする必要があります。
/etc/libvirt/qemu.conf に以下を追加:
user = "root"

 余談

ftrace marker を使うためのライブラリがあれば、もっとftrace backend のコードを短くできますし、他のアプリケーションでも手軽に ftrace marker が使えるようになって便利です。ライブラリ名は、"libftrace" などという名前になるのでしょうか。
最近、ftrace は snapshot 機能や、multiple buffer 機能が追加されています。ユーザ空間のプログラム内からそれらの機能を利用するライブラリがあるとさらに嬉しいです。

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