Bài 1
Lệnh nào làm vỡ cache khi build
Docker dùng lại kết quả của một bước khi đầu vào của bước đó không đổi. Câu đó nghe đơn giản, nhưng "đầu vào" gồm những gì thì phải đo mới biết: nội dung file hay cả thời gian sửa, cả thư mục hay chỉ file được chép, và một dòng ghi chú có tính không. Bài này đo mười bốn trường hợp, mỗi lần chỉ đổi một thứ.
Chín kịch bản trên một Dockerfile
FROM alpine:3.22
RUN echo "buoc-1" > /b1
COPY deps.txt /deps.txt
RUN echo "buoc-2" > /b2
COPY app.txt /app.txt
RUN echo "buoc-3" > /b3
Mỗi hàng dưới đây là một lần build đầy đủ sau khi đã hâm cache. Cột kết quả đọc theo thứ tự sáu bước ở trên: C là CACHED, R là chạy lại.
| Thay đổi | FROM · b1 · deps · b2 · app · b3 | Số bước chạy lại |
|---|---|---|
| không đổi gì | CCCCCC | 0 |
đổi nội dung app.txt | CCCCRR | 2 |
đổi nội dung deps.txt | CCRRRR | 4 |
touch app.txt, nội dung giữ nguyên | CCCCCC | 0 |
| thêm một file không lệnh nào COPY | CCCCCC | 0 |
sửa dòng RUN cuối | CCCCCR | 1 |
sửa dòng RUN đầu | CRRRRR | 5 |
| thêm một dòng ghi chú vào Dockerfile | CCCCCC | 0 |
thêm .dockerignore | CCCCCC | 0 |
COPY là nội dung file, không phải thời gian sửa — hàng
touch vẫn CACHED. COPY app.txt chỉ nhìn đúng file đó, nên thêm file khác
vào thư mục không ảnh hưởng gì. Và dòng ghi chú không nằm trong khoá cache.
deps.txt và app.txt: cùng một thao tác "đổi nội dung một
file", nhưng file được chép sớm hơn làm bốn bước chạy lại còn file chép muộn chỉ làm hai. Bước
RUN echo "buoc-2" không đọc deps.txt, không liên quan gì tới nó, vẫn phải
chạy lại — vì cache của Docker là một chuỗi, mất một mắt thì mất hết phần sau.
Một chú thích về đọc log: dòng [1/6] FROM … luôn hiện ra kể cả khi không có gì đổi.
Đó là BuildKit phân giải lại tham chiếu ảnh nền, không phải build lại layer. Bảng trên đã tính
FROM là CACHED ở mọi hàng trừ hàng sửa chính dòng đó.
COPY một file khác COPY cả thư mục
Dockerfile thứ hai, ngắn hơn, để tách riêng hành vi của COPY .:
FROM alpine:3.22
COPY . /src
RUN echo "buoc-1" > /b1
| Thay đổi | FROM · COPY . · RUN | Nghĩa là |
|---|---|---|
| không đổi gì | CCC | — |
| thêm một file lạ vào thư mục | CRR | COPY . nhìn cả thư mục, nên tập file chép đã đổi |
chỉ touch file lạ đó | CCC | vẫn là nội dung, không phải thời gian |
thêm .dockerignore bỏ file lạ | CRR | tập file chép đổi lần nữa, nên vỡ một lần |
| đổi file lạ sau khi đã bị bỏ | CCC | file không còn nằm trong tập được chép |
Hai hàng cuối là lý do nên thêm .dockerignore sớm: bản thân việc thêm nó làm vỡ cache
một lần, nhưng sau đó mọi thay đổi trong node_modules, .git,
dist đều không còn đụng tới build nữa.
BuildKit chỉ gửi file nó cần
Một niềm tin phổ biến từ thời builder cũ: không có .dockerignore thì Docker nén cả
thư mục rồi gửi sang daemon. Với BuildKit thì không còn đúng. Đo trên một thư mục 15 MB
(node_modules/blob.bin 15 MB cộng app.js vài byte):
| Dockerfile | .dockerignore | transferring context | Kích thước ảnh |
|---|---|---|---|
COPY app.js /app.js | không | 37 B | — |
COPY app.js /app.js | bỏ node_modules | 27 B | — |
COPY . /src | không | 15,73 MB | 19 859 207 B |
COPY . /src | bỏ node_modules | 107 B | 4 125 651 B |
COPY đúng những file cần, thì .dockerignore
gần như không thay đổi lượng dữ liệu gửi đi — 37 byte so với 27 byte. Nó chỉ trở nên quan trọng khi
có COPY ., và ở đó nó quan trọng thật: 15,73 MB xuống 107 byte, và ảnh từ 19,9 MB
xuống 4,1 MB.
Phòng thí nghiệm
Gõ Dockerfile của bạn, khai báo thứ vừa đổi, và xem từng bước. Thuật toán chạy trên trình duyệt đã đối chiếu với 14 kịch bản build thật trên Docker 29.7.2 — khớp toàn bộ.
Dockerfile
Bạn vừa đổi gì
Các kịch bản đã đo:
Từng bước khi build lại
Hệ quả: thứ tự dòng trong Dockerfile
Bảng đầu tiên nói rằng chi phí của một thay đổi bằng số bước đứng sau bước bị vỡ. Vậy thì thứ tự dòng là một quyết định về hiệu năng build, không phải về thẩm mỹ. Nguyên tắc rút ra từ số đo: đặt thứ ít đổi lên trên, thứ hay đổi xuống dưới.
# thu tu hay gap, va ton kem
COPY . /app
RUN npm ci
RUN npm run build
# thu tu re hon: danh sach phu thuoc doi it hon ma nguon
COPY package.json package-lock.json /app/
RUN npm ci
COPY . /app
RUN npm run build
Ở bản trên, sửa một dòng mã bất kỳ làm npm ci chạy lại. Ở bản dưới,
npm ci chỉ chạy lại khi package-lock.json đổi — đúng cơ chế đã đo ở hàng
deps.txt so với app.txt.
Tự kiểm tra
Dockerfile có 10 bước, bạn sửa dòng thứ 3. Bao nhiêu bước chạy lại?
Tám bước: dòng thứ 3 và cả bảy dòng sau nó. Cache của Docker là một chuỗi, nên chi phí của một thay đổi bằng số bước đứng sau nó, không phụ thuộc bước đó có liên quan hay không.Bạn chạy git checkout làm mtime của mọi file đổi, nhưng nội dung phần lớn file giữ nguyên. Cache còn không?
Còn, với đúng những file không đổi nội dung. Khoá cache của COPY là nội dung; hàng "chỉ touch" trong bảng đo cho kết quả CACHED. Bản build cũ trước BuildKit thì nhạy hơn với metadata, nên kinh nghiệm cũ hay nói ngược lại.
Dockerfile chỉ có COPY package.json . và COPY src/ ./src. Thêm .dockerignore có làm build nhanh hơn không?
Gần như không, về lượng dữ liệu gửi đi: đo được 37 byte so với 27 byte cho trường hợp COPY từng file. Nó chỉ có tác dụng lớn khi Dockerfile có COPY ., nơi đo được 15,73 MB xuống 107 byte.
Vì sao thêm .dockerignore lại làm vỡ cache một lần?
Vì tập file mà COPY . chép vừa đổi, nên đầu vào của bước đó đổi theo. Đó là một lần vỡ duy nhất; từ lần build sau, mọi thay đổi trong thư mục đã bị bỏ đều không còn đụng tới cache.