はじめに
Rustで書かれたアプリケーションをコンテナイメージとしてビルドする際にDockerfileの記述方法にいくつか書き方があったので、それぞれについてイメージのビルド時間の比較してみました。
背景
というのも、Rustはアプリケーションのビルド時間が長いためDockerfileの書き方によって開発効率を悪くしてしまう場合があります。
例えば、Rustで作ったアプリケーションをKubernetesで動かす場合を想定します。ローカルでのアプリケーション開発にskaffoldを利用するとします。
skaffoldはコードの変更を検知し、イメージの再ビルドを行い、Kubernetes上にアプリケーションを再デプロイします。
このとき、Rust特有のビルド時間が長い問題にぶち当たります。ビルド時間がながいことによって、コードの変更に対してイメージの再ビルドが追いつかないのです。
上記は一例ですが、どうせならビルドの速いDockerfileを書きたいよねっということで、いくつか比較してみました。
方法
今回は、コードの変更によって生じるイメージのビルド時間を測定し、それらを比較します。ビルドイメージサイズに関しては特に気にしてません。またcargo workspaceでの開発も想定していません。
serde
とrocket
を使ったアプリを想定します。Cargo.toml
、main.rs
は下記のようにしています。
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rocket = { git = "https://github.com/SergioBenitez/Rocket" }
#[macro_use] extern crate rocket;
#[get("/")]
fn hello() -> &'static str {
"Hello, before build!"
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![hello])
}
一度イメージのビルドを実行した後、コードを変更後イメージの再ビルドを行います。イメージとしては、下記のようなスクリプトになります。変更前と変更後のコードは上記のhello()
関数の返り値が異なります。
rm -rf src && cp -r testsrc/before src
docker build -q -f Dockerfile.base -t rust-docker-base .
rm -rf src && cp -r testsrc/after src
time docker build -q -f Dockerfile.base -t rust-docker-base .
Github上に各Dockerfileとスクリプトを置いています。
対象
1. base
最も基本的なDockerfileです。
FROM rust:1.48.0
WORKDIR /app
COPY . .
RUN cargo build --release
ENTRYPOINT ["/app/target/release/app"]
2. echo
Fast + Small Docker Image Builds for Rust Appsで紹介されているイメージのビルド方法です。
アプリケーションのビルドキャッシュをつくるために一度テンポラリなmain.rsを作成してます。
# https://shaneutt.com/blog/rust-fast-small-docker-image-builds/
FROM rust:1.48.0
WORKDIR /app
COPY Cargo.toml Cargo.toml
RUN mkdir src/
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs
RUN cargo build --release
RUN rm -f target/release/deps/app*
COPY . .
RUN cargo build --release
ENTRYPOINT ["/app/target/release/app"]
3. cargo-chef
cargo-chefを使ったイメージのビルド方法です。
# https://github.com/LukeMathWalker/cargo-chef
FROM rust as planner
WORKDIR app
# We only pay the installation cost once,
# it will be cached from the second build onwards
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM rust as cacher
WORKDIR app
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
FROM rust as builder
WORKDIR app
COPY . .
# Copy over the cached dependencies
COPY --from=cacher /app/target target
COPY --from=cacher $CARGO_HOME $CARGO_HOME
RUN cargo build --release
ENTRYPOINT ["/app/target/release/app"]
4. BuildKit + base
対象1のDockerfileに加えて、BuildKitを使用します。
FROM rust:1.48.0
WORKDIR /app
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release
ENTRYPOINT ["/app/target/release/app"]
5. BuildKit + sccache
BuildKitを使用していますが、あくまでキャッシュするのはsccacheで保存しているものに限定しています。
FROM rust:1.47.0
RUN cargo install sccache
ENV HOME=/app
ENV SCCACHE_CACHE_SIZE="1G"
ENV SCCACHE_DIR=$HOME/.cache/sccache
ENV RUSTC_WRAPPER="/usr/local/cargo/bin/sccache"
WORKDIR $HOME
COPY . .
RUN --mount=type=cache,target=/app/.cache/sccache cargo build --release
ENTRYPOINT ["/app/target/release/app"]
結果
以下のPCで計測しました。念の為、ビルド実行する前にdocker system prune -f -a
を実行しました。
macOS Catalina v10.15.7
MacBook Pro (16-inch, 2019)
Processor 2.4GHz 8-Core Intel Core i9
Memory 64GB 2667 MHz DDR4
結果は下記のようなものになりました。
Time of Dockerfile.base
87.27 real 2.86 user 0.86 sys
Time of Dockerfile.echo
21.49 real 2.86 user 0.85 sys
Time of Dockerfile.cargochef
18.40 real 2.86 user 0.84 sys
Time of Dockerfile.buildkit-base
14.09 real 0.12 user 0.06 sys
Time of Dockerfile.buildkit-sccache
34.50 real 0.16 user 0.08 sys
順番を入れ替えてもだいたい同じ結果になりました。
Time of Dockerfile.buildkit-base
15.63 real 0.15 user 0.08 sys
Time of Dockerfile.cargochef
18.81 real 3.05 user 0.92 sys
Time of Dockerfile.echo
22.82 real 3.00 user 0.92 sys
Time of Dockerfile.buildkit-sccache
35.12 real 0.16 user 0.09 sys
Time of Dockerfile.base
90.60 real 2.93 user 0.89 sys
感想
今回の実験だと、BuildKitを使うと速いですね。プライベートだと、echo
のDockerfileを使っていたのでちょっと置き換えて本当に速いか試してみたいと思います(置き換えれるか)。
emk/rust-musl-builderを使った場合どうなるのかとかもで試したいですね。
他にもこういうのもあるよっていうのがありましたら、ぜひコメントかGithubのIssueかPRください!
今回のコード類は、mkazutaka/rust-dockerfile-comparisonにあります。
ちなみに
私が使用しているRocketフレームワークを使っている最終的なアプリケーションのDockerfileです。
skaffoldのv1.17.2はBuildkit動かないので(skaffold Issue #5178)、bleeding edge buildをつかってください。
musl遅いよって記事(Why does musl make my Rust code so slow?)を見てからベースイメージにdebianを使うようにしています
FROM debian:buster-slim as runner
RUN apt update; apt install -y libssl1.1
FROM rust:1.48.0 as builder
WORKDIR /usr/src
RUN rustup target add x86_64-unknown-linux-musl
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/src/target \
cargo install --path .
FROM runner
COPY --from=builder /usr/local/cargo/bin/myapp .
COPY Rocket.toml .
USER 1000
CMD ["./myapp"]
参考
- Cache Rust dependencies with Docker build - Stack Overflow
- cargo build --dependencies-only · Issue #2644 · rust-lang/cargo
- Rust - Fast + Small Docker Image Builds
- LukeMathWalker/cargo-chef: A cargo-subcommand to speed up Rust Docker builds using Docker layer caching.
- benmarten/sccache-docker-test
- Build images with BuildKit | Docker Documentation
- How to Package Rust Applications Into Minimal Docker Containers · alexbrand's blog