HEALTHCHECK cho Docker biết container có đang phục vụ được không. Câu hỏi ít ai hỏi là: biết rồi thì Docker làm gì?

Câu trả lời ngắn: không gì cả. Bài này đo cả vòng đời để thấy rõ.

FROM nginx:alpine
RUN echo ok > /usr/share/nginx/html/suc-khoe
HEALTHCHECK --interval=5s --timeout=2s --start-period=2s --retries=3 \
  CMD wget -qO- http://localhost/suc-khoe || exit 1

Vòng đời trạng thái

0,0 s: starting
5,0 s: healthy

Container bắt đầu ở starting, và chuyển sang healthy sau đúng một chu kỳ. Trong suốt thời gian start-period, lần kiểm thất bại không tính vào retries — đó là khoảng ân hạn cho ứng dụng khởi động.

Giờ làm hỏng dịch vụ bằng cách xoá tệp mà healthcheck đang hỏi:

 0,0 s: da xoa tep suc-khoe
15,1 s: unhealthy

15,1 giây với --interval=5s --retries=3. Công thức rõ ràng:

độ trễ phát hiện ≈ interval × retries

Cộng thêm tối đa một chu kỳ nữa nếu lần hỏng xảy ra ngay sau một lần kiểm vừa thành công. Với mặc định của Docker (interval=30s, retries=3) thì con số là 90 giây — một phút rưỡi dịch vụ hỏng mà chưa ai biết.

Lịch sử kiểm tra lưu lại từng lần:

$ docker inspect h1 --format '{{json .State.Health.Log}}'
ma thoat 0
ma thoat 1
ma thoat 1
ma thoat 1

Phần quan trọng nhất: unhealthy không làm gì cả

trang thai suc khoe : unhealthy
container dang chay : true
so lan khoi dong lai: 0
van phuc vu duoc?   : co
sau 12 giay nua     : dang chay=true | khoi dong lai=0

Container vẫn chạy, vẫn nhận lưu lượng, và Docker chỉ đổi một chuỗi trạng thái trong metadata.

Tôi thử thêm --restart=on-failure, vì cái tên nghe như đúng thứ cần:

suc khoe=unhealthy | dang chay=true | khoi dong lai=0

Vẫn không. --restart phản ứng với tiến trình thoát, không phản ứng với healthcheck. Một container unhealthy mà tiến trình chính vẫn sống thì không bao giờ được khởi động lại.

Đây là điểm hay bị hiểu sai nhất về HEALTHCHECK. Nó không phải cơ chế tự chữa. Nó là một tín hiệu cho thứ khác đọc:

Ai đọc Làm gì
docker ps hiện (unhealthy) cạnh trạng thái
Docker Compose depends_on: condition: service_healthy đợi trước khi khởi động dịch vụ phụ thuộc
Docker Swarm thay container trong service
Bộ cân bằng tải / hệ điều phối bên ngoài rút container khỏi danh sách phục vụ
Docker Engine một mình không gì cả

Nếu bạn chạy docker run thuần trên một máy chủ và trông chờ healthcheck tự cứu dịch vụ, nó sẽ không cứu. Bạn cần một thứ khác đọc tín hiệu đó — và cách rẻ nhất là một script cron kiểm docker inspect ... .State.Health.Status rồi tự docker restart.

Chi phí của chính healthcheck

Mỗi lần kiểm là một tiến trình mới sinh ra bên trong container. Tôi đo CPU của container nginx rảnh rỗi với các tần suất khác nhau:

CPU trung bình
Không có healthcheck 0,00%
wget mỗi 30 giây 0,00%
wget mỗi 5 giây 0,10%
wget mỗi 1 giây 0,73%
Phép kiểm nặng mỗi 1 giây 0,65%

Hai điều đọc được:

Chi phí nằm ở việc sinh tiến trình, không ở phép kiểm. Hàng cuối — phép kiểm có thêm một vòng lặp tính toán — không đắt hơn wget thuần. Cái tốn là fork, exec, và dựng môi trường cho tiến trình mới, khoảng 7 mili giây CPU mỗi lần.

Con số nhỏ nhưng nhân lên thì không. 0,73% cho một container ở tần suất 1 giây; một máy chủ chạy 100 container cùng cấu hình đó tiêu 73% một nhân CPU chỉ để hỏi thăm sức khoẻ. Máy tôi lúc viết bài này đang chạy 35 container.

Nên --interval=1s là một lựa chọn cần có lý do, không phải mặc định "cho nhanh".

Chọn tham số thế nào

Bốn tham số, và chúng ràng buộc nhau:

HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \
  CMD wget -qO- http://localhost:8080/suc-khoe || exit 1
  • timeout phải nhỏ hơn interval. Nếu không, một phép kiểm treo sẽ chồng lên phép kiểm tiếp theo. Docker coi quá timeout là thất bại, nên timeout cũng là ngưỡng "chậm bằng hỏng".
  • start-period phải đủ cho lần khởi động chậm nhất. Ứng dụng Java nạp Spring context, ứng dụng chờ migration CSDL — đặt ngắn quá thì container bị đánh dấu hỏng ngay lúc còn đang lên.
  • interval × retries là độ trễ phát hiện. Chọn theo mức bạn chịu được, rồi cân với chi phí CPU ở bảng trên.
  • retries > 1 để không báo động vì một lần trục trặc. Đặt bằng 1 thì một gói tin mất là đủ làm container bị coi là hỏng.

Phép kiểm nên hỏi gì

Đây là chỗ nối với những gì tôi đã đo ở sê-ri Vert.x, phần 27: phân biệt liveness với readiness.

Healthcheck của Docker gần với readiness hơn — nó trả lời "có nên gửi khách vào đây không". Nhưng vì Docker không tự khởi động lại, nó không có hậu quả kiểu liveness, nên bạn được phép kiểm cả phụ thuộc bên ngoài mà không sợ vòng lặp khởi động lại.

Ba luật vẫn giữ nguyên:

  • Đừng để phép kiểm chặn. Ở phần 27 tôi đo được một health check tốn CPU trên event loop kéo thông lượng từ 102 118 xuống 156 req/s.
  • Phép kiểm phải nhẹ hơn một request thật. Nếu /suc-khoe của bạn truy vấn năm bảng thì mỗi chu kỳ là năm truy vấn, nhân với số container.
  • Đừng dùng curl nếu image không có curl. Lỗi sẽ là exit 127 và trông y hệt "dịch vụ hỏng" — kiểm bằng docker inspect ... .State.Health.Log để thấy thông báo thật.

Thử ba mươi giây

Xem container nào trên máy bạn có healthcheck, và cái nào đang hỏng mà không ai biết:

docker ps --format '{{.Names}}\t{{.Status}}' | grep -i health

Và xem độ trễ phát hiện thật của một container:

docker inspect <ten> --format '{{json .Config.Healthcheck}}' | python3 -m json.tool

Nhân Interval với Retries rồi chia cho một tỉ (Docker lưu bằng nano giây). Nếu ra 90 giây thì bạn đang ở mặc định — và một container hỏng sẽ nhận lưu lượng suốt một phút rưỡi trước khi có ai nhận ra.

Phần sau bàn về nhãn OCI và metadata: ghi nguồn gốc vào image để truy vết ngược về đúng commit.