Sáu mươi phần, mỗi phần một phép đo. Bài này gom chúng thành thứ dùng được: một script kiểm tra chạy trên hệ thống đang chạy của bạn.

Chín phép kiểm và con số đã đo ra chúng

Chín phép kiểm, mỗi phép một con số đứng sau

Kiểm gì Vì sao — con số đã đo
Ghim digest, không latest Cùng một nhãn, hai nội dung: kéo bằng nhãn ra bản BỊ THAY ĐỔI (p.4, p.59)
restart: unless-stopped Sau reboot, 2 trong 5 chính sách không tự lên — kể cả on-failure (p.57)
Bind cổng vào 127.0.0.1 Luật DROP giống hệt nhau: cổng thường bị chặn, cổng Docker trả 200 (p.35)
Giới hạn nhật ký Tệp log 97,2 MB trong khi docker ps -s báo 4,1 kB (p.29)
Trần bộ nhớ OOMKilled=true nhưng mã thoát vẫn là 0 (p.25)
Người dùng không phải root Container root ghi ra tệp root:root mà chủ thư mục không xoá nổi (p.43)
Có healthcheck Thiếu nó, depends_on cho ứng dụng chạy sớm 5 giây và trượt 97 lần (p.50)
Volume phải có tên volume prune mặc định bỏ sót 77% dung lượng thu hồi được (p.45)
Đang chạy và đang khoẻ phép kiểm tôi quên — xem mục dưới

Script, và kết quả chạy thật

Tôi viết kiem.sh đọc docker inspect của mọi container thuộc một project, rồi thử trên hai hệ thống cùng chức năng:

=== he thong viet theo thoi quen ===
  [X] nhan latest hoac khong nhan     nginx:latest
  [X] restart khong song sot reboot   no
  [X] publish ra moi giao dien        0.0.0.0 ::
  [X] nhat ky khong gioi han          json-file 0
  [X] khong co tran bo nho            0
  [X] chay bang root                  khong khai
  [X] khong co healthcheck            -
  [X] co volume an danh               38753ec9b269c06bc18a2ef3...

  TONG SO VAN DE: 13
=== cung he thong, sau khi va ===
  [v] ghim digest                     nginx@sha256:65645c7bb6a0661892a8b03b...
  [v] restart                         unless-stopped
  [v] publish gioi han                127.0.0.1
  [v] nhat ky co gioi han             json-file 2
  [v] tran bo nho                     256 MB
  [v] chay bang nguoi dung thuong     101:101
  [v] co healthcheck                  -
  [v] khong co volume an danh         -
  [v] dang chay on dinh               running / healthy

  TONG SO VAN DE: 0

Script đầy đủ nằm cuối bài.

Phép kiểm thứ chín, và vì sao nó có mặt

Bản đầu tiên của script chỉ có tám phép. Nó chấm hệ thống đã vá là 0 vấn đề — trong khi docker compose ps ghi:

web   Restarting (1) Less than a second ago

Dịch vụ đang crash-loop, và danh sách kiểm của tôi cấp cho nó giấy thông hành sạch sẽ.

Nguyên nhân: tám phép đầu chỉ đọc cấu hình. Cấu hình hoàn hảo, hành vi hỏng. nginx của tôi chạy bằng uid 101 với read_only: true, mà tmpfs được tạo thuộc về root nên nó không ghi được vào /var/cache/nginx — đúng cái bẫy phần 30 đã đo, chỉ khác là lần này tôi tự dẫm vào khi kết hợp hai khuyến nghị của chính mình.

Cách sửa nằm trong tuỳ chọn của tmpfs:

tmpfs:
  - /tmp:uid=101,gid=101
  - /var/cache/nginx:uid=101,gid=101
  - /var/run:uid=101,gid=101

Và phép kiểm thứ chín được thêm vào:

ST=$(docker inspect "$C" --format '{{.State.Status}}')
HS=$(docker inspect "$C" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}')
RC=$(docker inspect "$C" --format '{{.RestartCount}}')

RestartCount > 0 là dấu hiệu quan trọng nhất trong ba cái: container đang chạy, trạng thái đẹp, nhưng nó đã chết và dậy lại vài lần mà không ai báo cho bạn.

Bài học lớn nhất của cả sê-ri, gói trong một câu: cấu hình đúng không chứng minh hệ thống chạy được. Mọi danh sách kiểm tĩnh đều phải kết thúc bằng một phép đo động.

Sáu kết quả đi ngược trực giác nhất

Xếp theo mức độ tôi bất ngờ khi đo ra:

Tường lửa không chặn được cổng Docker. Hai luật iptables DROP giống hệt nhau, cổng của tiến trình thường bị chặn, cổng container publish trả về 200. Gói tin bị DNAT ở PREROUTING nên đi qua FORWARD, không bao giờ chạm INPUT — nơi ufw viết luật. (p.35)

--init làm mất nhiều dữ liệu hơn. Không có nó mất 382 bản ghi, có nó mất 433 — vì container chết nhanh hơn nên bộ đệm còn ít cơ hội tràn xuống đĩa. Chỉ ứng dụng tự xử lý SIGTERM mới về 0. (p.27)

Bind mount trên macOS không chậm vì thông lượng. Ghi tuần tự 993,7 so với 966,6 MB/s — chênh 2,7%. Nhưng stat 3.000 tệp là 3 ms so với 262 ms, gấp 87 lần. Và :delegated với :cached không làm gì cả trên Docker Desktop hiện tại. (p.42)

Docker rootless không chậm hơn ở bất kỳ phép đo nào — nhưng nó lặng lẽ vứt bỏ --memory: memory.max đọc ra max, docker run không in cảnh báo nào. (p.32)

Bỏ sạch 14 quyền mà ping vẫn chạy và cổng 80 vẫn bind được, vì ping_group_rangeip_unprivileged_port_start=0. Lời khuyên "thêm --cap-add=NET_RAW để ping được" đã cũ. (p.31)

Một image alpine từ 2022 sạch hơn image Debian đầy đủ mới nhất: 0 CRITICAL so với 56. Bề mặt quan trọng hơn tuổi. (p.59)

Sáu lần tôi tự đo sai, và cách phát hiện

Sê-ri này có một quy tắc: đo sai thì viết lại cả cách phát hiện ra. Chúng gom thành mấy kiểu lặp đi lặp lại:

Kiểu Ví dụ Cái làm lộ ra
Đo tốc độ của thất bại build "0,2 giây" — thực ra là lỗi từ chối quyền kiểm mã thoát, đòi đầu ra bắt đầu bằng sha256:
Chưa hề tương tranh 4 container ghi cùng tệp đều hoàn hảo — chúng chạy lần lượt thêm hàng rào đồng bộ, tăng khối lượng
Ca thử không thể thất bại đo rebuild bằng gói vốn đã cài sẵn kiểm "chưa có" trước khi đo
Công cụ đo im lặng không hỗ trợ ping -M do, date +%N của busybox ca đối chứng chắc chắn phải thành công
Nuốt mất thông báo lỗi >/dev/null 2>&1 giấu "invalid project name" bỏ phần nuốt lỗi ra khi kết quả lạ
Phép đo bị nhiễm container cũ vẫn mang bí danh api chạy lại từ trạng thái sạch, dùng IP tĩnh

Cái chung của cả sáu: một kết quả quá đẹp hoặc quá tròn thì đáng ngờ hơn một kết quả xấu. Con số 16,1 µs "nhanh gấp 3,3 lần" ở phần 36 biến mất hoàn toàn khi chạy lại ba lần.

Đi tiếp từ đây

Docker dừng lại ở một máy. Ba thứ sê-ri này cố ý không chạm tới, và đó là bước tiếp theo:

  • Nhiều máy. Compose không có cập nhật cuốn chiếu — đo được 0,59 giây gián đoạn mỗi lần up -d đổi cấu hình, và con số đó bằng đúng thời gian khởi động ứng dụng của bạn. Cần Swarm hoặc Kubernetes.
  • Giám sát. Sê-ri đo bằng tay; sản xuất cần Prometheus, cần cảnh báo trên RestartCount, trên dung lượng nhật ký, trên du -sm /var/lib/docker.
  • Quản lý bí mật. --mount=type=secret giải quyết lúc build (p.21), không giải quyết lúc chạy. Đó là việc của Vault hay secret của nền tảng.

Script đầy đủ

#!/usr/bin/env bash
# kiem.sh <ten-project>
P="${1:?can ten project}"; LOI=0
bao() { printf '  [%s] %-32s %s\n' "$1" "$2" "$3"; [ "$1" = "X" ] && LOI=$((LOI+1)); return 0; }

for C in $(docker ps -aq --filter "label=com.docker.compose.project=$P"); do
  echo "--- $(docker inspect "$C" --format '{{.Name}}' | tr -d /)"

  IMG=$(docker inspect "$C" --format '{{.Config.Image}}')
  if [[ "$IMG" == *@sha256:* ]]; then bao v "ghim digest" "${IMG:0:52}..."
  elif [[ "$IMG" == *:latest ]] || [[ "$IMG" != *:* ]]; then bao X "nhan latest hoac khong nhan" "$IMG"
  else bao v "co ghim nhan" "$IMG"; fi

  R=$(docker inspect "$C" --format '{{.HostConfig.RestartPolicy.Name}}')
  case "$R" in always|unless-stopped) bao v "restart" "$R";;
                *) bao X "restart khong song sot reboot" "${R:-khong khai}";; esac

  DC=$(docker inspect "$C" --format '{{range $p,$b := .HostConfig.PortBindings}}{{range $b}}{{.HostIp}} {{end}}{{end}}')
  if [ -z "${DC// /}" ]; then bao v "khong publish cong nao" "-"
  elif echo "$DC" | grep -q '0\.0\.0\.0'; then bao X "publish ra moi giao dien" "$(echo $DC)"
  else bao v "publish gioi han" "$(echo $DC)"; fi

  LG=$(docker inspect "$C" --format '{{.HostConfig.LogConfig.Type}} {{len .HostConfig.LogConfig.Config}}')
  [ "${LG##* }" = "0" ] && bao X "nhat ky khong gioi han" "$LG" || bao v "nhat ky co gioi han" "$LG"

  M=$(docker inspect "$C" --format '{{.HostConfig.Memory}}')
  [ "$M" = "0" ] && bao X "khong co tran bo nho" "0" || bao v "tran bo nho" "$((M/1024/1024)) MB"

  U=$(docker inspect "$C" --format '{{.Config.User}}')
  { [ -z "$U" ] || [ "$U" = "0" ] || [ "$U" = "root" ]; } \
    && bao X "chay bang root" "${U:-khong khai}" || bao v "chay bang nguoi dung thuong" "$U"

  H=$(docker inspect "$C" --format '{{if .Config.Healthcheck}}co{{else}}khong{{end}}')
  [ "$H" = "co" ] && bao v "co healthcheck" "-" || bao X "khong co healthcheck" "-"

  AD=$(docker inspect "$C" --format '{{range .Mounts}}{{if and (eq .Type "volume") (gt (len .Name) 40)}}{{.Name}} {{end}}{{end}}')
  [ -n "${AD// /}" ] && bao X "co volume an danh" "$(echo ${AD:0:24})..." || bao v "khong co volume an danh" "-"

  ST=$(docker inspect "$C" --format '{{.State.Status}}')
  HS=$(docker inspect "$C" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}')
  RC=$(docker inspect "$C" --format '{{.RestartCount}}')
  if   [ "$ST" != "running" ];  then bao X "khong chay" "$ST"
  elif [ "$HS" = "unhealthy" ]; then bao X "dang unhealthy" "$HS"
  elif [ "$RC" -gt 0 ];         then bao X "da khoi dong lai $RC lan" "co gi do dang hong"
  else bao v "dang chay on dinh" "$ST / $HS"; fi
done
echo; echo "  TONG SO VAN DE: $LOI"

Thử ba mươi giây

curl -o kiem.sh <dan-doan-ma-tren-vao-mot-tep>
chmod +x kiem.sh
./kiem.sh $(docker compose ps --format '{{.Label "com.docker.compose.project"}}' | head -1)

Con số cuối cùng là số việc phải làm. Trong phép đo của tôi, một tệp Compose viết theo thói quen cho 13; sau khi vá đúng chín điểm trên thì còn 0, và cả hai dịch vụ đều running / healthy.

Cảm ơn bạn đã đọc tới phần 60. Mọi con số trong sê-ri này đều chạy thật, và những chỗ tôi đo sai đều nằm nguyên trong bài — vì cách phát hiện ra sai lầm thường hữu ích hơn chính kết quả đúng.