こんにちは、サーバーサイドエンジニアの Steven です。
この記事は Enigmo Advent Calendar 2021 の22日目の記事です。
今回は Vagrant 環境をリプレースすることとなった Docker 環境をどう早くしたかについて説明します。
スタート地点は Vagrant 環境
エニグモでは以前から VirtualBox と Vagrant によるローカル環境を使って、開発してました。 使い勝手は完璧ではなかったのですが、開発する分には問題がとくになく長年活用されました。 ただし、それは構築ができたらの話で、構築時間が長いのと、時間が立てば立つほど自ずと新しい構築エラーが発生して、随時対応しないといけない状態でした。 エンジニアの場合、超えられない問題ではなかったのですが、デザイナーなどテクニカルな知識がそれほどない方だと、サポートしてもハードルがかなり高かったです。 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 内で完結するため、macOS と VM 間のファイルのやり取りが激減して、パフォーマンスのボトルネックがなくなります。
Docker チームでも Docker Desktop に Mutagen を正式的に導入する動きが以前ありましたが、導入で追加の複雑さが生じることから、やめることとなったようです。その代わりに gRPC FUSE を優先するようになりました。
導入例
Mutagen の導入はかなり簡単です。
まずはbrew
で Mutagen をインストールします。
$ brew install mutagen-io/mutagen/mutagen-beta # 現在はβバージョンが必要です
続いて、docker-compose.yml
で macOS 側のファイルにアクセスするためのボリュームを用意します。
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'
alpha
とbeta
は同期のエンドポイントとなります。
意味合いはmode
によりますが、以上ではalpha
は macOS 側のパス、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
に出てる、などと相談が来るようになりました。
差分の問題はファイル同期と関係がありそうだと思ったので、その方向で調査を進めたら、相談者の MacBook でmutagen 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 対応
新しい MacBook で ARM アーキテクチャーの 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 レベルのパフォーマンスが実現されるまでどれくらい時間がかかるのかがわからない状態なので、そうなるまでサードパーティーに頼るしかないかと思います。
明日の記事の担当はエンジニアの橋本さんです。お楽しみに。
株式会社エニグモ すべての求人一覧