※DXはデジタルトランスフォーメーションではなくてDeveloper Experienceの方です
2020/05/26追記
https://qiita.com/nanasess/items/16ab9274c34bdc34e567 を使うことでVagrantを経由せずにDocker for Macの機能だけでMutagenを使うことができるようになりそうです。
パフォーマンスの測定はしていませんが、Docker for Macの設定のみでこの記事の手法と同等の速度が出るようになるかもしれません。
概要
Macでネイティブでの動作とほぼ同等の速度の安定したDocker環境を手に入れることができたので、その知見について公開します。
ものによりますが、最大10倍程度パフォーマンスの向上が見られました。
具体的な手法としてはVirtualBox + Dockerを用います。
設定は2ファイル50行弱のコードでほぼ完結する程度なんで導入も特に難しくないはずです。
Docker for Macは遅い
MacでDockerを使って開発している方は体感していると思うのですが、Docker for Macの速度はネイティブで動かすのと比べて相当遅く、リソースも消費します。
自分はRails + React(webpack) というWebでは比較的一般的なスタックで開発しているのですが、Railsの初期読み込み・webpackの(差分)トランスパイル・yarn installなどが致命的に重く、LinuxでのDocker環境と比較してかなりDXが低下します。
特に大量のファイルの読み込みや、ファイル変更のリアルタイム検知が弱い印象が強いです。
また、Docker for Macは不安定な面もあり、(例えばgitのブランチ変更などで)大量のファイル変更が起こった場合にCPU使用率が100%に張り付いてしまい、再起動しないと戻ってこないなどの現象も起きることがあります。
com.docker.hyperkit 100% cpu usage is back again · Issue #3499 · docker/for-mac
この問題の大部分は調べた限りファイルシステムをマウントした際のオーバーヘッドに起因しており、docker-syncなどを使ってファイルシステムの扱い方を変えることである程度改善することが知られています。
しかし、docker-syncも不安定さが残るなど、懸念点があります。
参考: hanhan's blog - Docker for Macのmount遅い問題まとめ
また、自分は設定の煩雑さから使ったことがないのですが、Docker for MacはNFS Volume sharingに対応しているので、それを使うことでもある程度改善されるのではないかと思います。
この記事ではパフォーマンス低下の最も大きな原因となっているファイルシステムについていくつか改善案を試し、その中で最も効果があったVirtualBox(Ubuntu)を使い、ファイルシステムのマウントを行わずに(VMのネイティブ?ファイルシステム上で)Dockerを使う、という手法について解説しようと思います。
Docker for Macと今回構築する環境(VirtualBox + Docker)の違い
基本的にはどちらもVM上でDockerが動作することになります。
Docker for Mac
Docker for Macでは、インストール時にAlpine LinuxベースのHyperkit VMがインストールされ、起動時に /var/run/docker.sock
が生成されます。
このVMはDocker for Mac上で自動的に管理され、Mac上で docker
, docker-compose
コマンドを実行する際に特に設定をしていない限り docker.sock
を経由して透過的にVM内でDockerコマンドが実行されるようになっています。
VirtualBox + Docker
一方、今回構築する環境ではVirtualBoxを使って実際にVMを管理することになります。
Vagrantを用いてVirtualBoxでUbuntu環境を構築し、その中でDockerを動作させます。
完全に独立した環境のため、ファイルシステムについては VMに設定しているものがDocker上にマウントされ 、Dockerコマンドについてはsshで接続した上でVM内から実行することになります。
VM内の docker.sock
をMacにマウントすることでMac上から透過的にDockerコマンドを扱えるかもしれませんが、まだ検証していません。
ファイルシステムについてですが、Vagrantでファイルを同期する sync_folder
機能で指定できる同期タイプとして
- VirtualBox(オプション指定なし)
- NFS
- rsync
- SMB
があります。
しかし、今回はどれも使わずに(VM起動時に1度だけrsyncをする)、Vagrantから独立した手段でファイル同期を行います。
実際の速度差
Docker for Mac, VirtualBox + Docker, Macネイティブについて、計測が容易で差が大きかったものについて環境毎の実測値を記載します。
Railsへのアクセスについて、ネイティブよりVMの方が早い理由についてはよく分かっていません。
Command | Docker for Mac | VirtualBox + Docker | Macネイティブ | 差 |
---|---|---|---|---|
yarn install | 117.84s | 16.86s | 16.88s | 6.8倍高速化 |
Rails起動後の初回アクセス (curl) |
22.43s | 2.199s | 3.32s | 10倍高速化 |
VMの設定や使い方
VMの構築・設定
Vagrantを使ってVMを構築します。
実際に使っているVagrantfileの構成は以下です。
CPU・メモリ・ディスクサイズなどは適宜調整してください。
追加でプラグインとして
- vagrant-disksize
- vagrant-hostsupdater
- vagrant-mutagen
が必要になるので、 $ vagrant plugin install vagrant-disksize vagrant-hostsupdater vagrant-mutagen
を実行してプラグインをインストールしておいてください。
Vagrant.configure('2') do |config|
config.vm.box = 'ubuntu/xenial64'
config.vm.hostname = 'my-app'
config.vm.network :private_network, ip: '192.168.50.10'
config.vm.provider :virtualbox do |vb|
vb.gui = false
vb.cpus = 4
vb.memory = 8192
vb.customize ['modifyvm', :id, '--natdnsproxy1', 'off']
vb.customize ['modifyvm', :id, '--natdnshostresolver1', 'off']
end
config.disksize.size = '30GB'
config.mutagen.orchestrate = true
config.vm.synced_folder './', '/home/vagrant/app', type: "rsync",
rsync_auto: true,
rsync__exclude: ['.git/', 'node_modules/', 'log/', 'tmp/']
config.vm.provision 'shell', inline: <<-SHELL
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
usermod -aG docker vagrant
curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
SHELL
end
VMの使い方
(実際のコマンドの実行はファイル同期の章に書いている内容を行った後にしてください。)
$ vagrant up
を実行すると各設定が適用されたVMの作成・起動とrsyncの実行が行われ、 ./
が /home/vagrant/app
に同期されます。rsyncの実行は起動時のみで自動的な実行は行われません。
初回はprovisionでDockerとDocker Composeのインストールが行われます。
その後、 $ vagrant ssh
をしてVMにSSHでログインし、 $ cd app && docker-compose up {rails}
のようなコマンドを実行することでDockerコンテナが立ち上がります。
イメージのビルドなどの処理は必要に応じて適宜行ってください。
立ち上がったアプリケーションにはVagrantfileで指定しているIPでアクセスできます。 (http://192.168.50.10:{3000}
など)
ファイル同期の手段
開発を行う際にはローカルとVM上でファイルを同期する必要がありますが、その際にVirtualBoxの標準の共有フォルダやNFSを使うと、VM内からDockerへマウントした際のパフォーマンスが結局大幅に低下します。
そのため、MacからVMへのファイルシステム自体のマウントは行わず、ファイル変更を検知して相互にファイルを転送するような同期手段が望ましいです。
Vagrantのrsyncでの同期がそれに近いのですが、その場合ホストからVMへ単方向の同期しか行われないという問題があります。
具体的にはVM内で $ bundle install
や $ yarn install
を実行した際に更新される Gemfile.lock
や yarn.lock
のようなファイルについてVMからMacへ同期が行われません。
VM内のファイル変更を検知してホストにrsyncを実行するようなコマンド実行も試したのですが、双方向にrsyncで適切な同期を行うというのは実現できませんでした。
ファイル同期のソリューション 「Mutagen」
上記の問題についてですが、最終的にMutagenを使うことで解決しました。
今回Vagrantを採用している理由の一つとして、Vagrant PluginでVMに対してMutagenの設定が簡単に行える、というものもあります。
VMの構築・設定の節で既に記載しているのですが、Mutagenをインストールし、 vagrant-mutagen
プラグインを導入することで双方向のファイル同期が適切に行われます。
Mutagenのインストールですが、Macでは $ brew install mutagen-io/mutagen/mutagen
を実行することで行う事ができます。
その後、Vagrantfileと同じ階層に以下の mutagen.yml
を配置してください。
sync:
app:
mode: "two-way-resolved"
alpha: "./"
beta: "{my-app (Vagrantfileで指定したホスト名)}:/home/vagrant/app"
ignore:
vcs: true
paths:
- "/node_modules"
- "/log"
- "/tmp"
その後 $ vagrant up
でVMを起動すると、Mutagenによって双方向のファイル同期が行われます。
この同期は双方向にほぼリアルタイムで行われ、ファイルシステムのマウントではなくファイルの転送で実現されているため、最終的にDockerコンテナへマウントされた際のオーバーヘッドはほぼ発生しません。
結果
結果として現在業務で行っている開発にVagrantとMutagenを導入したことで、Macでも快適にDockerを使った開発が行えるようになりました。
Docker for Macでは速度・リソース・安定性のどの面でも苦労していたのですが、現在はある程度安定したDocker環境を構築できています。
sshしなければ使えないなど多少不便な点も残ってはいるのですが、それを上回るメリットが享受できていると感じています。
Docker for Macでの開発に消耗している方は数多くいる印象なので、もし機会があればこの構成を試してみていただけると幸いです。
備考・注意点
実行環境のスペック
MacBook Pro (16-inch, 2019)
CPU: 2.4GHz 8コア Intel Core i9
メモリ: 64GB 2667MHz DDR4
Docker for MacへのMutagenの適用
MutagenはDocker for Macで扱うこともできるようです。
その際のパフォーマンスは計測できていないのですが、もしかしたらVagrantを経由した場合と同等の速度が出るかもしれません。
今回はMutagen導入時には既にVagrant + rsyncを使った場合の速度が非常に速いことが確認できていたので、設定がシンプルだったvagrant-mutagenを採用したのですが、Docker for Mac + Mutagenでの開発も調査する価値があるかと思います。
マイクロサービス開発への対応
場合によっては複数のリポジトリに横断するマイクロサービス開発をDockerのnetwork機能を用いて行っている場合があるかと思います。
この構成ではリポジトリ毎に完全に独立したVMが作成されるという特性上、Docker networkを使った開発が難しくなってしまう可能性があります。
現在マイクロサービスでの開発は行っていないため確認できていないのですが、解決策として
- 上記Docker for MacへのMutagenの適用
- 必要なマイクロサービスをsubtree(submodule)として配置したリポジトリを作り、そのリポジトリ上でVMを構築する
というものが使えないかと考えています。
これについては今後マイクロサービス開発が必要になったタイミングで検証を進めていこうかと考えています。
Mutagenの同期セッションについて
vagrant-mutagenでのファイル同期についてですが、 $ vagrant halt
を行わずにVMが終了した場合にはセッションが削除されずに残ってしまうという問題が確認できています。
セッションが残っていた場合には次回のVM起動時にセッションが新たに作成され、多重にセッションが貼られたまま残り続けるという状態になってしまいます。
また、残っているセッションは mutagen.yml
の設定を読み込まずに古い設定のまま動作し続けるためバグの原因となります(なりました)。
MacのクラッシュやVMの正常な終了が行えなかった際には $ mutagen sync list
でセッションを確認し、不要なセッションについては $ mutagen sync terminate {session_id}
で削除するようにしてください。
docker-composeの設定について
docker-compose.yml
に以下のようにgem用、npm用のvolumeを作成しているのですが、設定に不備があってDocker for Macでパフォーマンスが落ちているなどあればコメントをいただけると助かります。
services:
rails:
volumes:
- .:/usr/src/app:cached
- bundle:/usr/local/bundle:cached
frontend:
volumes:
- .:/usr/src/app:cached
- yarn:/usr/local/yarn:cached
volumes:
bundle:
yarn: