Mọi thứ ở phần 6phần 18 đều giả định một điều: cache nằm trên máy đang build. Trên máy CI thì giả định đó thường sai — mỗi lần chạy là một container sạch, cache rỗng, và mọi tối ưu Dockerfile của bạn thành vô nghĩa.

BuildKit giải quyết bằng cách đẩy cache ra ngoài rồi kéo về ở lần build sau, trên bất kỳ máy nào.

Tôi dựng một registry cục bộ và hai builder riêng biệt để mô phỏng hai máy CI.

64,0 giây xuống 6,2 giây

Máy A build và đẩy cache lên registry:

docker buildx build --builder may-a \
  --cache-to type=registry,ref=localhost:5000/cache-app:main,mode=max \
  --output type=cacheonly .

Máy B là một builder hoàn toàn mới, chưa từng thấy dự án này:

Thời gian Số bước CACHED
Không nhập cache 64,0 s 0
--cache-from 6,2 s 5

Nhanh hơn 10,3 lần. Năm bước được dùng lại nguyên vẹn dù máy này chưa từng chạy một lệnh docker build nào cho dự án.

Đây là thứ biến "tối ưu Dockerfile" từ chuyện chỉ có tác dụng trên máy lập trình viên thành chuyện có tác dụng trên CI.

mode=min so với mode=max: gấp bốn lần dung lượng, gần gấp đôi tốc độ

Với build multi-stage và có xuất image thật (--push):

Registry tăng Máy sạch build
mode=min 45,6 MB 11,0 s
mode=max 188,2 MB 6,2 s

mode=min chỉ lưu cache của những layer có mặt trong image cuối. mode=max lưu cả các tầng trung gian — tầng build với toàn bộ build-essential, thư mục gói đã cài, mọi thứ.

Gấp bốn lần dung lượng đổi lấy 1,8 lần tốc độ. Với đa số đường ống CI thì mode=max đáng, vì dung lượng registry rẻ hơn thời gian chờ của lập trình viên — nhưng nếu bạn trả tiền theo dung lượng lưu trữ và build hàng trăm nhánh mỗi ngày thì con số 188 MB mỗi nhánh cần được nhân lên trước khi quyết định.

Một lưu ý về cách đo: lần đầu tôi so hai chế độ với --output type=cacheonly và chúng giống hệt nhau — 6 bước CACHED, cùng thời gian. Vì không có image nào được xuất ra, BuildKit lưu mọi thứ ở cả hai chế độ, nên khác biệt biến mất. Phải có --push hoặc --load thì min mới thật sự là min.

Trường hợp thường gặp nhất: chỉ sửa mã nguồn

Đây mới là kịch bản CI hằng ngày — phụ thuộc không đổi, chỉ vài dòng mã:

may sach + cache cu, ma nguon da doi: 7,6 s | 4 buoc CACHED

Bốn trên sáu bước vẫn dùng lại được. Hai bước phải chạy lại là COPY app.py và tầng cuối phụ thuộc vào nó — đúng như luật ở phần 6: mất cache ở đâu thì mất từ đó trở đi.

Nghĩa là cache registry vẫn hiệu quả kể cả khi mã nguồn đổi, miễn là Dockerfile của bạn đặt COPY mã nguồn sau bước cài phụ thuộc.

Cache kiểu local

Không có registry thì dùng thư mục trên đĩa:

docker buildx build --cache-to type=local,dest=./bo-nho-cache,mode=max ...
docker buildx build --cache-from type=local,src=./bo-nho-cache ...
cache local tren dia   : 229,3 MB
may sach nhap tu local :   8,1 s | 6 buoc CACHED

Hiệu quả tương đương registry (8,1 so với 6,2 giây), và hợp khi hệ thống CI của bạn đã có cơ chế cache thư mục sẵn — GitLab CI cache:, Jenkins workspace, hay một ổ đĩa dùng chung.

Nhược điểm: type=local luôn ghi lại toàn bộ cache mỗi lần, không ghi thêm. Với 229 MB thì việc tải lên tải xuống của hệ thống CI có thể ăn mất phần thời gian bạn vừa tiết kiệm — đo cả khoản đó trước khi chọn.

Bốn kiểu cache backend

Kiểu Dùng khi
type=registry phổ biến nhất; cần một registry ghi được
type=local có thư mục dùng chung giữa các lần chạy
type=gha GitHub Actions, dùng kho cache của chính nó
type=inline nhét cache vào chính image; đơn giản nhưng chỉ tương đương mode=min

type=inline đáng nhắc vì nó là cách rẻ nhất để bắt đầu — không cần kho lưu trữ riêng:

docker buildx build --cache-to type=inline --cache-from myapp:latest -t myapp:latest --push .

Đổi lại, nó không lưu được tầng trung gian, nên với multi-stage build thì lợi ích nhỏ hơn hẳn con số 6,2 giây ở trên.

Mẫu dùng trong CI

Ba thứ nên có:

docker buildx build \
  --cache-from type=registry,ref=registry.vi-du/app:cache-main \
  --cache-from type=registry,ref=registry.vi-du/app:cache-$NHANH \
  --cache-to   type=registry,ref=registry.vi-du/app:cache-$NHANH,mode=max \
  --tag registry.vi-du/app:$SHA --push .
  • Hai --cache-from: thử cache của chính nhánh trước, không có thì lấy của main. Nhánh mới vẫn được hưởng cache thay vì build từ đầu.
  • --cache-to theo nhánh: nhánh này không ghi đè cache của nhánh khác.
  • Tag bằng SHA: phần 4 đã cho thấy tag di động không đáng tin để triển khai.

Và nhớ bài học từ phần 18: đừng để --no-cache trong lệnh build. Nó vô hiệu hoá cả cache mount lẫn khả năng dùng lại layer, và biến toàn bộ cấu hình trên thành vô nghĩa.

Cache cũ có tự dọn không

Không. Cache trong registry là những tag bình thường và nằm đó mãi. Với mode=max và một nhánh mới mỗi tính năng, bạn tích 188 MB cho mỗi nhánh cho tới khi hết dung lượng.

Cần một cơ chế dọn: chính sách vòng đời của registry, hoặc một tác vụ định kỳ xoá tag cache của nhánh đã gộp.

Thử ba mươi giây

Xem lần build CI gần nhất của bạn dùng lại được bao nhiêu:

docker buildx build --progress=plain -t thu . 2>&1 | grep -c CACHED

So con số đó với tổng số bước trong Dockerfile. Trên máy lập trình viên nó thường gần bằng nhau; trên CI, nếu bạn chưa cấu hình --cache-from, nó là 0 — và mọi công sức sắp xếp Dockerfile ở các phần trước đang không có tác dụng ở nơi quan trọng nhất.

Phần sau dựng ảnh cho nhiều kiến trúc bằng buildx, và đo thời gian build chéo kiến trúc.