2020/05/26 追記
Docker for Mac の Mutagen-based caching で Volume のパフォーマンスが劇的に改善した
Mutagen単独で試してみたことがあって、すごく速くてよかったんですが、 Docker for macに統合されそうな感じになってるんですね。
これは期待。
2017/3/15 追記
先日この問題のissueに対して、
というコメントがつけられ、それに関する
というプルリクが 本体にマージされたようです。
まだ詳しく見ていませんが、マウント時に同期方法オプションが指定できるようになり、そのオプションによってキャッシュするレベルを制御して同期を軽くしよう、というような感じになるようでした。
実際にリリース版で使えるのはいつなのかわかりませんが、やはり本体が早くなるのが一番いいのでちょっと期待ですね。
========== 追記ここまで ==========
docker-syncでホスト-コンテナ間を爆速で同期する
問題
少し前にprivate betaが終わって、docker for mac
を使っている方も結構いらっしゃいますね。
で、僕もdocker for mac
は使っているのですが、ホスト側のディレクトリをマウントして使うと猛烈に遅い。
docker-toolbox(vitual box)
を使っていた頃も同じ問題はあったのですが、docker-machine-nfsというのを使えばnfsでマウント出来るので多少速度の問題は改善されていました。ただこの方式だと
- マウントしているディレクトリのファイルの変更検知ができない
- docker toolboxに縛られる(きっと今後はdocker for macが主流になると思われるため)
- 多少速くなるがそれでも数万ファイルオーダーがあると使えないぐらい遅い場合がある
などがあり、できれば docker-for-mac
を使いつつ早くする方法はないかと探していました。
実際docker-for-macの公式のフォーラムや、githubのissueには
- https://github.com/docker/for-mac/issues/77
- https://forums.docker.com/t/file-access-in-mounted-volumes-extremely-slow-cpu-bound/8076
などが挙げられているようです。で、この中でworkaroundとして docker-syncというのが紹介されていてためしてみたところほぼラグもなく、docker-toolbox + nfsマウントよりもはるかに速くなったので手順を残しておきます。
docker-sync
docker-syncの仕組みとしては、 docker
の -v
で直接ホスト側のディレクトリをマウントするのではなく、rsync
やunison
の仕組みでホスト側のファイルをコンテナ側に転送しよう、というものです。
(詳細はわかりませんが、実際には、シンク用の名前付きボリュームを作成し、そこにホスト側のファイルを同期した上でそのボリュームをコンテナにマウントすることで実現しているようです)
docker-syncのインストール
これは公式wikiにあるとおりで、
本体のインストール
gem install docker-sync
brew install fswatch
同期手段に合わせて必要なミドルウェアをインストール。今回の場合は双方向(ホスト<=>コンテナ)の同期をしたかったので unison
をインストールします。
brew install unison
これでdocker-sync
を使う準備は完了です。
試してみる
docker-sync無しでまず実験
適当にコンテナを立てて、100Mのファイルをホスト側と共有してみます。 公式にもある通り、docker-composeを使うほうが簡単なのでそちらで試します。
version: '2'
services:
app:
image: busybox
volumes:
- .:/var/test
command:
tail -f /dev/null
カレントディレクトリをコンテナの/var/test
にマウントしてtail
で待つだけ。
この状態でdocker-sync
使わない(とても遅い)環境はできたので、試してみましょう。
# up -dでもいいです。
docker-compose up
up
した後別のターミナル(up -d
の場合はそのまま) 下記コマンドでコンテナにアタッチ。コンテナ名はカレントディレクトリ名によって変わります。
今回の場合はdstest_app_1
というコンテナ名です。
docker exec -it dstest_app_1 sh
その後コンテナの中に入り、マウントしたディレクトリに100MBのテストデータを作成します。
cd /var/test
/var/test # time dd if=/dev/zero of=speedtest bs=1024 count=102400
102400+0 records in
102400+0 records out
104857600 bytes (100.0MB) copied, 36.817946 seconds, 2.7MB/s
real 0m 36.81s
user 0m 0.16s
sys 0m 2.58s
/var/test # ls -lh speedtest
-rw-r--r-- 1 root root 100.0M Oct 23 05:28 speedtest
たった100Mのファイルを作成するのに36秒かかっています。ここ(/var/test
)がマウントされているディレクトリのせいでこれだけ遅くなっているわけですね。
もちろんマウントしているのでホスト側にもこのファイルはできています。
ちなみに、マウントされていないコンテナ内のディレクトリで100Mのファイルを作成すると、
/var/test # cd /tmp
/tmp # time dd if=/dev/zero of=speedtest bs=1024 count=102400
102400+0 records in
102400+0 records out
104857600 bytes (100.0MB) copied, 0.215219 seconds, 464.6MB/s
real 0m 0.21s
user 0m 0.01s
sys 0m 0.20s
/tmp
で試すと約**0.2秒!**です。180倍ぐらい遅いみたいですね。
docker-syncを使って実験
では、docker-syncをつかってみます。
docker-sync自体の設定は docker-sync.yml
に書いていきます。で、docker-syncはdocker-composeでの設定上書きの機能をうまく使っていて既存の docker-compose.yml
はそのままに、それに対して設定を上書きするyml(デフォルトでは docker-compose-dev.yml
)を作ることで実現しています。(もちろん、そのままdocker-compose.yml
自体に設定してしまっても動きます。)
まずdocker-syncの設定
version: '2'
syncs:
sync-volume:
src: '.'
2017/03/15 追記 docker-syncのバージョン 0.2.0
以降から docker-sync.yml
の書式に色々変更があったようです。 詳しくは @smith_30 からのコメントと、そのリンク先参照してください。
2018/08/20 追記 @drmatio から指摘頂きまして、docker-syncのバージョン 0.3.0
以降から dest
は不要になったそうです。
たしかに、docker-compose側のvolumesでマウント先指定していたので二度手間だったんですね。
公式のwikiには除外ファイルの設定など色々オプションが載ってますので、参考にしてみてください。ここでは最小限の設定で、
ホスト側のディレクトリ(src
)、 それをコンテナ内のどのパスにするか?( (0.3.0からdestは不要になった),それらをどの同期方式で行うか?(dest
)sync_strategy
)を指定しています。
またここで書いているsync-volume
は任意の名称でよいのですが、これが実際にdockerの名前付きボリュームとして作られるボリューム名称になります。
% docker volume ls | grep sync-volume
local sync-volume
次に、docker-compose.ymlの上書き設定
version: '2'
volumes:
sync-volume:
external: true
services:
app:
volumes:
- sync-volume:/var/test
単純ですね。 docker-sync
側で自動で作ってくれるボリュームをexternal
として宣言しておき、それをカレントディレクトリでなくsync-volume:/var/test
としてマウントするように設定します。
これで設定は完了なので実行してみます。
実行するにはdocker-sync-stack start
で行います。が、実体としては、
docker-sync start # docker-syncで同期開始
docker-compose -f docker-compose.yml -f docker-compose-dev.yml up
相当のようですので、こちらでもいいと思います。
docker-compose
は -f ymlファイル
を出てきた順番に後勝ちで設定を上書きしていくので、
- docker-compose.yml
- docker-compose-dev.yml
の順番で指定することで、volumeの設定 .:/var/test
を sync-volume:/var/test
に上書きしてくれているわけです。
とりあえず、ここでは、
docker-sync-stack start
で開始します。その後同様に、コンテナ側でホスト側に同期するディレクトリに100Mのファイルを作成します。
/ # cd /var/test
/var/test # time dd if=/dev/zero of=speedtest bs=1024 count=102400
102400+0 records in
102400+0 records out
104857600 bytes (100.0MB) copied, 0.189765 seconds, 527.0MB/s
real 0m 0.19s
user 0m 0.00s
sys 0m 0.19s
/var/test #
うん。マウントしていないディレクトリで試したときと同じぐらいの速度でファイルができました。
コンソールを眺めていると分かるのですが、上記コマンド打った瞬間に同期が走り、即座にホスト側にも反映されていました。
また、上記の実験はコンテナ側->ホスト側
の同期でしたが、逆向きの同期もほぼラグなしに出来るようです。
まとめ
これだけ速くなるので、docker for mac
のマウントの遅さに辟易している方は、docker for mac
自体がパフォーマンス改善してくれるまで、docker-sync
を使ってみるのはおおいにアリだと思います。
別記事でdockerでrails5環境構築というのを書いていて、そこでもマウントの問題で色々困ったことがありましたが、docker-syncを使う回避策も良さそうです。
ちなみに、ファイル数が多くなったらどうなるんだ?という疑問があるかもしれませんが、 docker-syncによると、
efficient in its way to watch for file changes - even for 12k+ files it will not eat up your CPU
とあるので、万単位でも問題無いようですね。
実際僕はフロント側約6千ファイル、バックエンド側約2万ファイルのプロジェクトをフロント、バックの二箇所をそれぞれdocker-sync
で同期してみましたが、ほとんどローカルで開発しているのと大差無いぐらいの速度がでているので大量のファイルがあっても非常に効果的だと思います。
上記で使ったサンプルは、
https://github.com/pocari/docker-sync-sample
に置いています。