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
$ fio 64M_sequentail_read.fio
$ fio 64M_random_read.fio
どの条件でもext4の方が、vboxsfより3倍以上速いことがわかります。
また、ファイルサイズが大きい方が、ext4/vboxsfの差が広がっていることがわかります。
差の原因を調べる
次に、この差の原因を調べるため、systemtapを使って、ファイルシステムアクセス時のkernel関数呼び出し回数を集計してみました。
集計対象にした関数は、generic_file_read_iter
及び、ext4モジュール及びvboxsfモジュールに属する関数です。systemtapのコードはこんな感じです。
これらの関数が実行される大雑把な流れとしては、以下の様になります。
f_op->read
に設定されているファイルシステムの関数が呼ばれる。(sf_file_read
、generic_file_read_iter
)- その中から、データをpage cacheに乗せる関数が呼ばれる。(
sf_readpage
、ext4_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_readpage
、vboxCallRead
はそれよりも多くなっています。
ここから、一回のシステムコールで、ホストへのアクセスが複数回実行されていることがわかります。
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を貼ると長すぎるので、違いがわかりやすい一部分を抜きだしてこちらに貼ります。
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_readpage
やext4_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するサイズが小さく、読み込むファイルが大きい場合、ホストへのアクセス数が増加してしまいます。図にするとこんな感じ。
一方、ext4の場合、先読みによって一度のシステムコールで読み込むページ数を増やしています。
さらに、a_ops->readpages
の実装によって、それらを一度のDiskアクセスで読み込んでいます。以下、イメージ図。
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
$ fio 64M_sequentail_read.fio
$ fio 64M_random_read.fio
まとめ
先読みを有効にし、readpagesを実装することで、ホストへのアクセスを減らし、readを速くすることができました。
VirtualBoxへ投げるのは、page cacheが取り込まれたら頑張ることにします。
kokukuma/vboxguestに、コードとインストール方法を乗せておきますので、使ってみたい人がいればこちらからどうぞ。