Trực chiến 2h sáng và “nỗi đau” mang tên Rust Compilation
Màn hình terminal nhấp nháy lúc 2 giờ sáng. Một lỗi logic nghiêm trọng trên Production cần được vá ngay lập tức. Mình sửa code xong trong 30 giây, đẩy lên Git và nín thở đợi CI/CD. Nhưng thanh tiến trình GitHub Actions cứ chạy lầm lũi: 5 phút, 10 phút, rồi 15 phút. Pipeline vẫn kẹt cứng ở bước cargo build --release.
Cảm giác lúc đó thực sự muốn đập máy. Rust cực mạnh về hiệu năng, nhưng thời gian compile lại là một “cơn ác mộng” khi chạy trong môi trường Docker sạch. Chỉ cần thay đổi một dòng code nhỏ, Docker sẽ làm mất cache. Nó bắt đầu tải lại hàng trăm dependencies và compile lại từ đầu. Với hệ thống hơn 30 container mình đang quản lý, nếu không tối ưu, chi phí tài nguyên và thời gian chờ đợi sẽ là một thảm họa về vận hành.
Mấu chốt khiến Docker build cho Rust chậm kinh khủng
Vấn đề nằm ở cơ chế phân tầng (layering). Thông thường, một Dockerfile “ngây thơ” sẽ trông như thế này:
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/my-app /usr/local/bin/
CMD ["my-app"]
Trông thì có vẻ ổn, nhưng chỉ cần bạn sửa một dấu phẩy trong file .rs, lệnh COPY . . sẽ vô hiệu hóa toàn bộ cache của các layer phía sau. Docker không đủ thông minh để hiểu rằng bạn chỉ sửa logic app chứ không hề đổi thư viện trong Cargo.toml. Kết quả? Nó chạy lại cargo build, kéo lại tokio, serde, axum… và compile lại cả thế giới.
Lên bàn cân: 3 cách tiếp cận phổ biến
Mình đã kinh qua đủ mọi thủ thuật để “hack” cái cache này trước khi tìm thấy chân ái.
1. Copy tất cả (Naive approach) – Thảm họa CI/CD
- Ưu điểm: Viết nhanh, dễ hiểu.
- Nhược điểm: Chậm nhất. Hoàn toàn không tận dụng được cache cho dependencies. Cực kỳ tốn băng thông và CPU của build server.
2. Thủ thuật Dummy Main – Chạy ổn nhưng hơi “phèn”
Anh em thường tạo file src/main.rs giả với nội dung fn main() {}, copy Cargo.toml vào trước để build lấy cache, sau đó mới copy code thật.
# Cách này cực kỳ phổ biến nhưng rất thủ công
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -f target/release/deps/my_app*
COPY . .
RUN cargo build --release
- Ưu điểm: Đã bắt đầu dùng được cache cho dependencies.
- Nhược điểm: Dễ lỗi nếu project có nhiều binary. Phải dọn dẹp file rác thủ công (lệnh
rm). Dockerfile trông chắp vá và thiếu chuyên nghiệp.
3. Cargo Chef – Chân ái cho dân DevOps
Đây là công cụ mình đang áp dụng cho toàn bộ microservices hiện tại. Cargo Chef sinh ra để giải quyết đúng một việc: Tách biệt bước tính toán dependencies và bước build code.
- Ưu điểm: Tối ưu tuyệt đối layer caching. Hỗ trợ cực tốt cho Workspace (nhiều crate). Không cần thủ thuật tạo file giả.
- Nhược điểm: Cần thêm một bước cài tool nhỏ trong Docker image builder.
Triển khai Cargo Chef: “Công thức” thực tế
Trên cluster chạy 30+ container, mình đã tiết kiệm được khoảng 40% tài nguyên CPU nhờ mẫu Dockerfile Multi-stage dưới đây.
Bước 1: Lập kế hoạch (The Planner)
Cargo Chef sẽ quét dự án để tạo ra file recipe.json. File này gói gọn thông tin về các thư viện cần thiết mà không dính dáng gì đến code logic của bạn.
FROM lucatadeu/cargo-chef:latest-rust-1.75 AS chef
WORKDIR /app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-json recipe.json
Bước 2: “Nấu” Dependencies (The Cacher)
Đây là mấu chốt. Docker sẽ cache layer này chặt chẽ. Chỉ khi bạn sửa Cargo.toml, nó mới rebuild. Nếu chỉ sửa code trong src/, bước này sẽ lướt qua trong tích tắc.
FROM chef AS cacher
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-json recipe.json
Bước 3: Build code thực tế (The Builder)
Giờ mới là lúc đưa code thật vào. Vì dependencies đã compile sẵn từ bước trước, lệnh cargo build lúc này chỉ tốn vài giây để xử lý phần logic mới.
FROM chef AS builder
COPY . .
COPY --from=cacher /app/target target
COPY --from=cacher /usr/local/cargo /usr/local/cargo
RUN cargo build --release --bin my-app
Bước 4: Runtime Image nhẹ tênh
Đừng dùng image rust để chạy app vì nó nặng cả GB. Hãy dùng debian-slim để tối ưu dung lượng.
FROM debian:bookworm-slim AS runtime
WORKDIR /app
COPY --from=builder /app/target/release/my-app /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/my-app"]
Kết quả: Những con số biết nói
Trước khi tối ưu, mỗi lần sửa một câu log, mình mất trung bình 12 phút để build image. Sau khi áp dụng Cargo Chef, thời gian build cho những lần thay đổi logic tiếp theo chỉ còn vỏn vẹn 45 giây đến 1 phút.
Tại sao lại nhanh thế? Đơn giản là vì cargo chef cook đã giữ lại toàn bộ object files của các thư viện nặng như openssl hay diesel. Rust compiler chỉ việc link lại code mới với đống thư viện đã có sẵn, thay vì phải “nhai” lại hàng triệu dòng mã nguồn bên thứ ba.
Nếu bạn đang đau đầu vì CI/CD chạy quá lâu, hãy thử Cargo Chef ngay. Nó không chỉ giúp server build “dễ thở” hơn mà còn giúp tâm trạng bạn thoải mái hơn hẳn khi phải deploy hotfix lúc nửa đêm.

