ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Apple Silicon上でx86_64のDockerを使う

Apple Silicon上でLinux向けx86_64の環境が欲しくなったり、x86_64向けのバイナリやイメージを作らないといけなくなる事態は、稀に良くあると思う。

colimaを使うと、Aarch64なMac上であっても、かなり楽にx86_64のLinux環境とDocker環境が手に入る。

最近のcolimaには実は --arch というオプションがあり、x86_64又はaarch64であればあっさりと環境を作ることができるようになっている。

デフォルトで入ってくる環境と区別するため --profile オプションを明示する必要がある。

$ colima --profile colima-x64 start --arch x86_64

colimaの作ったDockerインスタンスcolima list で一覧できる。

$ colima list
PROFILE       STATUS     ARCH       CPUS    MEMORY    DISK
default       Running    aarch64    2       2GiB      60GiB
colima-x64    Running    x86_64     2       2GiB      60GiB

--profile を明示して colima start を実行すれば、ホストの docker コマンドでアクセスするインスタンスを切り替えられる*1。defaultに戻すときはオプションなしで colima start を打つ。

$ colima --profile colima-x64 start
$ docker info
Client: 
 Context:    colima-colima-x64
 Debug Mode: false

Server:
...
 OSType: linux
 Architecture: x86_64
...

実際どれくらい使えるの?

とは言え、これはAarch64の上でQEMUを経由してx86_64のCPUをエミュレートしているわけで、パフォーマンスがどれくらい出るかは気になる。

以下の条件で、Goのプロジェクトのコンパイル時間を比較してみた。

環境

ホスト: macOS Monterey 12.2.1(21D62)、Apple M1 Max(10コア)、64GB memory ゲスト: x86_64/aarch64共に、 CPU 2 、メモリ 2GiB (colimaが作成するインスタンス設定のデフォルト)

ビルドの方法

ビルドする対象としてはある程度の規模がある、ほぼ純粋なGoのプロジェクトが良いため、 gopher-lua を選んだ。

github.com

まず、apt等によるネットワーク状況の影響を最小限にするため、以下のDockerfileからbaseイメージを作成し、

FROM golang:1.18-bullseye

RUN apt -y update && apt -y install git make python
RUN mkdir /build && git clone https://github.com/yuin/gopher-lua /build/gopher-lua

これを元に以下のDockerfileのビルドが完了する時間を測定した*2

FROM go-build-bench-base

WORKDIR /build/gopher-lua
RUN make glua

CMD ["file", "/build/gopher-lua/glua"]

結果

x86_64 on Apple Silicon

$ docker build --no-cache -t go-build-bench .
[+] Building 14.9s (7/7) FINISHED                                                                              
 => [internal] load build definition from Dockerfile                                                      0.0s
 => => transferring dockerfile: 37B                                                                       0.0s
 => [internal] load .dockerignore                                                                         0.0s
 => => transferring context: 2B                                                                           0.0s
 => [internal] load metadata for docker.io/library/go-build-bench-base:latest                             0.0s
 => [1/3] FROM docker.io/library/go-build-bench-base                                                      0.0s
 => CACHED [2/3] WORKDIR /build/gopher-lua                                                                0.0s
 => [3/3] RUN make glua                                                                                  14.1s
 => exporting to image                                                                                    0.4s 
 => => exporting layers                                                                                   0.4s 
 => => writing image sha256:1b34681df8e9a8db531586e52a37f2d39e5db52de3acb19d500838f6e5b6f166              0.0s 
 => => naming to docker.io/library/go-build-bench                                                         0.0s

aarch64 on Apple Silicon

$ docker build --no-cache -t go-build-bench .
[+] Building 1.5s (7/7) FINISHED                                                                               
 => [internal] load build definition from Dockerfile                                                      0.0s
 => => transferring dockerfile: 37B                                                                       0.0s
 => [internal] load .dockerignore                                                                         0.0s
 => => transferring context: 2B                                                                           0.0s
 => [internal] load metadata for docker.io/library/go-build-bench-base:latest                             0.0s
 => [1/3] FROM docker.io/library/go-build-bench-base                                                      0.0s
 => CACHED [2/3] WORKDIR /build/gopher-lua                                                                0.0s
 => [3/3] RUN make glua                                                                                   1.4s
 => exporting to image                                                                                    0.0s
 => => exporting layers                                                                                   0.0s
 => => writing image sha256:5f5da0ae77cb42e8c0378b73f964c10b662bfd7f4988c13c474c54c961590db3              0.0s
 => => naming to docker.io/library/go-build-bench                                                         0.0s

結構歴然と差が...。

10倍近い差がついてしまうため、Goなどクロスコンパイル環境が整っているのならaarch64の上でx86_64のバイナリを作る方が速い*3。例えば以下のようにDockerfileを変えてビルドしたときの結果を残す。

FROM go-build-bench-base

WORKDIR /build/gopher-lua
ENV GOARCH amd64
RUN make glua

CMD ["file", "/build/gopher-lua/glua"]
$ docker build --no-cache -t go-build-bench .
[+] Building 4.4s (7/7) FINISHED                                                                               
 => [internal] load build definition from Dockerfile                                                      0.0s
 => => transferring dockerfile: 194B                                                                      0.0s
 => [internal] load .dockerignore                                                                         0.0s
 => => transferring context: 2B                                                                           0.0s
 => [internal] load metadata for docker.io/library/go-build-bench-base:latest                             0.0s
 => [1/3] FROM docker.io/library/go-build-bench-base                                                      0.0s
 => CACHED [2/3] WORKDIR /build/gopher-lua                                                                0.0s
 => [3/3] RUN make glua                                                                                   4.3s
 => exporting to image                                                                                    0.1s 
 => => exporting layers                                                                                   0.1s 
 => => writing image sha256:7bc11d676123ead4f72b6d3b34d0580571b14ae751d2b4e9f71005babac59df4              0.0s 
 => => naming to docker.io/library/go-build-bench                                                         0.0s 

基本的に「本番バイナリはGitHub Actionで/Cloud Buildで/他〜」という運用にして確実かつ高速にx64サーバ上でビルドした方がよさそう。

が、そうは言っても手元で作れた方がいい... とか、色々な非常事態的場面もあるだろう、ということで残しておく。


追記

BuildX について教えてもらった。クロスプラットフォームではこっちを使う方が良さそう。いずれにせよCPUエミュレーションなどのオーバヘッドはかかりそうだが試したい。

docs.docker.com

追記2

あと、はてブにあった通り、Rosetta 2 ( arch コマンド)経由で仮想環境を作れるのならばそちらも試したいが、やり方が見つからずという感じなので、有識者に教えてほしい。

*1:両方インスタンスが立ち上がっていればすぐに切り替わる

*2:とはいえgo getの速度などはネットワーク状況で変わりうるので、あくまでも大体

*3:バイナリ単体であればこれで対応できるけど、「イメージ作りたいっす」だと困っちゃうね...