はいさい〜
概要
cargo-chefを使ってDockerでビルドしているRustアプリのビルド時間を短縮する方法を紹介します。
問題
Dockerのキャッシュの仕組みとRustのビルドの相性が合わず、対策をしないと、毎回第三者パッケージをゼロからコンパイルし直すことになる
これでCICDのパイプラインが遅くなるばかりか、GitHub Actionsなど稼働時間によって課金されるサービスを使っている場合は料金に直結する問題です。💰💰
解決法
筆者が今回自らのプロジェクトで試した手法は、本題のcargo chefを使うことでした。
以下、元のファイルです:
FROM rust:1.98.0 AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y cmake libopenblas-dev libomp-dev git libgtest-dev build-essential wget unzip libgflags-dev
RUN git clone https://github.com/facebookresearch/faiss.git .
RUN git checkout 20f14b31a6d54e243a3d1de6ae193fc4c3ec18ed
# Faiss build
RUN cmake -B build -DFAISS_ENABLE_GPU=OFF -DFAISS_ENABLE_C_API=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DFAISS_ENABLE_PYTHON=OFF -DBUILD_TESTING=OFF
RUN cmake --build build
RUN make -C build -j8 faiss_c
RUN make -C build install
RUN cp /app/build/c_api/libfaiss_c.so /usr/lib/
# Download libtorch
RUN wget -O /tmp/libtorch-240.zip https://download.pytorch.org/libtorch/cpu/libtorch-shared-with-deps-2.11.0%2Bcpu.zip
RUN unzip /tmp/libtorch-240.zip -d /tmp/
RUN cp -r /tmp/libtorch /usr/lib/
RUN cp -a /tmp/libtorch/lib/** /usr/lib/
ENV LIBTORCH_INCLUDE=/usr/lib/libtorch
ENV LIBTORCH_LIB =/usr/lib/libtorch
ENV LIBTORCH=/usr/lib/libtorch
ENV LD_LIBRARY_PATH=${LIBTORCH}/lib:$LD_LIBRARY_PATH
COPY lb_jh_servers/Cargo.toml .
COPY Cargo.lock .
COPY . .
RUN cargo build -p lb_jh_servers --bin admin --release --features admin_app
FROM debian:stable-slim AS runner
# 省略
コンパイルするアプリは、Faissだったり、LibTorchだったり、機械学習とAI関連の複雑な部品を使ったビルドでRustの依存パッケージのビルドも非常に時間のかかるものです。
そして、cargo-chefを使って書き直したら以下のようになりました:
FROM rust:1.97.1 AS chef
RUN cargo install --locked cargo-chef
WORKDIR app
FROM chef AS planner
WORKDIR /app
COPY . .
RUN cargo chef prepare --bin admin --recipe-path recipe.json
FROM chef AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y cmake libopenblas-dev libomp-dev git libgtest-dev build-essential wget unzip libgflags-dev
RUN git clone https://github.com/facebookresearch/faiss.git .
RUN git checkout 20f14b31a6d54e243a3d1de6ae193fc4c3ec18ed
# Faiss build
RUN cmake -B build -DFAISS_ENABLE_GPU=OFF -DFAISS_ENABLE_C_API=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DFAISS_ENABLE_PYTHON=OFF -DBUILD_TESTING=OFF
RUN cmake --build build
RUN make -C build -j8 faiss_c
RUN make -C build install
RUN cp /app/build/c_api/libfaiss_c.so /usr/lib/
# Download libtorch (x86)
RUN wget -O /tmp/libtorch-240.zip https://download.pytorch.org/libtorch/cpu/libtorch-shared-with-deps-2.11.0%2Bcpu.zip
RUN unzip /tmp/libtorch-240.zip -d /tmp/
RUN cp -r /tmp/libtorch /usr/lib/
RUN cp -a /tmp/libtorch/lib/** /usr/lib/
ENV LIBTORCH_INCLUDE=/usr/lib/libtorch
ENV LIBTORCH_LIB =/usr/lib/libtorch
ENV LIBTORCH=/usr/lib/libtorch
ENV LD_LIBRARY_PATH=${LIBTORCH}/lib:$LD_LIBRARY_PATH
# Use rust build dep cache
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --bin admin --recipe-path recipe.json --features admin_app
COPY . .
RUN cargo build -p lb_jh_servers --bin admin --release --features admin_app
ここでcargo-chefのおかげで、planner段階から取得されたrecipe.jsonで含まれた依存パッケージが変わっていなければ、Dockerのキャッシュが利用され、多くの第三者依存パッケージを再ビルドせずに、スキップできるようになるのです。
Workspaceでの注意点
筆者の場合、複数のパッケージが同じRust Workspaceに置かれており、指定しないと不要なパッケージまでビルドされてしまうし、実際にcargo buildを実行する時にcargoのキャッシュが外れます。
この場合は、cargo-chefの--binフラグを使います。
使っているRust Workspaceのビルド対象になるbinの名前を調べて、cargo chef prepare --bin my_bin、cargo chef cook --bin my_binというふうに両方のコマンドで実行すると、そのbinに必要なパッケージのみがrecipe.jsonに含まれます。
ちなみに、対象にしたいbinのworkspaceにおける名前がわからない場合はcargo build --binを実行すれば、すべてのbin名を教えてくれます。
その他の注意点
筆者が困ったところの注意点を共有して参考になればと思います。
--releaseフラグ
cargo-chefのcookコマンドで--releaseのフラグを使用しないで、実際にcargo buildのところで--releaseを使ってしまうと、せっかくコンパイルされた第三者依存パッケージが無駄になってしまうので、忘れないようにお気をつけください。
--featuresフラグ
これもcargo-chefのcookコマンドですが、cargo build --features ...で指定しているものも、同様にcargo chef cook --features ...のところで指定しないとまたキャッシュが外れてしまいます。
RUNの順番
Dockerのキャッシュの仕組みを深く説明するつもりはないのですが、ステップ間で変更を検知すれば、キャッシュを無視して再実行になるので、cargo-chefのコマンドを実行する場所と、Rustのコンパイルに必要なファイルのコピー(COPY . .)のステップにご注意ください。
RUN cargo chef cook ...は必ずCOPY . .の前にします。そうじゃないとキャッシュは無意味になります。
まとめ
こちらの場合は、30分〜40分かかっていたビルドが、今回の改善で10~11分台に短縮できたので大いに満足しています。待ち時間が長すぎていよいよイライラが行動を起こしたのです。
皆さんも同様な問題を抱えているなら、ぜひ解決しましょう。