nekop's blog

OpenShift / JBoss / WildFly / Infinispanの中の人 http://twitter.com/nekop

Dockerfileの無駄なレイヤリングを排除してビルドを高速化する

Docker Advent Calendar 2016の10日目の記事です。土曜日は子供と遊んでいると思うので1日早く金曜日に公開しちゃいます。

Dockerでコンテナイメージを作成するときにはDockerfileを利用するのが最も一般的ですが、標準のdocker buildはインストラクション毎に中間レイヤーを作成し保存することを強制するようになっており、イメージ肥大化およびイメージビルドのパフォーマンス低下を避けるために、ユーザはコマンドを&&で繋げるRUNインストラクションを必要最小限な数になるよう最適化した奇妙なDockerfileを記述するという理不尽な作業が必要となります。

この強制レイヤリングは一応イメージビルド時の試行錯誤をしているときに中間までキャッシュできるので良い、という建前になっており、最初のうちはちょっと便利かなぁと思ったりもしますが、要件が固まっているベースレイヤは名前つけて必要ならpushもしちゃってFROMに書くと思いますので、実際に慣れてくるとこの仕組みはただ単に遅くてイライラするだけのものになり、Dockerfileの見た目も汚なくなってしまいます。

実例をみてみましょう。ENVRUNが3つずつある単なるDockerfileです。イメージの読み出しと保存があるので、ビルドの実行には3秒もあれば良いと考えるのが妥当でしょうか。実行環境はThinkPad T440s, 12GB mem, SSD storage, Fedora 25です。

FROM fedora

ENV FOO1=BAR1
ENV FOO2=BAR2
ENV FOO3=BAR3

RUN pwd
RUN pwd
RUN pwd

標準のdocker buildを実行してみます。13秒かかりました。

$ docker -v
Docker version 1.12.3, build 97974ae/1.12.3
$ time docker build . --no-cache -t normal-dockerbuild:latest
Sending build context to Docker daemon 
Step 1 : FROM fedora
 ---> 5bafccc4d5dc
Step 2 : ENV FOO1 BAR1
 ---> Running in fa7318e71dac
 ---> 7751ce4fb55e
Removing intermediate container fa7318e71dac
Step 3 : ENV FOO2 BAR2
 ---> Running in e9f8c9062182
 ---> d4240ff674a4
Removing intermediate container e9f8c9062182 
Step 4 : ENV FOO3 BAR3
 ---> Running in 9108d53e3193
 ---> 2b1dd6f364fb
Removing intermediate container 9108d53e3193
Step 5 : RUN pwd
 ---> Running in f77b5b36eb26
/
 ---> e22d9ef35156
Removing intermediate container f77b5b36eb26
Step 6 : RUN pwd
 ---> Running in 43e1e4b3a758 
/
 ---> 6e5bec6562f6
Removing intermediate container 43e1e4b3a758
Step 7 : RUN pwd
 ---> Running in d6a73a87c283
/
 ---> b8a0040efd28
Removing intermediate container d6a73a87c283
Successfully built b8a0040efd28

real    0m13.206s
user    0m0.041s 
sys     0m0.056s

$ docker images -a
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
normal-dockerbuild   latest              b8a0040efd28        1 seconds ago       199.9 MB
<none>               <none>              6e5bec6562f6        3 seconds ago       199.9 MB
<none>               <none>              e22d9ef35156        5 seconds ago       199.9 MB
<none>               <none>              2b1dd6f364fb        8 seconds ago       199.9 MB
<none>               <none>              d4240ff674a4        10 seconds ago      199.9 MB
<none>               <none>              7751ce4fb55e        12 seconds ago      199.9 MB
docker.io/fedora     latest              5bafccc4d5dc        2 weeks ago         199.9 MB

次のテストのために作ったイメージは一旦削除します。

$ docker rmi normal-dockerbuild:latest
$ docker images -q -f dangling=true | xargs --no-run-if-empty docker rmi

ここでおもむろにOpenShiftのリリースからocクライアントをダウンロードします。今回利用したのはv1.4.0-rc1のLinux版です。OpenShiftにはdocker buildと同等のoc ex dockerbuildコマンドがあり、成果物はdocker buildと同じですが、中間レイヤをDocker daemonに投げることはしません。

oc ex dockerbuildを実行してみます。2.5秒でビルドできました。このケースでは標準のdocker buildより5倍高速にビルドできています。

$ time ./oc ex dockerbuild . ex-dockerbuild:latest
--> FROM fedora
--> ENV FOO1=BAR1
--> ENV FOO2=BAR2
--> ENV FOO3=BAR3
--> RUN pwd
/
--> RUN pwd
/
--> RUN pwd
/
--> Committing changes to ex-dockerbuild:latest ...
--> Done

real    0m2.543s
user    0m0.205s
sys     0m0.011s

$ docker images -a
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ex-dockerbuild      latest              6b854accd835        1 seconds ago       199.9 MB
docker.io/fedora    latest              5bafccc4d5dc        2 weeks ago         199.9 MB

また、Dockerfileを利用しないコンテナイメージビルドの方法として、Ansible Containerというのも絶賛開発中ですのでお試しください。DockerfileではなくAnsibleを利用してコンテナイメージをビルドします。

テストに利用したスクリプトを以下に置いておきます。

#!/bin/bash

set -x

docker ps -q -f status=exited | xargs --no-run-if-empty docker rm
docker images -q -f dangling=true | xargs --no-run-if-empty docker rmi
docker images -a
time docker build . --no-cache -t normal-dockerbuild:latest
docker images -a
docker rmi normal-dockerbuild:latest
docker ps -q -f status=exited | xargs --no-run-if-empty docker rm
docker images -q -f dangling=true | xargs --no-run-if-empty docker rmi
time ./oc ex dockerbuild . ex-dockerbuild:latest
docker images -a
docker rmi ex-dockerbuild:latest

Storage Driverの差異について追記

ブコメでStorage Driverがdevicemapperのせいで差が大きく出ているのでは、という指摘があった(いいご指摘です、ありがとうございます)ので、overlayfsでの結果も載せておきます。差は縮まりましたが4倍の差がでており十分に高速です。

$ time docker build . --no-cache -t normal-dockerbuild:latest
real    0m6.784s
user    0m0.043s
sys 0m0.053s

$ time ./oc ex dockerbuild . ex-dockerbuild:latest
real    0m1.511s
user    0m0.172s
sys 0m0.013s

個人的にはもうdockerは専用のクラスタへオフロードしてしまって手元のラップトップでは動作させていない、かつ最近リリースされたFedora 25にしたときにすっかりStorage Driverの設定を忘れていたので、追記前の結果はもっさりdevicemapper loopbackでした。