Sáu phần trước đưa ra một loạt cách làm build nhanh hơn: sắp xếp COPY, cache mount, cache registry, tách tầng song song. Câu hỏi thực tế là áp cái nào trước — và câu trả lời chỉ có bằng cách đo chính Dockerfile của bạn.

--progress=plain cho thời gian từng bước

docker build --no-cache --progress=plain -t thu . 2>&1
#7 [3/7] RUN apt-get update && apt-get install -y --no-install-recommends curl git
#7 0.393 Hit:1 http://deb.debian.org/debian trixie InRelease
#7 3.701 Fetched 9947 kB in 4s (2796 kB/s)
#7 DONE 53.1s

Hai loại dấu thời gian: số ở đầu mỗi dòng log là giây trôi qua kể từ khi bước bắt đầu, còn DONE 53.1s là tổng thời gian của bước.

Một script xếp hạng

Ba chục dòng đọc log rồi sắp xếp:

#!/usr/bin/env python3
import re, sys

ten, giay = {}, {}
for dong in sys.stdin:
    m = re.match(r'^#(\d+)\s+(\[.*?\]|\[internal\].*?)\s*(.*)$', dong)
    if m and m.group(1) not in ten:
        ten[m.group(1)] = (m.group(2) + " " + m.group(3)).strip()
    d = re.match(r'^#(\d+)\s+DONE\s+([\d.]+)s', dong)
    if d: giay[d.group(1)] = float(d.group(2))
    if re.match(r'^#(\d+)\s+CACHED', dong): giay[re.match(r'^#(\d+)', dong).group(1)] = 0.0

tong = sum(giay.values())
print(f"tong: {tong:.1f} s\n")
for b, s in sorted(giay.items(), key=lambda x: -x[1])[:10]:
    print(f"{s:7.1f}  {s/tong*100:4.1f}%  {ten.get(b, '#'+b)[:60]}")
docker build --no-cache --progress=plain -t thu . 2>&1 | python3 phan-tich.py

Kết quả trên một Dockerfile Python bình thường:

tong thoi gian cac buoc: 92.2 s

   giay      %  buoc
  ------------------------------------------------------------------
   53.1  57.6%  [3/7] RUN apt-get update && apt-get install -y ...
   34.0  36.9%  [5/7] RUN pip install --no-cache-dir -r requirements.txt
    4.2   4.6%  #12  (exporting to image)
    0.3   0.3%  [internal] load build context
    0.3   0.3%  [2/7] WORKDIR /app
    0.2   0.2%  [7/7] RUN python -c "import flask; ..."
    0.1   0.1%  [6/7] COPY . /app

Hai bước chiếm 94,5%. Mọi thứ khác cộng lại là 5,5% — kể cả những dòng mà người ta hay dành thời gian tối ưu.

Bước #12 không có tên trong ngoặc vuông là xuất layer ra image:

#12 exporting to image
#12 exporting layers
#12 exporting layers 3.1s done

Nó tỉ lệ với dung lượng image, nên nếu bước này chiếm phần lớn thì phần 16 là chỗ cần đọc.

Tổng các bước so với thời gian thực tế

build sach : thuc te 108,2 s | tong cac buoc 107,9 s | 0 CACHED
build lai  : thuc te   0,4 s | tong cac buoc   0,1 s | 6 CACHED

Hai con số gần bằng nhau nghĩa là Dockerfile này chạy nối tiếp hoàn toàn — không có tầng nào song song với tầng nào. Nếu tổng các bước lớn hơn nhiều so với thời gian thực tế thì bạn đang có song song, và phần 23 là chỗ để đọc tiếp.

Đây là phép so sánh đáng làm đầu tiên, vì nó cho biết vấn đề của bạn thuộc loại nào: một bước chậm hay nhiều bước không chạy cùng lúc.

Đóng vòng lặp: sửa đúng chỗ rồi đo lại

Bảng xếp hạng chỉ ra apt-getpip. Cả hai đều là ứng viên của cache mount ở phần 18:

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 git

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
Thời gian
Bản gốc, cache rỗng 46,3 s
Có cache mount, cache rỗng 46,4 s
Có cache mount, cache đã ấm 14,4 s

Nhanh hơn 3,2 lần, và con số đó đến thẳng từ bảng xếp hạng chứ không từ việc đoán.

Hàng giữa cũng quan trọng: lần đầu không nhanh hơn, vì cache chưa có gì. Ai thêm cache mount rồi đo đúng một lần sẽ kết luận nó vô dụng — đúng cái bẫy tôi đã dính ở phần 18.

Một cảnh báo về con số tuyệt đối

Bạn sẽ để ý là "build sạch" trong bài này khi thì 92,2 giây, khi 108,2, khi 46,3. Cùng một Dockerfile, cùng một máy.

Khác biệt là mạngapt-getpip tải hàng chục megabyte, và tốc độ đó thay đổi theo từng lúc. Nghĩa là:

  • So sánh trong cùng một lần chạy thì tin được (tỉ lệ phần trăm ở bảng xếp hạng).
  • So sánh giữa hai lần chạy cách nhau thì phải lặp lại vài lần rồi lấy trung vị.
  • Và nếu bước tốn nhất của bạn là tải mạng, thì tối ưu Dockerfile chỉ giúp được một phần — cache mount hoặc gương registry nội bộ mới là câu trả lời.

--progress=rawjson cho công cụ

Muốn phân tích bằng máy thì có định dạng JSON từng dòng:

docker build --progress=rawjson -t thu . 2>&1 | head -1
{"vertexes":[{"digest":"sha256:44282f780b3d...","name":"...","started":"...","completed":"..."}]}

Mỗi dòng là một sự kiện với startedcompleted chính xác tới nano giây. Dạng này hợp để đẩy vào hệ thống giám sát CI và theo dõi thời gian build theo thời gian, thay vì chỉ nhìn một lần.

Quy trình bốn bước

Khi build chậm, làm theo thứ tự này:

  1. Chạy bảng xếp hạng. Biết hai bước tốn nhất trước khi đụng vào bất cứ thứ gì.
  2. So tổng các bước với thời gian thực tế. Gần bằng nhau nghĩa là vấn đề nằm ở một bước chậm; chênh nhiều nghĩa là đang có song song và bạn nên nhìn vào bước chậm nhất của nhánh dài nhất.
  3. Đo lại với cache ấm. Con số ở lần build sạch và lần build lại là hai bài toán khác nhau — CI thường quan tâm cái đầu, lập trình viên quan tâm cái sau.
  4. Sửa một thứ, đo lại. Sửa ba thứ cùng lúc thì không biết cái nào có tác dụng.

Thử ba mươi giây

Không cần script, chỉ cần một lệnh:

docker build --no-cache --progress=plain -t thu . 2>&1 | grep 'DONE' | sort -t' ' -k3 -rh | head -5

Năm dòng in ra là năm bước tốn nhất của bạn. Trong hầu hết Dockerfile tôi từng đo, hai dòng đầu chiếm hơn 90% — và mọi thứ bạn làm với các dòng còn lại đều là công sức bỏ phí.

Phần sau chuyển sang chạy container: giới hạn CPU, bộ nhớ, và chuyện gì xảy ra khi vượt hạn mức.