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