🐳

AppleシリコンのMacでのDocker開発環境のパフォーマンス改善

2023/06/08に公開

プロダクト開発部バックエンド開発グループでエンジニアをしています、おかだです。
ココナラには開発環境改善委員会があり、開発スピードの維持・改善に取り組んでいます。
本日は、取り組みの一環として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.ymlplatform: 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マウントされたディレクトリ下であれば、直ちにリモートのコンテナへ反映されます。

リモートVPSの構成

案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.TParallel()メソッドで実行する設定だったため、ネイティブ動作によりコア数でスケールできるようになったようです。

比較検討結果

それぞれの案の利点と欠点を下記にまとめました。

案1 リモートVPS化 案2 マルチアーキテクチャ化
Pros ローカルマシンのリソースを使わない
本番と同様のlinux/x86_64ネイティブで動作させられる
ネイティブ動作により高速
移行コスト低(rebuildする程度)
Cons マシン費用が別途発生する
環境構築やネットワーク接続に一定知識が必要
ライブラリやツールによってはARMアーキテクチャをサポートしてない

我々の環境では、検証によって案2 マルチアーキテクチャ化の対応が可能というのが判明しましたので、移行・運用コスト的にもライトに済む案2で推進することになりました。

まとめ

今回はAppleシリコンのMacでのDockerのパフォーマンス改善の取り組みについて紹介しました。
我々と同様の課題に直面している場合には、まずマルチアーキテクチャ化が可能かを調査し、難しい場合には、リモートVPS化も検討するといったアプローチがよいと思います。


ココナラでは、一緒に事業のグロースを推進していただける様々な領域のエンジニアを募集しています。
プロダクトの開発から日々の改善活動など幅広く活躍いただけるエンジニアのご応募をお待ちしています!

https://coconala.co.jp/recruit/engineer

今回のお話に興味を持たれた方はぜひカジュアル面談のご応募をお願いします。

https://open.talentio.com/r/1/c/coconala/pages/70417

脚注
  1. 現在、DockerDesktopではベータ機能として、Rosettaを使うオプションも存在しているようです。 ↩︎

Discussion