docker
docker-compose
docker-for-mac

docker-syncでホスト-コンテナ間を爆速で同期する

2017/3/15 追記

先日この問題のissueに対して、

https://github.com/docker/for-mac/issues/77#issuecomment-283996750

というコメントがつけられ、それに関する

https://github.com/docker/docker/pull/31047

というプルリクが 本体にマージされたようです。

まだ詳しく見ていませんが、マウント時に同期方法オプションが指定できるようになり、そのオプションによってキャッシュするレベルを制御して同期を軽くしよう、というような感じになるようでした。

実際にリリース版で使えるのはいつなのかわかりませんが、やはり本体が早くなるのが一番いいのでちょっと期待ですね。

========== 追記ここまで ==========

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には

などが挙げられているようです。で、この中でworkaroundとして docker-syncというのが紹介されていてためしてみたところほぼラグもなく、docker-toolbox + nfsマウントよりもはるかに速くなったので手順を残しておきます。

docker-sync

docker-syncの仕組みとしては、 docker-v で直接ホスト側のディレクトリをマウントするのではなく、rsyncunisonの仕組みでホスト側のファイルをコンテナ側に転送しよう、というものです。

(詳細はわかりませんが、実際には、シンク用の名前付きボリュームを作成し、そこにホスト側のファイルを同期した上でそのボリュームをコンテナにマウントすることで実現しているようです)

docker-syncのインストール

これは公式wikiにあるとおりで、

本体のインストール

gem install docker-sync
brew install fswatch

同期手段に合わせて必要なミドルウェアをインストール。今回の場合は双方向(ホスト<=>コンテナ)の同期をしたかったので unison をインストールします。

brew install unison

これでdocker-syncを使う準備は完了です。

試してみる

docker-sync無しでまず実験

適当にコンテナを立てて、100Mのファイルをホスト側と共有してみます。 公式にもある通り、docker-composeを使うほうが簡単なのでそちらで試します。

docker-compose.yml
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の設定

docker-sync.yml
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)、 それをコンテナ内のどのパスにするか?(dest) (0.3.0からdestは不要になった),それらをどの同期方式で行うか?(sync_strategy)を指定しています。

またここで書いているsync-volumeは任意の名称でよいのですが、これが実際にdockerの名前付きボリュームとして作られるボリューム名称になります。

% docker volume ls | grep sync-volume
local               sync-volume

次に、docker-compose.ymlの上書き設定

docker-compose-dev.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/testsync-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を使う回避策も良さそうです。

http://qiita.com/pocari/items/456052a291381895f8b3#%E3%83%AA%E3%83%AD%E3%83%BC%E3%83%89%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E5%95%8F%E9%A1%8C

ちなみに、ファイル数が多くなったらどうなるんだ?という疑問があるかもしれませんが、 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
に置いています。