Phần 6 đã cho thấy sắp xếp COPY đúng chỗ giữ được cache của bước cài phụ thuộc. Nhưng khi requirements.txt thật sự đổi — thêm một gói — thì bước đó chạy lại từ đầu, tải lại toàn bộ.

--mount=type=cache giải quyết đúng chuyện đó: giữ thư mục cache của trình quản lý gói giữa các lần build, độc lập với layer cache.

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

Phép đo đầu tiên của tôi nói nó vô dụng

khong cache mount, lan 1: 29,2 s
khong cache mount, lan 2: 31,2 s
CO cache mount,    lan 1: 29,4 s
CO cache mount,    lan 2: 38,9 s
CO cache mount,    lan 3: 36,6 s

Có cache mount chậm hơn. Con số này vô lý nên tôi đi kiểm tra thay vì viết ra.

Thư mục gắn đúng — pip cache dir trả về /root/.cache/pip. Cache có được điền — 38 MB sau lần build đầu. Nhưng đếm số gói pip dùng lại:

co cache mount, lan 2: tai ve 42 goi | dung lai 0 goi
co cache mount, lan 3: tai ve 42 goi | dung lai 0 goi

Cache đầy 38 MB mà không gói nào được dùng lại.

Thủ phạm là chính cờ tôi dùng để đo. Tôi thêm --no-cache vào docker build để buộc lệnh RUN chạy lại — và --no-cache cũng vô hiệu hoá cache mount.

Phép thử xác nhận:

build BINH THUONG            : dung lai 42 goi
build --no-cache             : dung lai  0 goi
build binh thuong lai sau do : dung lai 42 goi

Cờ đó bỏ qua cache mount cho lần build ấy, chứ không xoá nó — lần build bình thường tiếp theo lại dùng được đủ 42 gói.

Đây không chỉ là chuyện đo đạc. Rất nhiều đường ống CI có --no-cache để "cho chắc", và chúng đang vô hiệu hoá cache mount mà không ai biết. Nếu bạn thêm cache mount rồi thấy CI không nhanh lên, hãy tìm cờ đó trước.

Phép đo đúng

Buộc RUN chạy lại bằng một ARG đổi giá trị mỗi lần, không dùng --no-cache:

Lần 1 Lần 2 Lần 3
cache mount 43,4 s 12,4 s 12,2 s
Không cache mount 74,1 s 64,9 s 45,1 s

Và số gói:

co cache mount,    lan 2: tai ve  0 | dung lai 42
khong cache mount, lan 2: tai ve 42 | dung lai  0

Từ lần thứ hai trở đi, cache mount cho 12,2 giây so với 45–65 giây — nhanh hơn khoảng bốn lần, và không tải một byte nào qua mạng.

Lần đầu vẫn phải tải (43,4 s), đó là chuyện tất nhiên. Cái đáng giá là mọi lần sau.

Công thức cho các hệ sinh thái

Nguyên tắc giống nhau: tìm thư mục cache của trình quản lý gói rồi gắn vào.

# Python
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Node
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# Maven
RUN --mount=type=cache,target=/root/.m2 \
    mvn -B package -DskipTests

# Go
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg/mod \
    go build -o /app .

# Debian/Ubuntu — can hai thu muc, va phai tat co che tu xoa cua apt
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean \
 && apt-get update && apt-get install -y --no-install-recommends curl

# Alpine
RUN --mount=type=cache,target=/var/cache/apk \
    apk add --no-cache curl

Hai chi tiết dễ sai:

Với apt, image Debian có sẵn /etc/apt/apt.conf.d/docker-clean — một tệp cấu hình tự xoá gói .deb ngay sau khi cài. Không gỡ nó thì cache mount luôn rỗng.

Đừng dùng --no-cache-dir của pip cùng với cache mount. Hai thứ đó triệt tiêu nhau: một bên bảo pip đừng lưu cache, một bên gắn thư mục cache vào. Phần 16 đo được --no-cache-dir chỉ tiết kiệm 1,8 MB dung lượng image; đổi 1,8 MB đó lấy bốn lần tốc độ build là món hời rõ ràng. Với cache mount thì nội dung cache không vào image, nên bạn được cả hai.

Chế độ chia sẻ

Cache mount có ba chế độ, khai bằng sharing=:

Nghĩa
shared (mặc định) nhiều build cùng ghi vào một cache
locked build sau chờ build trước xong
private mỗi build một cache riêng

apt cần locked vì hai tiến trình apt cùng ghi vào một thư mục sẽ hỏng cơ sở dữ liệu gói. pip, npm, go an toàn với shared — chúng ghi theo tệp riêng biệt.

Nếu bạn dựng nhiều image song song trên cùng một máy CI, đây là tham số quyết định giữa "nhanh gấp bốn" và "hỏng ngẫu nhiên".

Cái giá: dung lượng đĩa

Build Cache: 2,705 GB
Reclaimable: 2,705 GB

Cache mount nằm chung với build cache và không xuất hiện trong docker images — đúng như phần 2 đã đo. Nó lớn dần theo số dự án bạn dựng, và không tự giới hạn.

Dọn có chọn lọc:

docker buildx du                    # xem het bao nhieu
docker builder prune --filter until=168h   # xoa cache cu hon 7 ngay
docker builder prune -af            # xoa sach, ke ca cache mount

Trên máy CI, một lịch dọn theo tuổi là đủ. Đừng chạy prune -af sau mỗi lần build — làm vậy là bỏ đi chính thứ bạn vừa thêm vào để tăng tốc.

Cache mount so với layer cache

Hai thứ khác nhau và bổ sung cho nhau:

Layer cache Cache mount
Giữ gì kết quả của cả một lệnh thư mục làm việc của công cụ
Mất khi lệnh hoặc thứ trước nó đổi không mất, trừ khi bị dọn
Giúp khi không có gì đổi đổi, nhưng phần lớn phụ thuộc giữ nguyên
Vào image có (thành layer) không

Nói cách khác: layer cache lo trường hợp "không đổi gì", cache mount lo trường hợp "đổi một dòng trong requirements.txt". Cần cả hai.

Thử ba mươi giây

Xem bước cài phụ thuộc của bạn tốn bao lâu khi cache hỏng:

echo "# $(date)" >> requirements.txt        # buoc pip chay lai
time docker build -t thu .

Rồi thêm một dòng cache mount vào lệnh cài đó và chạy lại đúng phép thử. Nếu con số không giảm, kiểm tra hai thứ theo thứ tự: có --no-cache trong lệnh build không, và thư mục bạn gắn có đúng là thư mục công cụ dùng không:

docker run --rm <anh-nen> pip cache dir      # hoac: npm config get cache

Phần sau đo cache chia sẻ giữa các máy CI — thứ giải quyết chuyện máy build mới luôn có cache rỗng.