ラベル linux の投稿を表示しています。 すべての投稿を表示
ラベル linux の投稿を表示しています。 すべての投稿を表示

2015/09/27

Hadoop : CPU system 使用率高騰 "zone_reclaim_mode = 1" 編

はじめに


 会社で PB 級の Hadoop クラスタを運用していますが、ある日から Datanode の CPU system (Kernel 内での CPU 使用率) が高騰し、Job が遅延するという症状が発現しました。Hadoop で CPU system 高騰というと、 Transparent HugePage 設定が有名ですが、そちらについては既に特定し、対策済みでした。 THP と Hadoop に関係については下記 Blog が詳しいです。
Transparent Huge Pages and Hadoop Workloads

 今回は THP ではなく、 "zone_reclaim_mode" の設定による性能劣化について、現象から原因特定に至るまでの経緯と、推奨する設定について解説します。

現象


 観測された現象について簡単に箇条書きします。
  1. CPU user が 5% 程度の時でも CPU system が30% を超えるなど、 Kernel 内での CPU 使用率が異常に高かった
  2. CPU 使用率高騰により、いくつかの Job 実行時間が、問題発生前と比較して 1.5 倍に増えた
  3. 一部のマシンで発生し、他のモデルのマシンでは発生しなかった

perf による原因調査


Kernel 内での CPU 使用率が高騰した際には perf と呼ばれる Linux Profiling Tool が非常に有用です。特別な準備をする必要なく、簡単に Profiling を取得できます。
 今回は Profiling により、 "どの関数で CPU を使用しているのか" 、 "どの処理で問題の関数が呼ばれるのか(Call Graph)" を調査します。

perf での Profiling 取得

 CPU system が高騰したタイミングを見計らい、下記コマンドでプロファイルを取得します。
perf record -F 99 -a -g -- sleep 30
取得した結果を表示。Call Graph が取得できます。
perf report

結果の一部:
-  33.01%             java  [kernel.kallsyms]                     [k] _spin_lock_irq
   - _spin_lock_irq
      - 98.89% shrink_inactive_list
           shrink_mem_cgroup_zone
           shrink_zone
           zone_reclaim
           get_page_from_freelist
         - __alloc_pages_nodemask
            - 89.56% alloc_pages_current
               - 79.46% __page_cache_alloc
                  - 99.76% grab_cache_page_write_begin
                       ext4_da_write_begin
                       generic_file_buffered_write
                       __generic_file_aio_write
                       generic_file_aio_write
                       ext4_file_write
                       do_sync_write
                       vfs_write
                       sys_write
                       system_call_fastpath
                     + 0x3fda40e6fd
               + 17.54% tcp_sendmsg
               + 1.75% __get_free_pages
               + 1.25% pte_alloc_one
            + 9.29% alloc_pages_vma
            + 1.15% kmem_getpages
+  14.24%             java  [kernel.kallsyms]                     [k] _spin_lock
+   4.75%             java  libjvm.so                             [.] SpinPause
+   4.03%             java  perf-1947.map                         [.] 0x00007fd9550209cd
+   2.64%             java  libsnappy.so.1.1.3                    [.] snappy::internal::CompressFragment(char const*, unsigned long, char*,
+   2.01%             java  libjvm.so                             [.] ParallelTaskTerminator::offer_termination(TerminatorTerminator*)
+   1.84%             java  [kernel.kallsyms]                     [k] __isolate_lru_page
+   1.58%             init  [kernel.kallsyms]                     [k] intel_idle
...

 ちなみに、上記の Call Graph を可視化した FlameGraph の画像は以下です。

perf 結果からわかること

  1. CPU system を高騰させているのは "spin_lock*" 関数であること
  2. "spin_lock*" 関数は "メモリ回収処理" の延長で呼ばれていること

 つまり、メモリが足りなくなったために、メモリ回収処理があまりに頻繁に呼ばれ、spin_lock のオーバヘッドが高騰したことが予想されます。さて、メモリ使用量について改めて観測すると、半分程度しか使用していません。にもかかわらずメモリ回収処理が頻繁に呼ばれるのは何故でしょうか...

 Linux のメモリ解放に関連するパラメータを洗い出してみると、一つの気になるパラメータがありました。"zone_reclaim_mode" です。有効になっている場合、NUMA 環境で zone 毎のメモリ回収が積極的に行われるようになります。デフォルトでは無効のはずですが、今回の該当マシン(CentOS 6系)で調べてみると、なんと有効になっていました。
zone_reclaim_mode について詳細: https://www.kernel.org/doc/Documentation/sysctl/vm.txt

zone_reclaim_mode 無効設定の結果


/proc/sys/vm/zone_reclaim_mode に 0 を設定したところ、問題の CPU system 高騰は収まりました。Job の実行時間も元の水準に戻りました。

なぜ zone_reclaim_mode が有効になっていたか


zone_reclaim_mode は一部のマシンで有効になっており、 CPU system が高騰していない別のマシンでは無効になっていました。同じ OS を使っていたのに、なぜ設定に違いが出たのでしょうか。Kernel のソースコードを読んで調べてみましょう。Kernel の Version は CentOS6(2.6.32-431.11.2.el6) とします。

 zone_reclaim_mode は default = 0 ですが、NUMA 環境における Node 間の距離(RECLAIM_DISTANCE) の値によっては、Kernel 起動時に 1 に修正されてしまうようです。
  mm/pagealloc.c
3096         /*
3097          * If another node is sufficiently far away then it is better
3098          * to reclaim pages in a zone before going off node.
3099          */
3100         if (distance > RECLAIM_DISTANCE)
3101             zone_reclaim_mode = 1;
 その閾値は "20" とあります。
 57 /*       
 58  * If the distance between nodes in a system is larger than RECLAIM_DISTANCE
 59  * (in whatever arch specific measurement units returned by node_distance())
 60  * then switch on zone reclaim on boot.
 61  */      
 62 #define RECLAIM_DISTANCE 20

 有効になっていたマシンの NUMA Node 距離を調べると... 21
$ numactl --hardware
...
node distances:
node   0   1 
  0:  10  21
  1:  21  10 

 無効になっていたマシンでは 20 でした。
 要するに、ハードウェア構成によっては zone_reclaim_mode が自動的に有効になってしまう様です。

最新の Kernel ではどうなっているか


ちなみに最新の Kernel では 下記 Commit により、 "ハードウェア構成によっては zone_reclaim_mode を自動で有効とする" 挙動が無効に変更されています。
http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=4f9b16a64753d0bb607454347036dc997fd03b82

他のソフトウェアでの推奨値


メモリを大量に使う DB 等のソフトウェアでの推奨値はやはり "無効" のようです。

まとめ

  • Hadoop Datanode での CPU system 高騰原因を perf を使って調査した
  • 原因: zone_reclaim_mode = 1 によるメモリ回収処理多発
  • ハードウェア構成によってい zone_reclaim_mode のデフォルト設定は変わる
  • Hadoop 含め、NUMA 環境でメモリを大量に使うソフトウェアで CPU system が高騰していた場合、 zone_reclaim_mode 設定を確認する

2014/07/24

Linux システムコールのブロック・ノンブロックまとめ

はじめに

Linux にはブロックするシステムコールとノンブロックなシステムコールがあります。さて、システムコールが「ブロックする」とはどういうことでしょうか。よく、ブロックするシステムコールとは「処理が完了するまでプロセスの動作が中断され待たされること」という説明を見ますが、より詳細に、どういう処理の場合に待たされるのか、整理してみましょう。

「ブロックする」とは

Linux において、システムコールがブロックするとは、「プロセスが、システムコール呼び出しの延長で待状態(TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE) に遷移し、CPU時間を消費せずにあるイベントが完了するのを待つようになる」、ことを指します。ブロックするシステムコールのうち代表的なものと、完了待ち対象イベントをまとめると、以下のようになります。


システムコール待ち対象イベント
read, write, fsyncディスク I/O
read, write, send, recv, accept, connect, select, poll, epoll FIFO読み書き可能
futex, flock ロック取得
nanosleep タイマ発火

待ち対象イベントをそれぞれ解説します。

ディスクI/O

ディスクをバックエンドとするファイルへの読み書きで、ディスクアクセスが生じる場合にはディスクへのI/O発行が完了するまでプロセスは待ち状態になります。例えば、読み込み対象のファイルがページキャッシュに乗っていない場合や、キャッシュを介さないダイレクトI/Oを発行した場合、fsync(2) でメモリ上の内容をディスクと同期させた場合、です。
なお、細かい話ですが、この場合の待ち状態は TASK_UNINTERRUPTIBLE という、シグナルを受け付けない待ち状態です。ディスクI/O以外の待ち対象イベントでは、TASK_INTERRUPTIBLEで待ちます。

キュー読み書き可能

パイプやソケットなど、キュー(FIFO)の構造を持つファイルを読み書きしようとした時に、キューが空で読み取れるデータがない場合と、キューが満杯でこれ以上書き込めない場合には、読み書きできる状態になるまでプロセスは待ち状態になります。キューに新しくデータが到着すると、キューが読み込み可能になります。キューに空きが出来ると、キューは書き込み可能状態になります。
read(2)、write(2)、select(2) については上記の説明で良いのですが、accept(2) は少し状況が違うので補足して説明します。
accept(2) はクライアントからの接続要求が キューにない場合にプロセスが待状態に遷移します。複数のプロセスが同じファイルに対し accept(2) を発行している時にクライアントからの接続要求があった場合、待状態にあるプロセスたちのうち、一つのプロセスのみが起床され、accept(2) 処理を実行します。一つのプロセスのみが起床されるのは所謂 Thundering Herd 問題への対処です。日本語の解説では、id:naoya さんによるprefork サーバーと thundering herd 問題 が詳しいです。

ロック取得

futex(2)は指定したアドレスに対応するキューでプロセスを待状態にしたり(FUTEX_WAIT)、指定したアドレスに対して待ち状態にあるプロセスを起床する(FUTEX_WAKE)同期機構です。
分かりやすくいうと、pthread_mutex_lock(3) などを通してロックを取得しようとしたが、すでに他のプロセスがロックを取得していた場合に、プロセスはFUTEX_WAIT で待ち状態に遷移します。flock(2) も同様です。

タイマ発火

所謂タイムアウトです。指定時間が経過するとプロセスが起床します。


「ノンブロック」とは

ブロックと対をなす概念:ノンブロックについても触れておきます。
ノンブロックなシステムコールとは、ブロックしないシステムコールのことです。システムコールをノンブロックにするには、対象とするファイルにfcntl(2)  でノンブロッキングフラグ(O_NONBLOCK)を付与します。ノンブロッキングフラグを付与されたファイルに対して、完了待ち対象イベントが「キュー読み書き可能」なシステムコールを発行すると、キューが読み書き可能でない場合、システムコールは即座に失敗し(return -1)、errno に EAGAIN が設定されます。キューが読み書き可能になるのを待ちはしません。
完了待ち対象イベントが「ディスクI/O」なシステムコールについては、ノンブロックには出来ませんが、その代わり非同期I/Oシステムコール io_submit(2) が用意されています。

まとめ

システムコールがどのような場合にブロックするのか、完了待ち対象イベント別に分けて説明しました。待ち対象イベントには大きくわけて「ディスクI/O」「キュー読み書き可能」「ロック取得」「タイマ発火」があります。「キュー読み書き可能」なシステムコールについては、ノンブロッキングフラグを付与することで、ノンブロックにできます。

ブロックするシステムコールは上記ですべてではありません。他にもちょくちょくありますが、細かいので省略します。

参考文献

各システムコール Man Page
Linux Kernel 3.15 ソースコード

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/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

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/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

2012/01/30

SystemTapで関数の呼び出し元を取得する

SystemTapはスクリプトで気軽にLinux Kernelの動作を調査できる素敵なツールです.
この記事は,SystemTapで関数の呼び出し元を取得する方法についてのメモです.
Tapset Referenceを参考にし,Context Functionsのfunction:callerを使用します.
最も簡単なものだと,このような形になります.


probe kernel.function("schedule").return {
  printf("caller : %s", caller())
}

出力はこんな感じ.Kernel内のschedule()関数を呼び出した関数名と,アドレスが出力されます.

[eiichi@fedora]~/work/stap% sudo stap schedule.stp
caller : cpu_idle 0xffffffffc04023d0
caller : cpu_idle 0xffffffffc04023d0
caller : run_ksoftirqd 0xffffffffc04558ed
caller : cpu_idle 0xffffffffc04023d0


さて,簡単な応用として,linux/kernel/sched.cで定義されているschedule()関数を呼び出している関数Top20を表示させてみましょう.
systemtap scriptは以下のようになります.


global callers

function print_top() {
  cnt=0
  printf("%-50s\tCount\n", "Caller")
  printf("---------------------------------\n")
  foreach( [name] in callers-) {
    printf("%-50s\t%5d\n", name, callers[name])
    if (cnt++ == 20)
      break
  }
  printf("---------------------------------\n")
}

probe kernel.function("schedule").return {
  callers[caller()]++
}

probe end {
  print_top()
  delete callers
}





eiichi@fedora]~/work/stap% sudo stap schedule_caller_top.stp -c "sleep 1"
Caller                                             Count
---------------------------------
cpu_idle 0xffffffffc04023d0                          332
worker_thread 0xffffffffc0468418                     166
schedule_hrtimeout_range_clock 0xffffffffc09202e7    116
futex_wait_queue_me 0xffffffffc047ec83                52
do_nanosleep 0xffffffffc0920158                       18
work_resched 0xffffffffc0921302                       17
schedule_timeout 0xffffffffc091fa05                   12
run_ksoftirqd 0xffffffffc04558ed                       8

---------------------------------




sleep ではcpu_idleが多くなります.
ちなみに,hackbenchを動かすと,このような感じです.

[eiichi@fedora]~/work/stap% sudo stap schedule_caller_top.stp -c "hackbench 10"
Running in process mode with 10 groups using 40 file descriptors each (== 400 tasks)
Each sender will pass 100 messages of 100 bytes
Time: 0.374
Caller                                             Count
---------------------------------
schedule_timeout 0xffffffffc091fa05                 1590
work_resched 0xffffffffc0921302                      735
cpu_idle 0xffffffffc04023d0                          349
worker_thread 0xffffffffc0468418                      83
rwsem_down_failed_common 0xffffffffc0920d25           22
schedule_hrtimeout_range_clock 0xffffffffc0920375     17
do_wait 0xffffffffc0452025                            10
schedule_hrtimeout_range_clock 0xffffffffc09202e7      5
run_ksoftirqd 0xffffffffc04558ed                       5
do_nanosleep 0xffffffffc0920158                        2
futex_wait_queue_me 0xffffffffc047ec83                 1
---------------------------------
ここで,schedule()関数ではなく,他の関数でも,呼び出し元Top20を表示させる場合を考えてみましょう.いちいちstap scriptを書き換えるのも面倒です.
付け焼刃的ですが,shell scriptと組み合わせると,簡単にできます.

#!/bin/sh
func=$1
cmd=$2
stap -e 'global callers
function print_top() {
  cnt=0
  printf("%-50s\tCount\n", "Caller")
  printf("---------------------------------\n")
  foreach( [name] in callers-) {
    printf("%-50s\t%5d\n", name, callers[name])
    if (cnt++ == 20)
      break
  }
  printf("---------------------------------\n")
}
probe kernel.function("'$func'").return {
  callers[caller()]++
}
probe end {
  print_top()
  delete callers
}' -c "$cmd"

使い方はこんな感じ.
linux/kernel/sched_fair.c のupdate_curr()関数で試してみました.
[eiichi@fedora]~/work/stap% sudo sh caller_top.sh update_curr "hackbench 10"
Running in process mode with 10 groups using 40 file descriptors each (== 400 tasks)
Each sender will pass 100 messages of 100 bytes
Time: 0.469
Caller                                             Count
---------------------------------
update_cfs_shares 0xffffffffc043c777               21460
put_prev_task_fair 0xffffffffc043d062              18516
dequeue_entity 0xffffffffc043cb79                  12369
enqueue_entity 0xffffffffc043d0e1                  12363
check_preempt_wakeup 0xffffffffc043c51f             9622
task_tick_fair 0xffffffffc043c90d                   1968
task_fork_fair 0xffffffffc043c308                    400
---------------------------------


ちなみにhackbench 10 は400個のtaskを生成するので,task_fork_fairからのupdate_curr呼び出し回数がちょうど400になってますね.

参考
RedHat Tapset Reference Manual