2015年11月11日

VirtualBoxのファイルシステムを10倍速くする 〜 read ahead編 〜

はてなブックマークに登録

vboxsfを速くするために頑張る記事の3本目です。

前回は、vboxsfでpage cacheを使えるようにして高速化を実現しました。

今回は、VirtualBoxのファイルシステムvboxsfと、VM上で使われているファイルシステムext4との違いを調べていきます。

もちろんvboxsfとext4では、ファイルシステムより下の構造が全く違います。

またvboxsfの場合、NFSと同様に複数のクライアント(vboxsfの場合、ホストOSやその他のゲストOS)からアクセスされるため、ext4ほどキャッシュを多用できないかもしれません。

とはいえ、何かしらvboxsfを速くするヒントが見つかるのではないか?と思い調べてみました。

比較してみる

とりあえず、vboxsfとext4でどの程度違いが出るのか調べてみました。使っているのは、前回の修正を取り込んだpage cache付きのvboxsfです。

ベンチマークには、fioを利用しました。

条件は、前回の記事に習い、以下の3つで試してみました。

  • 小さいファイルのsequentail read : fio_file
  • 大きなファイルのsequentail read : fio_file
  • 大きなファイルのrandom read : fio_file

また今回は、データがpage cacheに乗っていない状態からの計測をしたかったので、page cache、directry entry の cacheを削除した上で計測しています。

$ fio 64k_sequentail_read.fio
seq_64k
$ fio 64M_sequentail_read.fio
seq_64m
$ fio 64M_random_read.fio
rand_64m

どの条件でもext4の方が、vboxsfより3倍以上速いことがわかります。

また、ファイルサイズが大きい方が、ext4/vboxsfの差が広がっていることがわかります。

差の原因を調べる

次に、この差の原因を調べるため、systemtapを使って、ファイルシステムアクセス時のkernel関数呼び出し回数を集計してみました。

集計対象にした関数は、generic_file_read_iter及び、ext4モジュール及びvboxsfモジュールに属する関数です。systemtapのコードはこんな感じです。

これらの関数が実行される大雑把な流れとしては、以下の様になります。

  • f_op->readに設定されているファイルシステムの関数が呼ばれる。(sf_file_readgeneric_file_read_iter)
  • その中から、データをpage cacheに乗せる関数が呼ばれる。(sf_readpageext4_readpages)
  • さらにその中から、block / hostにアクセスする関数が呼ばれる。(vboxCallRead_ext4_get_block)

file size 64M, block size 64K, sequentail read の条件におけるfioの実行結果を以下に示します。

また、64Mのファイルを64Kでreadしていくので、システムコールreadの実行回数は1024回になります。

vboxsfの結果

$ sudo stap /vagrant/systemtap/syslog_profile.stp -c "fio seq_read_benchmark.fio"

# 実行回数, 実行しているプロセス, 実行されている関数
32768 fio(5820) vboxCallRead                <- ホストへのアクセス回数
17408 fio(5820) generic_file_read_iter
16384 fio(5820) sf_readpage                 <- データをpage cacheに乗せる回数
1024 fio(5820) sf_file_read                 <- ファイルを読み込む関数の実行回数
1024 fio(5820) new_sync_read
426 fio(5820) vboxCallCreate
424 fio(5820) sf_inode_revalidate
(省略)

結果を見ると、sf_file_readはシステムコールの呼び出し回数と同じですが、sf_readpagevboxCallReadはそれよりも多くなっています。

ここから、一回のシステムコールで、ホストへのアクセスが複数回実行されていることがわかります。

ext4の結果

$ sudo stap /vagrant/systemtap/syslog_profile.stp -c "fio seq_read_benchmark.fio"

# 実行回数, 実行しているプロセス, 実行されている関数
46594 kswapd0(22) merge
6686 kswapd0(22) list_sort
2304 kswapd0(22) __es_try_to_reclaim_extents
2183 kswapd0(22) __ext4_es_shrink
1028 fio(28327) ext4_map_blocks
1024 fio(28327) generic_file_read_iter        <- ファイルを読み込む関数の実行回数
771 fio(28327) ext4_ext_map_blocks
698 kswapd0(22) shrink_slab_node
514 fio(28327) _ext4_get_block                <- block layerへのアクセス回数
514 fio(28327) ext4_es_insert_extent
514 fio(28327) do_mpage_readpage
371 fio(28323) ext4_map_blocks
257 fio(28327) ext4_readpages                 <- データをpage cacheに乗せる回数
(省略)

ファイルを読み込む関数の呼び出し回数は、ext4 / vboxsfで違いはありません。

しかし、データをpage cacheに乗せる関数の実行回数が、ファイルを読み込む関数の実行回数よりも少なくなっています。

それに伴い、block layerへアクセスする回数も少なくなっているようです。

ここからext4 / vboxsfの速度の差は、ホストへのアクセス数の多さが一因となっているのではないかと考えられます。

少なくとも、ホストへのアクセス数を減らしてvboxsfのreadを速くする余地はありそうです。

コードを読む

続いて、「なぜext4でデータをpage cacheに乗せる関数の呼び出し回数が少ないのか」を調べるため、システムコールreadが実行された時のコードを読んでみます。

前回、vboxsfでもgeneric_file_read_iterを呼ぶようにしたので、そこから読んでいきます。

generic_file_read_iterを読む

まず、generic_file_read_iterを読むと、最後のほうでdo_generic_file_readが呼ばれています。

途中の処理は、directフラグを立てていた場合の処理なので、今回は無視します。

// linux-source-3.16/mm/filemap.c
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
    struct file *file = iocb->ki_filp;
    ssize_t retval = 0;
    loff_t *ppos = &iocb->ki_pos;
    loff_t pos = *ppos;

    /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */
    if (file->f_flags & O_DIRECT) {
        (省略)
    }

    retval = do_generic_file_read(file, ppos, iter, retval);  // <- こっちだけ
out:
    return retval;
}

つぎに、do_generic_file_readですが、この中は結構複雑です。

コードを読んでいるだけでは、この関数の基本的な役割はわかりますが、どのように呼び出されているかを把握するのは難しそうです。

そのため、systemtapを使って、callgraphを出力してみることにしました。

systemtapでcallgraphを出力する

callgraphを出力するためのsystemtapのコードはこんな感じにしました。

設定するprobが多すぎると、ERROR: probe overhead exceeded thresholdとか言われてしまいましたので、最低限確認したい関数だけに絞っています。

callgraphを貼ると長すぎるので、違いがわかりやすい一部分を抜きだしてこちらに貼ります。

vboxsfとext4のcallgraphの一部

ext4とvboxsfのcallgraphを比較してみると、以下2つの違いがあることがわかります。

  • ext4では、データをpage cacheに乗せる要求(ext4_readpages)が呼ばれない場合がある

    generic_file_read_iterが呼ばれたとき、vboxsfでは必ずsf_readpageが呼ばれています。

    しかし、ext4ではext4_readpagesを呼ばない場合があります。

  • vboxsfでは、データをpage cacheに乗せる要求が複数回呼ばれている

    1回のgeneric_file_read_iterに於いて、vboxsfではsf_readpageが複数回呼ばれています。

    一方、ext4ではext4_readpagesが、最大でも1回しか呼ばれていません。

次に、それぞれの違いについて考えていきます。

ext4でデータをpage cacheに乗せる要求が呼ばれない場合がある原因

これはデータ読み込み時にpage cacheにヒットしたためだと考えられます。

しかし、ベンチマーク時のpage cacheは毎回クリアされているにもかかわらず、まだ読み込み要求していないデータがキャッシュヒットするのはなぜでしょうか?

これは先読み(read ahead)が行われているためだと考えられます。

先読みとは..

Linuxカーネル2.6解読室 P.291 > リード処理では、ディスクからの読み込みをある程度順番に行っていることが予想される場合、その先のデータまでディスクからの読み込み要求を下位のデバイスドライバに対して事前に発行しておきます。現在読み込んだデータに対する処理が終わって、次のデータに対するリード要求が来た時に、ファイルキャッシュにヒットするという寸法です。この処理を先読み処理といいます。

つまり、ext4では先読みが行われているのでときどきしかext4_readpagesが呼ばれないのに対し、vboxsfでは先読みが行われていないので毎回sf_readpageを呼ぶ必要が有るということです。

確かにvboxsfのコードを読んで見ると、以下のような記述が見つかります。

//vboxsf/utils.c

int sf_init_backing_dev(struct sf_glob_info *sf_g)
{
    (省略)
    sf_g->bdi.ra_pages = 0; /* No readahead */      // <- これ
}

vboxsfはpage cacheを使わない方針のようなので、先に読んでも保存できないし、先読みは無効になっているのでしょう。

vboxsfでデータをpage cacheに乗せる要求が複数回呼ばれている原因

これは、sf_readpageext4_readpagesが呼ばれているコードを確認するのが早そうです。

readpageが呼び出されている場所を探すと以下が見つかりました。

これを見ると、複数ページ読み込む場合、ファイルシステムの実装によってやり方が異なっていることがわかります。

ext4の場合は、a_ops->readpagesが実装されているので一度に読み込んでいるようです。

一方、vboxsfではそれが実装されていないので、a_ops->readpageをforで回して取得することになります。

つまりvboxsfの場合、複数ページを読み込むためにはその回数分だけホストにアクセスしなければならないということです。

// linux-source-3.16/mm/readahead.c

static int read_pages(struct address_space *mapping, struct file *filp,
        struct list_head *pages, unsigned nr_pages)
{
(略)

    if (mapping->a_ops->readpages) {
        ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages);   // <- ここがreadpages
        /* Clean up the remaining pages */
        put_pages_list(pages);
        goto out;
    }

    for (page_idx = 0; page_idx < nr_pages; page_idx++) {
        struct page *page = list_to_page(pages);
        list_del(&page->lru);
        if (!add_to_page_cache_lru(page, mapping,
                    page->index, GFP_KERNEL)) {
            mapping->a_ops->readpage(filp, page);                          // <- ここがreadpage
        }
        page_cache_release(page);
    }

vboxsfをもっと速くする方法

以上から、vboxsfでは「先読みを有効にしていない」、「複数ページ読み込む実装がない」ということがわかりました。

そのため、readするサイズが小さく、読み込むファイルが大きい場合、ホストへのアクセス数が増加してしまいます。図にするとこんな感じ。

read

一方、ext4の場合、先読みによって一度のシステムコールで読み込むページ数を増やしています。

さらに、a_ops->readpagesの実装によって、それらを一度のDiskアクセスで読み込んでいます。以下、イメージ図。

readahead

vboxsfでも、これと同じことをすれば、もっと速くできそうです。

vboxsfを修正する

先読みを有効にし、複数ページを一度に読む実装をすることで、ホストへのアクセスを減らしてみます。

先読みを有効にする

先読みを有効にするためには、sf_g->bdi.ra_pages に何ページまで先読みを許すかを設定すればいいだけのようだったので、32ページまでと設定しました。

src/vboxguest-5.0.4/vboxsf/utils.c

複数ページを読み込めるようにする

複数ページを読み込む実装(sf_readpages)は、探してみるとやってる人が既にいました。

speedup: implement readpages() for bulk reads

しかし、バグっていたらしく、その後消されていました。

Remove sf_readpages because it's buggy'

せっかくなので、これをベースに修正を加えて使わせて頂きました。

src/vboxguest-5.0.4/vboxsf/regops.c

結果

この修正を取り込んだときのsystemtapの結果が以下になります。

想定通り、ホストへのアクセス数を減らせていることがわかります。

ext4のようにsystem callの呼び出し回数より小さくならないのは、先読みページ数の上限値がvboxsfよりも大きいからだと考えられます。

Linuxでの最大先読みページ数は、((512*4096)/PAGE_CACHE_SIZE)と定義されているので、ページサイズが4KBなら最大512ページとvboxsfよりも大きくなっている可能性が高いです。

vboxsfでも上限値をもっと上げればsf_readpagesの実行回数を減らすことができますが、vboxsfでホストのファイルを読み込む上限が16KBに制限されていたので、これ以上ホストへのアクセスを減らすことは難しいと考えられます。

$ sudo stap /vagrant/systemtap/syslog_profile.stp -c "fio seq_read_benchmark.fio"

# 実行回数, 実行しているプロセス, 実行されている関数
41323 kswapd0(22) merge
8192 fio(28295) vboxCallRead                  <- ホストへのアクセス回数
6497 kswapd0(22) list_sort
4096 fio(28295) sf_readpages                  <- データをpage cacheに乗せる回数
2963 kswapd0(22) __es_try_to_reclaim_extents
2846 kswapd0(22) __ext4_es_shrink
1112 kswapd0(22) shrink_slab_node
1024 fio(28295) sf_file_read                  <- ファイルを読み込む関数の実行回数
1024 fio(28295) generic_file_read_iter
1024 fio(28295) new_sync_read
944 kswapd0(22) shrink_page_list
257 fio(28295) __do_page_cache_readahead
176 fio(28295) vboxCallCreate
174 fio(28295) sf_inode_revalidate
(省略)

また、この時のfioの結果を以下に示します。

64MBのsequentail read / random readでは、vboxsf修正後の方が速くなっていることがわかります。

また64KBでは、vboxsf修正前後でそれほど大きな違いが出ていません。

これはもともとホストへのアクセス数が少ないため差がでにくいだけだと考えられます。

$ fio 64k_sequentail_read.fio
seq_64k_res
$ fio 64M_sequentail_read.fio
seq_64m_res
$ fio 64M_random_read.fio
rand_64m_res

まとめ

先読みを有効にし、readpagesを実装することで、ホストへのアクセスを減らし、readを速くすることができました。

VirtualBoxへ投げるのは、page cacheが取り込まれたら頑張ることにします。

kokukuma/vboxguestに、コードとインストール方法を乗せておきますので、使ってみたい人がいればこちらからどうぞ。

kokukuma2 at 09:00│Comments(0)TrackBack(0)

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント