Hình dung một cái báo khói. Nó cảm nhận có cháy và kêu inh ỏi — nhưng nó không dập lửa; muốn có tác dụng, nó phải được nối vào một vòi phun, một trạm giám sát, hay ít nhất một người đang ở nhà nghe thấy. Treo một cái báo khói trong ngôi nhà trống thì nó cứ kêu, chẳng ai tới. HEALTHCHECK đúng là cái báo khói đó: nó 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
timeoutphải nhỏ hơninterval. 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átimeoutlà thất bại, nêntimeoutcũng là ngưỡng "chậm bằng hỏng".start-periodphả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×retrieslà độ 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-khoecủ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
curlnếu image không cócurl. Lỗi sẽ làexit 127và trông y hệt "dịch vụ hỏng" — kiểm bằngdocker inspect ... .State.Health.Logđể thấy thông báo thật.
Muốn biết container nào trên máy mình có healthcheck (và cái nào đang hỏng mà chưa ai hay), rồi tính độ trễ phát hiện thật của một cái, soi hai chỗ:
docker ps --format '{{.Names}}\t{{.Status}}' | grep -i health
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). Ra 90 giây nghĩa là 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.
Mẫu số chung
Bài học đầu tiên, chính là cái báo khói: một phép kiểm sức khoẻ là một tín hiệu, không phải một hành động — biết có chuyện là vô nghĩa nếu không có gì được nối vào để phản ứng, và với docker run thuần thì chẳng có ai. unhealthy chỉ đổi một chuỗi trong metadata; phải có Compose, Swarm, load balancer hay một cron đọc nó thì mới thành hành động. Cùng cái bẫy "cảm nhận mà không ai xử lý" ở khắp nơi: cảnh báo giám sát mà không ai bị đánh thức, một log.error rồi chạy tiếp, một kết quả kiểm tra không ai đọc, một cảnh báo lint không cổng CI nào chặn. Nguyên tắc: tách phát hiện khỏi phản ứng, và luôn hỏi "ai hành động dựa trên tín hiệu này, và cái người-hành-động đó có tồn tại ở đây không" — đo đạc mà thiếu người xử lý chỉ là trưng bày.
Điều thứ hai, đọc từ công thức interval × retries và bảng CPU: tần suất của một cái giám sát là một đánh đổi ba chiều — độ trễ phát hiện, báo động giả, và chi phí — chứ không phải "càng nhanh càng tốt". Kiểm mỗi giây phát hiện nhanh nhưng dễ báo nhầm vì một gói tin mất, và ngốn 73% một nhân khi có 100 container; kiểm mỗi 30 giây rẻ nhưng để dịch vụ hỏng 90 giây mới lộ. Và bản thân việc quan sát cũng tốn — mỗi lần kiểm là một lần fork một tiến trình mới, thứ tiêu tài nguyên của chính cái nó theo dõi. Cùng đánh đổi ấy ở tần suất polling, nhịp heartbeat, tốc độ lấy mẫu, độ trễ retry, debounce. Nguyên tắc: chọn tần suất giám sát có chủ đích theo mức trễ mình chịu được cân với giá phải trả — và nhớ rằng đo đạc không miễn phí, cái hành động nhìn ngó tự nó đã ăn vào thứ đang được nhìn.
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.