2015年10月09日

VirtualBoxのファイルシステムを10倍速くする ~ find編 ~

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

もう、あって当たり前というところまで浸透してきた仮想環境、みなさまは何をお使いでしょうか?

私の周辺ではVirtualBoxがよく使われています。

典型的な使い方としては、 以下のような感じです。

  • ホストOSには、mac/windowsをつかう
  • ゲストOSには、Linuxを使う
  • 共有フォルダを使って、ホストとゲストでファイルを共有する

その中でも地味に重要なのが共有フォルダ。

共有フォルダとは、ホストOSのファイルシステムをゲストOSからマウントするための、VirtualBoxが提供している仕組みです。

しかし便利な反面、ファイルアクセスが非常に遅いという声をよく聞きます。

findが終わらないとか、git statusが遅すぎるとか...

この問題への対策を探してみると、下記のような物がみつかります。

しかし、vboxsfそのものへの対策は取られていないようです。

そこで、vboxsfと他のファイルシステムとの挙動や実装の違いを調べ、vboxsfを速くしてみようと思いました。

そのいくつか紹介していきたいと思います。

今回取り上げるのは、findが遅い件について。

検証した環境

検証した環境としては以下の通りです。

また、調査対象にしたディレクトリは、ファイル数3万、容量は4GBとなっています。

環境 ツール version
VirualBox 4.3.28
VMWare Fusion 7.1.2
ホストOS OS X Yosemite 10.10.2
ゲストOS Debian 8.1 (Jessie)
findutils GNU findutils 4.4.2
coreutils GNU coreutils 8.23
glibc 2.19
linux kernel 3.16.0-4-amd64
VBoxGuestAdditions 4.3.18
VMwareTools 9.9.3

現象を確認する

まずは、findコマンドが本当に遅いのか、他のファイルシステムと比較してみることにしました。

比較対象には、VMWareのホスト-ゲスト間のファイル共有で使われているvmhgfsを選びました。

これを選んだ理由は、vboxsfと仕組みが良く似ていることからです。

vboxsfとvmhgfsそれぞれで、findコマンド実行したときのtime/straceの結果を下に示します。

timeの結果を見ると、確かにvboxsfの方が遅くなっていることが確認できます。

またstraceで、実行されているシステムコールを確認してみると、vboxsfにおいて最も時間のかかっているシステムコールはnewfstatatで、その実行回数がvmhgfsと大きく異なる事がわかります。

ここから、vboxsf/vmhgfsの何らかの違いによってfindコマンド内で実行される処理が異なっていることがわかります。

parameter vmhgfs vboxsf
time(real) 0m7.205s 0m19.774s
time(sys) 0m5.740s 0m8.088s
// vboxsf
$ strace -c find .
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 76.59    0.509083          15     34145           newfstatat   <-ここ
 10.86    0.072196          73       993           openat
 10.24    0.068043          68      1005         6 open
  2.14    0.014249           0     34145           write
  0.14    0.000952           1       998           fstat
  0.03    0.000173           0      2013           getdents
  (省略)
// vmhgfs
$ strace -c find .
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 94.99    3.207002        1593      2013           getdents
  2.36    0.079565          79      1005         6 open
  1.96    0.066032          66       993           openat
  0.38    0.012751          13       993           newfstatat   <-ここ
  0.19    0.006551           0     34145           write
  0.12    0.004198           2      1993           close
  0.00    0.000139           0       998           fstat
  (省略)

コードを追う

つぎに、なぜvboxsf/vmhgfsでnewfstatatの呼び出し回数が異なるのかを調べてみました。

findのコード

コードを読み進めていった結果、newfstatatを実行するか否かは、以下で決められている事がわかりました。

// findutils-4.4.2/gnulib/lib/fts.c 1135-1140
            bool skip_stat = (ISSET(FTS_PHYSICAL)
                      && ISSET(FTS_NOSTAT)
                      && DT_IS_KNOWN(dp)
                      && ! DT_MUST_BE(dp, DT_DIR));
            p->fts_info = FTS_NSOK;
            fts_set_stat_required(p, !skip_stat);

dpはglibcの関数readdirを使って取得したdirent 構造体です。

dirent 構造体は、ファイルパスとinode番号をキャッシュしているもので、主にパスからinode番号への変換に使われています。

この判定ではd_typeを確認し、DT_UNKNOWN,DT_DIRであった場合に、statを実行することになっています。

また、gdbでvboxsf/vmhgfsでこの時の値を確認してみると、vboxsfでは常にdp->d_type=0 (DT_UNKNOWN)であるに対して、vmhgfsではdp->d_type=4 (DT_DIR)dp->d_type=8 (DT_REG)を返している事がわかりました。

確かに readdirの説明にも以下のように記されています。

d_type フィールドは Linux 以外では 主に BSD 系のシステムにだけ存在する. このフィールドを使うと その後の動作がファイルの種別により決まる場合に lstat(2) を呼び出すコストを避けることができる. 機能検査マクロ BSDSOURCE が定義された場合 glibc は d_type で返される値として以下のマクロ定数を定義する.

つまりこの問題は、vboxsfではd_typeを全てDT_UNKOWNで返しているために、statを余計に実行する必要があり、その分vmhgfsより遅くなっている事が原因であるといえます。

glibc / linux kernelのコード

続いて、なぜvboxsfでd_typeを全てDT_UNKOWNで返しているのか調べてみました。

そのために、readdirがどのようにファイルシステムから情報を取得しているのかを追う必要があります。

readdirは、glibc内に定義されており、以下のようにsyscall getdentsを呼んでいることがわかります。

# glibc-2.19/sysdeps/posix/readdir.c
      bytes = __GETDENTS (dirp->fd, dirp->data, maxread);

また、getdentsは、linux kernel内で定義されており、その中からfile->f_op->iterateが呼び出されています。

// linux/fs/readdir.c
    if (!IS_DEADDIR(inode)) {
        ctx->pos = file->f_pos;
        res = file->f_op->iterate(file, ctx);
        file->f_pos = ctx->pos;
        fsnotify_access(file);
        file_accessed(file);
    }

file->f_opはファイル操作を行うメソッド群で、ファイルシステムごとに定義されています。

vboxsfのコード

file->f_opはファイルシステムに定義されているので、ここからはvboxsfの実装をみていきます。

vboxsfにおいて、file->f_op->iterateは以下のように定義されています。

他のファイルシステムでも、適当にgrepすれば簡単に見つけられるでしょう。

// VirtualBox-4.3.28/src/VBox/Additions/linux/sharedfolders/dirops.c
struct file_operations sf_dir_fops =
{
    .open    = sf_dir_open,
    .iterate = sf_dir_iterate,
    .release = sf_dir_release,
    .read    = generic_read_dir,
    .llseek  = generic_file_llseek
};

そして、vboxsfのsf_dir_iterateを追っていくと、以下の記述が見つかりました。

// VirtualBox-4.3.28/src/VBox/Additions/linux/sharedfolders/dirops.c
        if (!dir_emit(ctx, d_name, strlen(d_name), fake_ino, DT_UNKNOWN))
        {
            LogFunc(("dir_emit failed\n"));
            return 0;
        }

dir_emitは、取得したファイル名とinode番号をディレクトリエントリとして、登録するための関数です。

このようにvboxsfでは、getdentsを実行した結果として、inode番号とファイル名だけ返しており、d_typeには全てDT_UNKNOWNを返していることがわかりました。

そのため、vboxsfでホスト側からdentryの種類を取得し、d_typeとして返すことができれば、vboxsfでもfindを速くすることができると考えました。

vboxsfを修正する

正しいd_typeを登録するためには、ホスト側でd_typeを取得しなければなりません。

dentryを取得するやり方を確認すると、ゲスト側で動いているvboxsfから、ホスト側で動いているサービスに結果を問い合わせていることがわかりました。

dentryを取得しているホスト側でのコードはmac/windowsそれぞれ以下のようになっています。

// VirtualBox-4.3.28/src/VBox/Runtime/r3/posix/dir-posix.cpp
RTDECL(int) RTDirRead(PRTDIR pDir, PRTDIRENTRY pDirEntry, size_t *pcbDirEntry)
...
            pDirEntry->INodeId = pDir->Data.d_ino; /* may need #ifdefing later */
            pDirEntry->enmType = rtDirType(pDir->Data.d_type);
            pDirEntry->cbName  = (uint16_t)cchName;
// VirtualBox-4.3.28/src/VBox/Runtime/r3/win/direnum-win.cpp
RTDECL(int) RTDirRead(PRTDIR pDir, PRTDIRENTRY pDirEntry, size_t *pcbDirEntry)
...
    pDir->fDataUnread  = false;
    pDirEntry->INodeId = 0; /** @todo we can use the fileid here if we must (see GetFileInformationByHandle). */
    pDirEntry->enmType = pDir->Data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY
                       ? RTDIRENTRYTYPE_DIRECTORY : RTDIRENTRYTYPE_FILE;
    pDirEntry->cbName  = (uint16_t)cchName;

これを見ると、dentryの情報を取得する際、d_type(pDirEntry->enmType)も一緒に取得されていることがわかります。

ここから問題は、ゲスト側まで値を返しているけどそれを設定していないだけであるようです。

そこでvboxsfで、こんな感じでホスト側から取得したd_typeを返すように修正を行いました。

結果

この修正を行った上で、先ほどと同様にfindコマンドを実行したときのtime/straceの結果を以下に示します。

以下のように、newfstatatの呼び出し回数が減っていることがわかります。

また、その分実行時間も短くなっており、vmhgfsと同程度の速度になりました。

parameter vmhgfs vboxsf (before) vboxsf (after)
time(real) 0m7.205s 0m19.774s 0m3.385s
time(sys) 0m5.740s 0m8.088s 0m0.860s
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 55.75    0.090346          90      1005         6 open
 37.71    0.061110          62       993           openat
  4.58    0.007425           0     34145           write
  0.97    0.001571           2       993           newfstatat   <- ここ
  0.66    0.001071           1       998           fstat
  0.20    0.000326           0      1989           fchdir
  0.12    0.000194           0      2013           getdents
  0.00    0.000000           0         4           read
  (省略)

まとめ

vboxsfで正しいd_typeを返してやることで、findのようにd_typeを使って処理を削減しているコマンドを速くすることができました。

またこの修正は、VirtualBox 5.0.2の修正に取り込まれました。

kokukuma2 at 14:17│Comments(0)TrackBack(0)

トラックバックURL

この記事にコメントする

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