Help us understand the problem. What is going on with this article?

DXを大幅に低下させるDocker for Macを捨ててMac最速のDocker環境を手に入れる

※DXはデジタルトランスフォーメーションではなくてDeveloper Experienceの方です

概要(2020/02/22追加)

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)の違い

docker.png

基本的にはどちらも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.lockyarn.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:
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした