Mọi thứ ở phần 6 và phầ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 |
Có --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ủamain. Nhánh mới vẫn được hưởng cache thay vì build từ đầu. --cache-totheo 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.