Cache của Docker hoạt động theo một luật duy nhất: một layer dùng lại được nếu lệnh sinh ra nó và mọi thứ trước nó không đổi. Từ luật đó suy ra tất cả, nhưng suy ra thì dễ mà nhận ra hậu quả trong một Dockerfile thật thì khó. Bài này đo.
Hai Dockerfile, cùng kết quả, khác thứ tự
Một dự án Python nhỏ: bốn phụ thuộc trong requirements.txt, hai mươi tệp mã nguồn.
# SAI: copy het roi moi cai phu thuoc
FROM python:3.12-alpine
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
# DUNG: copy manifest truoc, cai, roi moi copy ma
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
Hai image giống hệt nhau về nội dung. Khác nhau lúc build:
| Build đầu (cache rỗng) | Sửa một dòng mã rồi build lại | |
|---|---|---|
| Thứ tự sai | 11,8 s | 5,7 s |
| Thứ tự đúng | 6,9 s | 0,7 s |
Gấp tám lần ở lần build lại — và lần build lại mới là thứ bạn làm hàng chục lần mỗi ngày.
Lý do nằm đúng ở luật trên. Ở bản sai, COPY . /app đứng trước pip install. Sửa bất kỳ tệp mã nào cũng làm layer COPY đổi, và mọi layer sau nó — kể cả bước cài phụ thuộc tốn thời gian nhất — mất cache theo.
Ở bản đúng, chỉ requirements.txt mới làm mất cache của pip install. Mà tệp đó cả tháng mới đổi một lần.
Nguyên tắc: sắp xếp Dockerfile theo tần suất thay đổi, thứ ít đổi lên trên. Nền hệ điều hành gần như không đổi, phụ thuộc đổi vài tuần một lần, mã nguồn đổi vài phút một lần — nên chúng phải nằm theo đúng thứ tự đó.
Mẫu này áp cho mọi hệ sinh thái: package.json trước npm ci, pom.xml trước mvn dependency:go-offline, go.mod trước go mod download, Cargo.toml trước cargo fetch.
Cache tính theo nội dung, không theo thời gian
Câu hỏi hay gặp: script build của tôi có touch vài tệp, vậy có phá cache không?
build lai khong doi gi : 0,75 s | 4 buoc CACHED
sau khi touch src/main.py : 0,72 s | 4 buoc CACHED
sau khi THEM noi dung : 0,67 s | 3 buoc CACHED
sau khi sua requirements.txt : 6,47 s | 1 buoc CACHED
touch không phá cache. Docker băm nội dung tệp, không nhìn thời gian sửa đổi. Chỉ khi nội dung thật sự đổi thì layer mới bị dựng lại.
Hàng cuối cho thấy cái giá thật: đổi requirements.txt là chỉ còn 1 bước được dùng lại, và thời gian build nhảy từ 0,7 lên 6,5 giây.
Mất cache ở dòng nào thì mất từ đó trở đi
Năm lệnh RUN, mỗi lệnh mất một giây. Sửa từng dòng một rồi đo:
| Sửa dòng | Bước còn cache | Thời gian |
|---|---|---|
| không sửa gì | 5 | 0,31 s |
| dòng 5 (cuối) | 4 | 1,39 s |
| dòng 3 (giữa) | 2 | 3,66 s |
| dòng 1 (đầu) | 1 | 6,09 s |
Tuyến tính và không có ngoại lệ: cache mất từ chỗ bạn sửa cho tới hết Dockerfile. Đó là lý do một dòng vô hại đặt nhầm chỗ ở đầu tệp có thể làm cả đường ống CI chậm đi.
Hệ quả thực tế ít người để ý: ARG khai ở đầu Dockerfile và được truyền giá trị khác nhau mỗi lần build — ví dụ ARG BUILD_DATE hay ARG GIT_SHA — sẽ phá cache của toàn bộ phần còn lại. Nếu bạn cần chúng, hãy khai càng gần cuối càng tốt.
Cái bẫy apt-get update tách dòng
Đây là chỗ phép đo cho ra kết luận ngược với trực giác.
# tach dong
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
# gop dong
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Thêm một gói vào danh sách cài rồi build lại:
| Thời gian | Dung lượng image | |
|---|---|---|
| Tách dòng | 5,4 s | 46,6 MB |
| Gộp dòng | 10,2 s | 32,1 MB |
Bản tách dòng nhanh gấp đôi. Và đó chính là lỗi. Nhìn vào log build:
#5 [2/3] RUN apt-get update
#5 CACHED
#6 [3/3] RUN apt-get install -y --no-install-recommends curl ca-certificates gnupg
apt-get update được dùng lại từ cache, nên apt-get install chạy với chỉ mục gói cũ — cũ bằng đúng tuổi của layer đó, có thể là hàng tháng. Kết quả: bạn cài một phiên bản đã bị thay thế, hoặc gặp lỗi "404 Not Found" khi kho gói đã xoá tệp cũ đi. Bản build nhanh hơn nhanh chính vì nó bỏ qua việc làm mới thông tin.
Và nó còn nặng hơn 14,5 MB (45%), vì /var/lib/apt/lists/ nằm lại trong layer của apt-get update. Xoá nó ở một RUN sau cũng vô ích — phần 2 đã đo: layer chỉ cộng thêm chứ không trừ đi, phải xoá trong cùng lệnh đã tạo ra nó.
Nên với trình quản lý gói, luật là: update, install và dọn dẹp phải nằm trong một RUN duy nhất. Đây là ngoại lệ có chủ đích của lời khuyên "chia nhỏ để tận dụng cache".
Bốn thứ hay phá cache mà không ai ngờ
COPY . /appđặt sớm. Mọi tệp trong thư mục dự án đều tính, kể cảREADME, kể cả thư mục.gitnếu bạn không loại nó ra — chủ đề của phần sau.ARGđổi giá trị mỗi lần build. Đặt cuối, hoặc chấp nhận mất cache.- Image nền dùng tag di động.
FROM python:3.12-alpinehôm nay có thể là digest khác hôm qua, và khi nó đổi thì cả Dockerfile mất cache. Ghim digest cho ổn định — xem phần 4. - Đổi máy build. Cache nằm trên máy; máy CI mới là cache rỗng. Đó là lý do có cache registry, và là chủ đề của phần 19.
Thử ba mươi giây
Đo xem Dockerfile của bạn mất bao nhiêu khi sửa một dòng mã:
docker build -t thu . >/dev/null # lam nong cache
echo "// $(date)" >> src/tep-nao-do.js # sua mot dong
time docker build -t thu .
Rồi đếm số bước CACHED trong log:
docker build --progress=plain -t thu . 2>&1 | grep -c CACHED
Nếu con số đó nhỏ hơn nhiều so với tổng số lệnh trong Dockerfile, bạn đang có một COPY đặt quá sớm — và chuyển nó xuống dưới bước cài phụ thuộc là thay đổi rẻ nhất bạn làm được cho tốc độ CI hôm nay.
Phần sau đo build context và .dockerignore: bao nhiêu dữ liệu thật sự được gửi cho daemon mỗi lần bạn gõ docker build.