Mọi container đều bị dừng, thường xuyên hơn nhiều so với bị lỗi. Bài này đo sáu cách đóng gói cùng một ứng dụng và xem cái nào thực sự dừng êm.

Sáu cách đóng gói, sáu kết quả khi dừng

Bảng

Ứng dụng là một script có trap TERM dọn dẹp 1 giây rồi thoát. docker stop -t 10.

Cách chạy Dừng sau Mã thoát Dọn dẹp có chạy?
PID 1 = ứng dụng, không bắt SIGTERM 10.160 ms 137 không
PID 1 = ứng dụng, bắt SIGTERM 1.150 ms 0
sh -c "/app" — ứng dụng là con 10.150 ms 137 không
sh -c "exec /app" — thêm một từ 1.149 ms 0
--init + sh -c "/app" 113 ms 143 không
--init + /app trực tiếp 1.152 ms 0

Dòng nhanh nhất là dòng tệ nhất

113 mili giây. Nhanh gấp mười lần cấu hình đúng.

Và bước dọn dẹp không chạy. Nhật ký chỉ có dòng khởi động, không có dòng "nhận SIGTERM".

Trên bảng điều khiển, cấu hình này trông tốt nhất: container dừng gần như tức thì, không có cảnh báo timeout, không có SIGKILL. Nó là cấu hình duy nhất trong bảng mất dữ liệu.

Thời gian dừng ngắn không phải chỉ số của việc dừng êm. Chỉ số đúng là mã thoát 0 cộng với bằng chứng trong nhật ký rằng bước dọn dẹp đã chạy xong.

Hai lỗi độc lập, cùng một triệu chứng

Lỗi thứ nhất: ứng dụng không bắt SIGTERM.

Nhân có một quy tắc đặc biệt cho PID 1: tín hiệu không có trình xử lý thì bị bỏ qua, chứ không giết tiến trình như với mọi PID khác. Quy tắc đó tồn tại để bảo vệ init của hệ thống khỏi bị giết nhầm.

Trong container, ứng dụng của bạn PID 1. Nên SIGTERM không làm gì cả, và mười giây sau, SIGKILL — thứ không thể bỏ qua — kết thúc mọi thứ giữa chừng.

Lỗi thứ hai: ứng dụng bị bọc trong một shell.

ENTRYPOINT /app          # dang shell -> chay thanh: /bin/sh -c "/app"
ENTRYPOINT ["/app"]      # dang JSON  -> chay thang

Dạng shell làm PID 1 trở thành /bin/sh. Shell nhận SIGTERM, và không chuyển tiếp cho tiến trình con. Ứng dụng có trình xử lý hoàn hảo và không bao giờ nhận được tín hiệu để chạy nó.

Thêm đúng một từ:

ENTRYPOINT exec /app

exec thay thế shell bằng ứng dụng, nên ứng dụng trở thành PID 1. Dòng thứ tư trong bảng: 1.149 ms, mã thoát 0.

Hai lỗi này cho cùng một triệu chứng — container mất mười giây để dừng và thoát với mã 137 — nên chẩn đoán sai rất dễ.

--init sửa được gì và không sửa được gì

docker run --init chèn tini làm PID 1. Nó giải quyết đúng hai việc:

  • Thu hồi tiến trình mồ côi. Không có PID 1 tử tế thì tiến trình con chết để lại xác zombie mãi mãi.
  • Chuyển tiếp tín hiệu. Nó gửi SIGTERM xuống dưới.

Dòng thứ sáu cho thấy nó hoạt động: --init + ứng dụng trực tiếp cho 1.152 ms và mã thoát 0.

Dòng thứ năm cho thấy giới hạn: --init + sh -c vẫn hỏng. tini gửi tín hiệu tới nhóm tiến trình, nên cả shell lẫn ứng dụng cùng nhận SIGTERM — và ứng dụng chết vì tín hiệu (mã 143) trước khi kịp chạy trình xử lý.

--init không thay thế được việc đóng gói đúng. Nó bù cho việc thu hồi zombie, không bù cho một tầng shell thừa.

Mã thoát nói gì

Nghĩa
0 Thoát sạch, tự nguyện
137 128 + 9 — bị SIGKILL. Hết thời gian chờ hoặc OOM.
143 128 + 15 — chết vì SIGTERM, không tự thoát
139 128 + 11 — SIGSEGV

143 là mã dễ hiểu nhầm nhất. Nó không nghĩa là dừng êm — nó nghĩa là tiến trình bị tín hiệu giết vì không có trình xử lý, chỉ khác 137 ở chỗ tín hiệu là SIGTERM chứ không phải SIGKILL.

Dừng êm thật sự luôn cho mã 0.

Dừng êm nên làm gì

Thứ tự đúng cho một dịch vụ mạng:

  1. Ngừng nhận yêu cầu mới — đóng cổng lắng nghe, để bộ cân bằng tải thấy dịch vụ đã rời hàng.
  2. Báo kiểm tra sức khoẻ là không sẵn sàng, và chờ đủ lâu để bộ cân bằng tải kịp nhận ra. Với Kubernetes, đó là periodSeconds × failureThreshold.
  3. Xử lý xong các yêu cầu đang dở, có thời hạn.
  4. Đóng kết nối tới cơ sở dữ liệu, xả bộ đệm log (phần 41).
  5. Thoát với mã 0.

Bước 2 hay bị bỏ qua nhất. Nếu bạn đóng cổng lắng nghe ngay lập tức, bộ cân bằng tải vẫn gửi yêu cầu tới trong vài giây nữa và người dùng nhận lỗi kết nối — dù dịch vụ của bạn "dừng êm" hoàn hảo.

Trong Kubernetes:

lifecycle:
  preStop:
    exec:
      command: ["sleep", "5"]      # cho bo can bang tai kip cap nhat
terminationGracePeriodSeconds: 30  # phai LON HON tong thoi gian tren

preStop chạy trước SIGTERM, và thời gian của nó nằm trong thời hạn ân hạn. Đặt preStop 5 giây với ân hạn 30 giây nghĩa là ứng dụng còn 25 giây.

Thử ba mươi giây

Kiểm tra dịch vụ của bạn có dừng êm không:

c=<ten-container>
t0=$(date +%s%N)
docker stop -t 30 $c
t1=$(date +%s%N)
echo "dung sau $(( (t1-t0)/1000000 )) ms"
echo "ma thoat: $(docker inspect $c --format '{{.State.ExitCode}}')"
docker logs --tail 20 $c 2>&1 | tail -5

echo "--- PID 1 la gi ---"
docker inspect $c --format 'Entrypoint: {{.Config.Entrypoint}}  Cmd: {{.Config.Cmd}}'

Ba dấu hiệu cần cả ba:

  • Dừng trước thời hạn ân hạn.
  • Mã thoát 0.
  • Nhật ký có dòng chứng minh bước dọn dẹp đã chạy.

Thiếu dòng thứ ba thì hai dòng đầu không có nghĩa gì — đó chính là dòng 113 mili giây trong bảng.

Phần sau: bẫy khi đo — những sai lầm tôi đã mắc trong bốn mươi bốn phần trước.