macOSでdocker環境をどう早くしたか

こんにちは、サーバーサイドエンジニアの Steven です。

この記事は Enigmo Advent Calendar 2021 の22日目の記事です。

今回は Vagrant 環境をリプレースすることとなった Docker 環境をどう早くしたかについて説明します。

スタート地点は Vagrant 環境

エニグモでは以前から VirtualBoxVagrant によるローカル環境を使って、開発してました。 使い勝手は完璧ではなかったのですが、開発する分には問題がとくになく長年活用されました。 ただし、それは構築ができたらの話で、構築時間が長いのと、時間が立てば立つほど自ずと新しい構築エラーが発生して、随時対応しないといけない状態でした。 エンジニアの場合、超えられない問題ではなかったのですが、デザイナーなどテクニカルな知識がそれほどない方だと、サポートしてもハードルがかなり高かったです。 VM 内で使っていた OS も古いバージョンの CentOS で、いずれ更新しないといけなかったです。

Docker 環境ができました

Docker で新しいローカル環境を作ることで以上の問題を解消できないかと動いてくださったエンジニアがいました。 そうすれば、構築時間の短縮と、安定性の改善、使い勝手の向上を実現できるからです。

構築して想定通り改善はできましたが、代わりに新しい問題が現れました。 それもよくあるパターンのようで、Vagrant 環境よりパフォーマンスが悪く、使い物にならない環境になってしまいました。 ローカル環境とはいえデータセンターにあるサーバーとつなげたりするので、もともとの Vagrant 環境でもはじめからそれほど早くはなかったです。 なので、それ以上パフォーマンスが落ちると、対策が必須となってしまいます。

当時はチューニングを試みましたが、根本的な改善が見られず、Docker 環境の導入は一旦保留となりました。

レスポンスタイム比較

ページ Docker環境 Vagrant環境
トップ 9.95s 1.07s
検索結果 9.22s 1.89s
マイページ 9.30s 1.79s

Docker Desktop for Mac について

この場合 Docker 環境のパフォーマンスが悪かったのはコンテナーと macOS 間のファイル IO のパフォーマンスが悪かったからです。 アプリケーションコードをすべてメモリーに保持するなど、ファイル IO がそれほど発生しないアプリケーションの場合は問題にならないこともあると思いますが、私達の場合はファイル IO がどうしても多く発生する環境なので、必然的にパフォーマンスが悪かったです。

Docker Desktop for Mac では、Linux 環境と違って、コンテナーはそのまま macOSカーネルに実行されておらず、macOS 上で動く VM の中にある Linux カーネルによって実行されています。 なぜそうなっているかというと、macOSカーネルではコンテナー化のサポートがなくて docker のようなコンテナーを実装することができないからです。 なので、bind ボリュームを通してコンテナー内から macOS 側にあるファイルにアクセスする時は、VM の中から osxfs(レガシー)か gRPC FUSE というファイルシステムレイヤーを通して、macOS 側のファイルが読み込まれます。 ただし、抽象化が多いところから、ケースによってそのレイヤーがかなり遅くて、ネイティブのアプリケーションと比べ物にならないことが珍しくないです(当然といえば当然ですが)。

Docker Desktop for Mac でキャッシュのオプションもありますが、試した結果それほど影響が大きくなくて、違いに気づけるかどうかというレベルでした。

Docker チームでパフォーマンスの問題を認識していて、改善を以前から試していますが、ファイルシステムの実装はかなり難しいもので、トレードオフが多いです。 パフォーマンスを高くするために工夫すると、整合性などの面で新しい問題が現れたりします。 パフォーマンスは改善傾向にありますが、満足の行かないケースがまだ多いと思います。

Mutagen とは

Docker Desktop が提供するオプションだけでは解決できない問題なので、サードパーティーによる解決策を探しました。 最初は docker-sync を試しましたが、最終的に Mutagen に落ち着きました。

Mutagen はファイル同期とネットワークのフォワーディングのためのツールで、本来はクラウドにあるリソースをローカル環境で使うためのものかと思いますが、最近は docker compose のサポートが追加されて、docker 環境と合わせて使うことが可能になりました。 ファイル同期は rsync によるものなので、パフォーマンスがよくて、かなり堅牢なものです。

docker compose と合わせて使う場合は macOS とコンテナーの間に、VM 内に同期されているファイルのコピーが用意されます。 コンテナ内から本来 bind であったボリュームへのファイルアクセスが発生した場合は macOS 側のファイルを読みに行かず、VM のファイルにのみアクセスするようになります。 アプリケーションのファイル処理が VM 内で完結するため、macOSVM 間のファイルのやり取りが激減して、パフォーマンスのボトルネックがなくなります。

Docker チームでも Docker Desktop に Mutagen を正式的に導入する動きが以前ありましたが、導入で追加の複雑さが生じることから、やめることとなったようです。その代わりに gRPC FUSE を優先するようになりました。

導入例

Mutagen の導入はかなり簡単です。

まずはbrewで Mutagen をインストールします。

$ brew install mutagen-io/mutagen/mutagen-beta # 現在はβバージョンが必要です

続いて、docker-compose.ymlmacOS 側のファイルにアクセスするためのボリュームを用意します。

services:
  bm_on_rails:
    # ...
    volumes:
      - rails-source-sync:/bm_on_rails

  bm_php:
    # ...
    volumes:
      - php-source-sync:/home/web/bm_php

volumes:
  rails-source-sync:
  php-source-sync:

最後に、同じファイルで、x-mutagenの項目の配下に Mutagen の設定を指定します。

x-mutagen:
  sync:
    rails-source-sync:
      mode: 'two-way-resolved'
      alpha: './volumes/bm_on_rails'
      beta: 'volume://rails-source-sync'
    php-source-sync:
      mode: 'two-way-resolved'
      alpha: './volumes/bm_php'
      beta: 'volume://php-source-sync'

alphabetaは同期のエンドポイントとなります。 意味合いはmodeによりますが、以上ではalphamacOS 側のパス、betaは Docker のボリュームを指しています。 modeにはいくつかの選択肢がありますが、コンフリクトを自動解消するとして、alphaの変更をどんな時も優先したい場合はtwo-way-resolvedが適切です。 詳しくはこちらをご確認ください。

セットアップができたら、次は Docker 環境をmutagen compose upで立ち上げます(Mutagen の新しいバージョンではmutagen-compose up)。 docker composeコマンドを使うと、Mutagen の処理がスキップされるので、間違えないよう注意してください。 ただのラッパーなので、docker composeでできることはmutagen composeでもできるはずです。 ちょっと不便かもしれませんが、Mutagen の開発者側でdocker composeをそのまま使えるように検討されているようです。

環境の初回起動に macOS 側のファイルが VM 内にコピーされるので、ファイルの量によって時間がかなりかかってしまう可能性があります(私達の場合は 10分ぐらい)が、二回目以降は Mutagen を使ってないのとあまり変わらなくなります。

導入後、アプリケーションのパフォーマンスは Vagrant 環境よりやや早くなりました。 データセンターへのアクセスがどうしても発生するので、そのパフォーマンスで目標を達成としました。

注意

Mutagen は Docker Desktop のバージョンに依存していますので、Mutagen のバージョンと Docker Desktop のバージョンに気をつけてください。 Docker Desktop のアップデートが来る度にすぐアップデートすると、Mutagen が動かなくなってしまう恐れがあります。

Mutagen の docker compose サポートはまだβですが、バグがほぼなくとても安定しています。

調整

デフォルトで macOS 側のファイルすべてが VM 内に同期されるので、.gitディレクトリなど VCS 用のファイルを同期したくない場合は追加の設定が必要となります。 任意のファイルの同期をスキップすることも可能です。詳しくはこちらをご参照ください。

x-mutagen:
  sync:
    defaults:
      ignore:
        vcs: true
    # ...

特に設定がない状態では Mutagen に同期されているファイルのオーナーとグループ、パーミッションはコンテナー内でデフォルトなものとなってしまいます(オーナーとグループはおそらく root となります)。実行権限のみ同期されます。 なので、コンテナー内のファイルのオーナーとグループ、パーミッションを調整したい場合は追加の設定が必要となります。 詳しくはこちらをご確認ください。

x-mutagen:
  sync:
    # ...
    php-source-sync:
      # ...
      configurationBeta:
        permissions:
          # php コンテナー内ではファイルのオーナーとグループを apache にする
          defaultOwner: 'id:2000'
          defaultGroup: 'id:2000'

同期オプションは他にもいろいろありますので、必要に応じてご確認ください。

同期セッション重複問題

Mutagen を導入してから、社内で特にファイル同期に関する問題が報告されなかったのですが、少しずつ、MacBook の CPU 使用率が高い、見覚えのない差分がgit statusに出てる、などと相談が来るようになりました。

差分の問題はファイル同期と関係がありそうだと思ったので、その方向で調査を進めたら、相談者の MacBookmutagen sync listが本来2つしかないはずのセッションを大量出力しました。

問題出力

--------------------------------------------------------------------------------
Name: rails-source-sync
Identifier: sync_93JIPMqNq5WkYV20nV9Wq4XdvyBr3CXz3oonMfIkyYQ
Labels:
    io.mutagen.compose.daemon.identifier: JH67_K5QB_5FG6_F4UH_OG45_7EKG_LBBY_7IPY_Z4ME_IKQY_HVSG_PPTU
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
Name: php-source-sync
Identifier: sync_bzryoXJaLbxevdit2ODkZuGz2RChyN2C2W5wS8CdbdU
Labels:
    io.mutagen.compose.daemon.identifier: JH67_K5QB_5FG6_F4UH_OG45_7EKG_LBBY_7IPY_Z4ME_IKQY_HVSG_PPTU
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
Name: php-source-sync
Identifier: sync_FaC9uwjuhGziEeggNVbPI2EFgGUE1sxtKArzva4rSck
Labels:
    io.mutagen.compose.daemon.identifier: T3PW_AONQ_MWDI_T5BO_Z6EH_6PQB_6CJZ_336T_M2KO_AXQH_ZSAQ_DQ7E
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
Name: rails-source-sync
Identifier: sync_tPPnFvmEjlwKhLtkrudgukM3Qc7AHdTOc0QANYjwAmN
Labels:
    io.mutagen.compose.daemon.identifier: T3PW_AONQ_MWDI_T5BO_Z6EH_6PQB_6CJZ_336T_M2KO_AXQH_ZSAQ_DQ7E
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
...

出力を見てわかりますが、同期セッションが重複しています。 設定は一緒ですが、daemon.identifierというものだけがそれぞれ違います。

daemon.identifierは Docker デーモンの id です。 デーモンはもちろん一つしかなくて、再起動しない限り id も変わらないはずです。

問題は Docker 開発環境を終了せず、MacBook を再起動すると、発生していました。 原因としては再起動前に Mutagen のセッションを終了しなければ、再起動後に Docker 環境を立ち上げた時、古い同期セッションが残っていながらも、Docker デーモンの id が変わった影響で、同期セッションがまだ作成されてないと Mutagen が判断して、新しい同期セッションを作ってしまうということでした。

該当するイッシューはあります(問題を解消できないか検討中のようです)。 https://github.com/mutagen-io/mutagen/issues/243

対策としては Docker 環境起動後にmutagen sync listの出力を確認して、重複したセッション(現在の Docker デーモン id を使ってないセッション)があった場合、mutagen sync terminateでそのセッションを終了するようにスクリプトを作成しました。

MacBook 停止の際に Mutagen の同期セッションを必ず終了するようにするのも考えられる対策です。

M1 対応

新しい MacBookARM アーキテクチャーの M1 チップを使うことで macOS の業界で動かなくなってしまったものが多くあります。 なので、M1 対応をした時はもしかすると Mutagen が動かなくなってしまうと懸念しましたが、問題なく動きました。 インストールで調整は必要なく、同期も支障なく行われていますので、M1 で使う分には問題ないと思います。

docker-sync について

Mutagen を使うようになる前に 0.5.1 の docker-sync をまず試しました。

docker-sync は ruby の gem と unison を生かした、Docker Desktop 専用のファイル同期ツールです。 仕組みも設定方法も Mutagen に似ていますが、Mutagen と違って macOS 側で動くプログラムが多く、rbenv/ruby などのインストールが必要です。 Vagrant での環境構築ではそれらのインストール時に様々な問題が発生していたため、今回は rbenv/ruby などのインストールは避けたかったです。

docker-sync で初回同期は問題なくて、パフォーマンスも Mutagen と同じぐらい改善されましたが、ファイル同期が不安定で、macOS 側でファイルが変わっても、コンテナー内に反映されないことが多々ありました。 ファイル同期を強制するにも docker-sync のデーモンを再起動するしかなく、そうする度に CPU 使用率が跳ね上がって、MacBook がドライヤーなみにうるさくなったりしていました。 docker-sync のイッシューを確認したら、開発者が問題を認識していても解決策が思いつかない状態のようでした。

なので、以上のことから docker-sync はあまりおすすめできません。

終わりに

Docker Desktop for Mac のファイル同期のパフォーマンスの悪さで悩まされた期間が割と長かったのですが、Mutagen を導入することで完全に解消して、デメリットもほぼないので、同じ悩みを抱えられているなら、ぜひ導入をご検討ください。

Docker チームによる Docker Desktop のパフォーマンス改善に期待したいところですが、Mutagen レベルのパフォーマンスが実現されるまでどれくらい時間がかかるのかがわからない状態なので、そうなるまでサードパーティーに頼るしかないかと思います。

明日の記事の担当はエンジニアの橋本さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co