Docker for Mac のホストを volume マウントするときに cache モードで高速化する

Docker 公式ドキュメントにあるこの話。

Docker for Mac ではホストを volume マウントすると非常に遅くなる。これは MacファイルシステムLinux と異なるので、ホストとコンテナのファイルを同期するのにオーバーヘッドが生じるために起きる。そのため大量のファイルをマウントすると、ファイルの読み書きに時間がかかるようになる。

そこで、Docker 17.04 から、docker run--volume オプションに cacheddelegated が加えられた。

今回は cached オプションでどれくらい高速化されるかを実験してみる。

各オプションの説明

--volume のオプションは 3 つあって、それぞれ以下のようになっている。

  • consistent: 完全な一貫性 (ホストとコンテナのマウントが常に同一のファイルに見える)
  • cached: ホストからの見え方が信頼できる (ホストの更新の前にコンテナ側で遅延が許容される)
  • delegated: コンテナから見え方が信頼できる(コンテナの更新の前にホスト側で遅延が許容される)

consistent がデフォルトで、cached ではファイル同期にいつくかの保証がなくなり、delegated ではさらに保証が少なくなる。保証が少なくなるほど高速になる。cached オプションはファイル読み込みが多いときにパフォーマンスが改善され、delegated は一時ファイルの大量書き込みなどのユースケースで高速化される。cacheddelegated の両オプションとも、適切なユースケースに限定して使うべき。

cached モードが使えるユースケース

User-guided caching in Docker for Mac によると、次のようなユースケースcached モードが活躍する。

  • ホストのエディタで開発し、コンテナで開発ツールを動かす
  • コンテナで定期的なジョブを実行し、ホストのディレクトリに出力を保存する
  • コンテナ側の処理のために巨大なデータアセットをホスト側にキャッシュする

cached モードでどれくらい高速になるか試す

簡単に試してみる。まず volume 用のディレクトリを作る。

$ npm init -y
$ npm add eslint

これで作られた node_modules ディレクトリをマウントすることにする。

ホストとコンテナで同じコマンドを実行し、その時間を計測する。コマンドは Node.js のプロセスを起動して、"eslint" モジュールを読み込むだけ。node_modules 下の大量のファイルを読み込むことになるので、ディスク I/O が遅いと時間がかかる。

ホストから。

$ time node -e "require('eslint')"

real    0m0.586s
user    0m0.404s
sys 0m0.098s

IO 待ち含めて 0.586 秒かかった。

次は Docker コンテナで同じことをする。

volume にオプションを付けない場合:

$ docker run --rm -it -v $PWD/node_modules:/node_modules node:12-slim bash -c "time node -e \"require('eslint')\""

real    0m3.925s
user    0m0.790s
sys 0m0.610s

volume に cached オプションを付けた場合:

$ docker run --rm -it -v $PWD/node_modules:/node_modules:cached node:12-slim bash -c "time node -e \"require('eslint')\""

real    0m1.958s
user    0m0.410s
sys 0m0.230s

cached オプションを付けると、3.925 秒から 1.958 秒になった。

結論

cached オプションを付けると確かにコンテナ側のファイル読み込みが速くなる。が、ホストと同等ではなく、やはりまだ遅い。

開発用にホストのディレクトリをマウントするなら、node_modules などの大きなディレクトリはマウントから除外したほうがいいだろう。