Ứng dụng trong container hỏng lúc 3 giờ sáng. Chuyện gì xảy ra tiếp theo hoàn toàn phụ thuộc vào một chuỗi ký tự bạn gõ lúc docker run — và ba trong bốn lựa chọn có hành vi mà tài liệu mô tả gọn tới mức dễ hiểu sai.
Tôi cho một ứng dụng chạy 0,2 giây rồi exit 1, chạy đủ bốn chính sách, quan sát 30 giây:
--restart |
RestartCount sau 30 s | Trạng thái cuối |
|---|---|---|
no (mặc định) |
0 | exited |
on-failure:3 |
3 | exited |
always |
9 | restarting |
unless-stopped |
9 | restarting |
always và unless-stopped giống hệt nhau ở đây. Khác biệt của chúng nằm ở chỗ khác, đo ở cuối bài.
Khoảng nghỉ nhân đôi sau mỗi lần
Docker không quay vòng liên tục. Tôi đo mốc thời gian của từng lần khởi động lại:
| Lần | Cách lần trước |
|---|---|
| 1 | 0,18 s |
| 2 | 0,39 s |
| 3 | 0,49 s |
| 4 | 0,67 s |
| 5 | 1,09 s |
| 6 | 1,90 s |
| 7 | 3,47 s |
| 8 | 6,71 s |
| 9 | 13,10 s |
| 10 | 25,89 s |
Từ lần 5 trở đi nó nhân đôi đều đặn, và trần là 60 giây. Sau 54 giây container mới thử được 10 lần; sau 5 phút chỉ thêm khoảng 4 lần nữa.
Hệ quả thực tế: container crash-loop trông như đã chết hẳn. Bạn nhìn docker ps thấy nó restarting, docker logs không có dòng mới, và bạn kết luận nó treo — trong khi thật ra nó đang chờ hết 60 giây để thử lần nữa. Cứ docker logs mà đọc từ đầu, thông báo lỗi thật nằm ở đó, lặp lại y hệt hàng chục lần.
Khoảng nghỉ nhân đôi này là thứ bảo vệ máy chủ: không có nó, một ứng dụng hỏng vì thiếu biến môi trường sẽ quay vòng vài nghìn lần mỗi phút và đốt sạch CPU cho việc dựng namespace.
always khởi động lại cả khi thoát thành công
Điểm khác nhau lớn nhất giữa on-failure và always không phải là số lần:
| RestartCount sau 20 s | Trạng thái | |
|---|---|---|
on-failure:3 |
0 | exited (mã 0) |
always |
8 | restarting |
Ứng dụng thoát với mã 0 — chạy xong việc, không lỗi gì cả. on-failure để yên, đúng như tên gọi. always kéo dậy 8 lần trong 20 giây.
Nghĩa là đừng dùng always cho tác vụ có kết thúc: một job xử lý dữ liệu, một lệnh migrate CSDL, một script backup. Chúng chạy xong rồi thoát 0, và always biến chúng thành vòng lặp vô hạn. Với job, dùng no hoặc on-failure.
Docker cũng chặn một tổ hợp vô nghĩa:
$ docker run --rm --restart on-failure:5 ...
docker: conflicting options: cannot specify both --restart and --rm
Hợp lý — --rm xoá container ngay khi thoát, thì không còn gì để khởi động lại.
Ba chuyện restart policy KHÔNG làm
1. Không phản ứng với unhealthy
Đây là hiểu nhầm phổ biến nhất. Tôi chạy một container treo vĩnh viễn với healthcheck luôn hỏng, kèm --restart always, chờ 25 giây:
suc khoe = unhealthy
trang thai = running
RestartCount = 0
so lan healthcheck da chay = 5
Healthcheck chạy đủ 5 lần, tuyên bố unhealthy, và không có gì xảy ra cả. Container vẫn running, restart policy vẫn im.
Restart policy chỉ nhìn tiến trình PID 1 có thoát hay không. Healthcheck chỉ ghi một nhãn. Như phần 14 đã nói, cần một bộ điều phối (Swarm, Kubernetes, hoặc depends_on: condition: service_healthy trong Compose) mới có ai đó đọc nhãn đó và hành động.
Ứng dụng treo mà không chết là kịch bản xấu nhất cho Docker đơn lẻ: cổng vẫn mở, tiến trình vẫn sống, restart policy vẫn hài lòng, và mọi request đều treo.
2. Không reset hạn mức của on-failure:N
on-failure:2 với ứng dụng sống 15 giây rồi mới hỏng:
| Thời điểm | RestartCount | Trạng thái |
|---|---|---|
| ~20 s | 1 | running |
| ~40 s | 2 | running |
| ~60 s | 2 | exited |
| ~100 s | 2 | exited |
Hai lần khởi động lại là hạn mức của cả đời container, không phải "2 lần mỗi khoảng thời gian". Mười lăm giây chạy khoẻ mạnh không mua lại được lượt nào.
Điều đó có nghĩa là on-failure:5 cho một dịch vụ hỏng mỗi tuần một lần sẽ đứng hẳn sau năm tuần — vào một đêm ngẫu nhiên nào đó, chẳng liên quan gì tới sự cố nào cả.
3. docker start cũng không xoá hạn mức đó
Tôi thử tiếp: container đã cạn hạn mức, docker start bằng tay:
truoc: RestartCount=2 exited
sau 'docker start': RestartCount=2 exited
Nó chạy đúng một lần rồi hỏng, và không khởi động lại nữa. Muốn xoá bộ đếm thì phải tạo lại container (docker rm rồi run, hoặc docker compose up --force-recreate).
Vì hai điều trên, với dịch vụ chạy dài tôi dùng unless-stopped chứ không dùng on-failure:N. Cái tên on-failure:5 nghe như "có bảo hiểm", thực tế nó là "hỏng lần thứ sáu thì tắt vĩnh viễn".
always khác unless-stopped ở đúng một chỗ
Cả hai chính sách này đều không kéo container dậy sau khi bạn dừng nó bằng tay:
Sau docker stop, chờ 5 s |
|
|---|---|
always |
exited |
unless-stopped |
exited |
Khác biệt chỉ lộ ra khi daemon khởi động lại — máy chủ reboot, hoặc bạn cập nhật Docker. Tôi đo bằng một daemon Docker lồng trong container để không đụng tới máy thật: dừng tay cả hai, rồi khởi động lại daemon.
=== SAU KHI KHOI DONG LAI DAEMON ===
c-unless: Exited (137) 28 seconds ago
c-always: Up 5 seconds
always quên mất là bạn đã cố tình dừng nó và kéo dậy sau reboot. unless-stopped nhớ.
Đó là lý do unless-stopped gần như luôn là lựa chọn đúng cho dịch vụ chạy dài: bạn dừng một container để bảo trì, máy chủ reboot vì lý do khác, và bạn không muốn nó tự lên lại sau lưng bạn.
Chọn cái nào
| Loại việc | Chính sách |
|---|---|
| Job có kết thúc (migrate, backup, build) | no |
| Job cần thử lại vài lần rồi thôi | on-failure:3 |
| Dịch vụ chạy dài | unless-stopped |
| Dịch vụ hạ tầng phải lên cùng máy, kể cả sau khi dừng tay | always |
Trong Compose:
services:
web:
image: vi-du:1.0
restart: unless-stopped
migrate:
image: vi-du:1.0
command: ["/migrate.sh"]
restart: "no"
Lưu ý restart: "no" phải có ngoặc kép. YAML đọc no trần thành giá trị boolean false, và Compose sẽ báo lỗi kiểu.
Kiểm tra container của bạn
docker inspect <ten> --format \
'chinh sach={{.HostConfig.RestartPolicy.Name}} han muc={{.HostConfig.RestartPolicy.MaximumRetryCount}} da restart={{.RestartCount}}'
RestartCount lớn hơn 0 mà bạn không biết là dấu hiệu ứng dụng đã hỏng và tự dậy — không ai báo cho bạn cả. Ba con số đáng để đưa vào giám sát:
RestartCounttăng → có gì đó đang hỏng lặp lại.- Trạng thái
restartingkéo dài → crash-loop, và khoảng nghỉ đã lên tới hàng chục giây. - Mã thoát 137 kèm
OOMKilled=true→ không phải lỗi ứng dụng, là giới hạn bộ nhớ (phần 25).
Và nhớ rằng khởi động lại chỉ hữu ích khi ứng dụng dừng tử tế — nếu nó bị SIGKILL mỗi lần như phần 27 đo được, mỗi lần restart là một lần mất dữ liệu trong bộ đệm.
Phần sau bắt đầu nhóm bài về mạng: container nói chuyện với nhau bằng cách nào, và vì sao localhost trong container không phải localhost của bạn.