AppleシリコンのMacでのDocker開発環境のパフォーマンス改善
プロダクト開発部バックエンド開発グループでエンジニアをしています、おかだです。
ココナラには開発環境改善委員会があり、開発スピードの維持・改善に取り組んでいます。
本日は、取り組みの一環としてAppleシリコンのMacでのDocker開発環境のパフォーマンスを5-10倍程度改善したお話をしたいと思います。
はじめに
AppleシリコンのMacが発表されてから数年が経ちました。ココナラでも昨年から導入が進んでおります。新しく入社したメンバへはM1 MacBook Pro(最近だとM2)が標準で支給されるようになっており、現在はIntel MacとM1 Macが混在して利用されている状況です。
バックエンドの開発では、ローカルでの動作確認や単体テストはコンテナ内で実行できるようになっています。
Dockerパフォーマンス問題
M1 Macを導入したメンバから
単体テストが遅い
静的解析が遅い
ローカルが遅すぎるので都度GitHubへpushしてCIを実行している
と言った声が聞かれるようになりました。
特にジョイン直後の開発体験が損なわれているのは、由々しき事態だと考え、対応に動き出しました。
Dockerが遅い原因
bindマウントしたボリュームへのアクセスが遅い
この問題はM1に限らず、Intel Macでも発生していました。
rakeタスクの一覧を出力するだけでも倍以上の差が出ます。
# osxfs
root@6f440765e89c:/app # time bundle exec rake -T >/dev/null
real 0m7.235s
user 0m2.006s
sys 0m0.594s
# gRPC FUSE
root@9aa4f8394b75:/app # time bundle exec rake -T >/dev/null
real 0m3.204s
user 0m1.585s
sys 0m0.413s
# VirtioFS
root@88ac06e32c81:/apps # time bundle exec rake -T >/dev/null
real 0m2.738s
user 0m1.571s
sys 0m0.478s
こちらの問題については、最近のDockerDesktopでは、VirtioFSが正式採用となっているため。
各メンバのDockerDesktopを最新へバージョンアップし、設定を見直すだけで解消します。
x86_64イメージを利用している
バックエンドの利用する、多くのリポジトリでdocker-compose.yml
へplatform: linux/x86_64
を追加しているのがみられました。
これまでIntel Macで利用してきた環境をM1で動作させるために追加されたもので、ARMアーキテクチャのM1 Macではx86_64のイメージを動作させると、QEMUによるエミュレーション[1]を介して実行されるため、パフォーマンスが大きく下がります。
今回の問題はこちらの影響によるところが大きいようです。
改善案
案1 リモートVPS化
ローカルのDocker Desktopではなく、クラウド上のVPS(Virtual Private Server)、例えばAWSのEC2上にDockerデーモンを起動し、リモート操作することでパフォーマンスを向上する案です。
Dockerそのものは本番環境同様のLinux/x86_64ネイティブで動作させる事ができます。
Dockerコンテキスト
docker context
コマンドを使うことで、SSH接続さえできれば簡単にクラウド上のVPSのコンテナを操作できます。
下記の例ではローカルからSSH経由でリモートのコンテナを操作する例です。
(ssh remote-vps
でSSH接続できる状態である必要があります。)
# `remote`という名前のcontextの作成
> docker context create remote ssh://remote-vps
# contextを切り替える
> docker use remote
# 以降のdocker操作はリモートのDocker Engineに対して行われる
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
コンテキストが切り替わったあとは、これまで通りdocker compose up
などを実行するとリモートのDockerコンテナが立ち上がるようになります。
ローカルディレクトリをbindマウントする
リモートのDockerデーモンを操作はできますが、Dockerがローカルで動作していないので
bindマウントするためには、マウント元のリポジトリをローカルからリモートへ転送して起動する必要があります。
これを簡単に行うために、Mutagenを使って、ローカルのリポジトリをリモートへ同期するのも1つの手です。
下記では~/repo
をローカルのMacとリモートのLinuxで同期する例です。
# リモートマシンへローカルのホームディレクトリと同じ階層構造を作成する
> ssh remote-vps sudo mkdir -p "$HOME"
# 権限付与
> ssh remote-vps sudo chown '$USER:$USER' "$HOME"
# mutagen同期を作成する
> mutagen sync create "$HOME"/repo remote-vps:"$HOME"/repo
ローカルのレポジトリのディレクトリとリモートのディレクトリの階層を一致させるところが少しトリッキーですが、
これによってDocker Context越しでもローカルから透過的にbindマウントを使うことができます。
ローカルのエディタでソースコードを編集すれば、bindマウントされたディレクトリ下であれば、直ちにリモートのコンテナへ反映されます。
案2 マルチアーキテクチャ化
Dockerイメージをマルチアーキテクチャに対応させることで、パフォーマンス改善を図る案です。
マルチアーキテクチャのイメージ
しばらくのあいだはIntel/M1が混在する環境で開発するため、それに適したイメージを選定する必要があります。
Dockerイメージにはマルチアーキテクチャという仕様があり、これに対応しているイメージは、複数のCPUアーキテクチャをサポートできます。
各種のイメージがマルチアーキテクチャに対応しているか、DockerHubのサイトから検索してもよいですが、mqueryというCLIツールで調べることができます。
下記の例ではMySQLの公式イメージのマルチアーキテクチャ対応状況を調べています。
# 5系はAMD64/x84_64イメージのみ
> docker run --rm mplatform/mquery mysql:5.7
Image: mysql:5.7 (digest: sha256:f57eef421000aaf8332a91ab0b6c96b3c83ed2a981c29e6528b21ce10197cd16)
* Manifest List: Yes (Image type: application/vnd.docker.distribution.manifest.list.v2+json)
* Supported platforms:
- linux/amd64
# 8系はARMイメージも提供されている
> docker run --rm mplatform/mquery mysql:8
Image: mysql:8 (digest: sha256:d6164ff4855b9b3f2c7748c6ec564ccff841f79a7023db0f9293143481a44b6e)
* Manifest List: Yes (Image type: application/vnd.docker.distribution.manifest.list.v2+json)
* Supported platforms:
- linux/amd64
- linux/arm64/v8
調査と動作確認
バックエンドの扱う、各リポジトリについて調査を行いイメージを適切なものを利用するようにDockerfileやdocker-compose.ymlを修正しました。
注意点としてライブラリやツールによってはARMアーキテクチャでは動作しない場合があります。
われわれの環境でもいくつかのライブラリARM版イメージでは動作やbuildが失敗しました。
幸いなことに本番で利用しているライブラリではなく、デバッグ用途だったため、代替へ置き換えるなど整理していくことで、マルチアーキテクチャイメージで動作する状態にまでもっていくことができました。
イメージ最適化の効果
イメージ最適化によってどの程度パフォーマンスが改善されたのか、バックエンドで主に利用する2つのリポジトリの単体テストの実行時間を比較してみます。
Ruby on Railsプロジェクトの単体テスト(RSpec)
x86_64イメージ on M1
root@96c57bba51b3:app# uname -m
x86_64
root@96c57bba51b3:app# time bundle exec rspec spec/apis/api/ >/dev/null 2>&1
real 95m44.780s
user 87m27.059s
sys 1m17.068s
ARM64イメージ on M1
root@2d095f0d9933:/app# uname -m
aarch64
root@2d095f0d9933:/app# time bundle exec rspec spec/apis/api/ >/dev/null 2>&1
real 19m21.943s
user 12m16.354s
sys 0m46.032s
5倍近く改善しました!
Golangプロジェクトの単体テスト(testing)
x86_64イメージ on M1
root@cf34547b9588:# uname -m
x86_64
root@cf34547b9588:# time make 2>&1 > /dev/null
real 1m49.541s
user 11m47.095s
sys 1m36.954s
ARM64イメージ on M1
root@7cc0f4d70176:# uname -m
aarch64
root@7cc0f4d70176:# time make test 2>&1 > /dev/null
real 0m15.340s
user 0m51.645s
sys 0m27.664s
10倍以上改善しました!
テスト実行を*testing.T
のParallel()
メソッドで実行する設定だったため、ネイティブ動作によりコア数でスケールできるようになったようです。
比較検討結果
それぞれの案の利点と欠点を下記にまとめました。
案1 リモートVPS化 | 案2 マルチアーキテクチャ化 | |
---|---|---|
Pros | ローカルマシンのリソースを使わない 本番と同様のlinux/x86_64ネイティブで動作させられる |
ネイティブ動作により高速 移行コスト低(rebuildする程度) |
Cons | マシン費用が別途発生する 環境構築やネットワーク接続に一定知識が必要 |
ライブラリやツールによってはARMアーキテクチャをサポートしてない |
我々の環境では、検証によって案2 マルチアーキテクチャ化
の対応が可能というのが判明しましたので、移行・運用コスト的にもライトに済む案2で推進することになりました。
まとめ
今回はAppleシリコンのMacでのDockerのパフォーマンス改善の取り組みについて紹介しました。
我々と同様の課題に直面している場合には、まずマルチアーキテクチャ化
が可能かを調査し、難しい場合には、リモートVPS化
も検討するといったアプローチがよいと思います。
ココナラでは、一緒に事業のグロースを推進していただける様々な領域のエンジニアを募集しています。
プロダクトの開発から日々の改善活動など幅広く活躍いただけるエンジニアのご応募をお待ちしています!
今回のお話に興味を持たれた方はぜひカジュアル面談のご応募をお願いします。
-
現在、DockerDesktopではベータ機能として、Rosettaを使うオプションも存在しているようです。 ↩︎
Discussion