Hãy coi docker stop như lời thông báo hết giờ ở một cửa hàng: "chúng tôi đóng cửa, mời quý khách hoàn tất" (một tín hiệu lịch sự), chờ một khoảng ân hạn, rồi cắt điện bất kể xong hay chưa (một cú dứt khoát). Nếu bạn đinh ninh khoảng ân hạn đó là "mười phút" như lời đồn, mà thật ra chỉ có một giây, thì khách bị đẩy ra giữa lúc đang thanh toán và cái quầy thu ngân chưa kịp chốt sổ. Còn một cửa hàng "đã đóng" thì hàng hoá và sổ sách vẫn nằm nguyên bên trong cho tới khi bị đập bỏ. docker run là một lệnh gộp — nó tạo container rồi khởi động — và giấu đi đúng cái mô hình trạng thái đó, thứ bạn sẽ phải hiểu ngay khi có gì đó không dừng đúng cách.
Sáu trạng thái, đo từng bước
truoc khi tao : (khong ton tai)
sau docker create : created
sau docker start : running
sau khi tien trinh xong : exited | ma thoat = 7
start lai lan nua : running
sau docker rm : (khong ton tai)
Ba điều đáng chú ý:
created là một trạng thái thật. Container đã có lớp ghi, đã có cấu hình mạng, chỉ chưa chạy. Phần 1 đo được docker create mất 74,4 ms và docker start mất 193,1 ms — nên nếu bạn cần khởi động thật nhanh, tạo trước rồi start là một thủ thuật có thật.
exited không phải là đã biến mất. Container giữ nguyên lớp ghi, log, và mã thoát. Máy tôi lúc đo có 61 container ở đủ trạng thái, chiếm 2,53 GB. Đó là lý do docker ps (chỉ hiện cái đang chạy) luôn cho cảm giác dễ chịu hơn docker ps -a.
Container đã thoát khởi động lại được, và nó chạy tiếp trên đúng lớp ghi cũ. Đây là khác biệt lớn giữa docker start và docker run: cái đầu dùng lại container, cái sau dựng cái mới tinh.
docker stop: đừng tin mặc định
Đây là phép đo làm tôi phải quay lại kiểm tra ba lần.
Tôi dựng một container cố tình bỏ qua SIGTERM, rồi đo docker stop:
| Lệnh | Thời gian | Mã thoát |
|---|---|---|
docker stop (không đặt -t) |
1,16 s | 137 |
docker stop -t 2 |
2,15 s | 137 |
docker kill |
0,14 s | 137 |
docker stop trên container có xử lý TERM |
1,08 s | 0 |
Con số đầu tiên sai so với mọi thứ tôi từng đọc: thời gian ân hạn mặc định của docker stop được ghi ở khắp nơi là 10 giây. Đo được 1,16.
Không đoán, tôi hỏi thẳng container:
$ docker inspect kt --format '{{.Config.StopTimeout}}'
1
Rồi hỏi những container do người khác tạo trên cùng máy:
/imageworkflow-web : StopTimeout=1
/imageworkflow-server : StopTimeout=1
/elated_greider : StopTimeout=1
Tất cả đều là 1. Và khi đặt tường minh thì nó tôn trọng đúng:
voi --stop-timeout 10 : Config.StopTimeout = 10
docker stop mat 10,16 s
Tôi không xác định được chắc chắn con số 1 đến từ đâu — daemon.json trên máy không khai gì về stop-timeout, nên nhiều khả năng là mặc định của bản Docker này (29.3.1, Docker Desktop). Tôi không kết luận rằng "Docker 29 đổi mặc định thành 1", vì tôi chỉ có một máy để đo.
Nhưng bài học thực dụng thì không phụ thuộc vào việc máy bạn là 1 hay 10:
Đừng dựa vào thời gian ân hạn mặc định. Hãy khai tường minh.
docker run --stop-timeout 30 ...
# trong Compose
services:
api:
stop_grace_period: 30s
Vì con số này quyết định ứng dụng của bạn có kịp đóng kết nối, ghi nốt bộ đệm và dừng tử tế hay không. Một giây là quá ít cho gần như mọi thứ có trạng thái.
Mã thoát 137 nghĩa là bị SIGKILL (128 + 9). Thấy nó trong log nghĩa là container đã không tự dừng trong thời gian ân hạn và bị giết. Nếu dịch vụ của bạn thường xuyên thoát với 137 lúc triển khai, bạn đang mất dữ liệu ở đâu đó mà không ai báo.
Còn docker kill thì không có ân hạn: 0,14 giây, gửi SIGKILL ngay. Dùng nó khi gỡ lỗi, đừng dùng trong kịch bản triển khai.
Lớp ghi: co lại được, khác với image
Mỗi container có một lớp filesystem ghi được nằm trên các layer chỉ đọc của image. docker ps -s cho biết nó to bao nhiêu:
moi tao, chua ghi gi : SizeRw = 4,1 kB (virtual 9,51 MB)
sau khi ghi 30 MB : 31,5 MB (virtual 41 MB)
sau khi XOA tep do : 4,1 kB (virtual 9,51 MB)
sau khi dung container: 4,1 kB
Chú ý dòng thứ ba: xoá tệp làm lớp ghi co lại thật. Đây là điểm ngược hẳn với layer của image — phần 2 đo được rằng xoá tệp ở một layer sau không làm image nhỏ đi, vì layer chỉ cộng thêm chứ không trừ đi.
Khác biệt nằm ở chỗ: layer image đã đóng băng và được chia sẻ giữa nhiều container, nên không sửa được. Lớp ghi thì thuộc về riêng container này và là một thư mục bình thường, xoá là mất.
Hai cột trong docker ps -s cũng đáng phân biệt: SizeRw là phần container tự ghi, còn virtual là tổng cả layer image — mà layer image thì dùng chung, nên cộng cột virtual của nhiều container sẽ ra con số phóng đại, đúng như cái bẫy docker images ở phần trước.
Container đã thoát vẫn giữ log
Log không nằm trong lớp ghi — nó nằm ở chỗ khác trên máy chủ:
$ docker inspect nhat-ky --format '{{.LogPath}}'
/var/lib/docker/containers/822ba76f.../822ba76f...-json.log
Tôi cho một container in 200 000 dòng rồi đo:
docker logs tra ve : 200 000 dong | 10,0 MB
tep tren dia : 23,4 MB
cau hinh xoay vong : map[] (rong — khong xoay vong)
Nội dung log 10 MB nhưng tệp trên đĩa 23,4 MB, gấp 2,3 lần, vì driver json-file bọc mỗi dòng vào một đối tượng JSON kèm dấu thời gian và tên luồng. Và cấu hình xoay vòng rỗng — mặc định không có giới hạn nào cả.
Chúng ta sẽ mổ kỹ chuyện này ở phần về log driver. Điều cần nhớ bây giờ, vì nó thuộc về vòng đời: log sống cùng container, và chỉ mất khi docker rm. Container đã exited vài tháng vẫn giữ nguyên tệp log của nó.
Bốn lệnh nên phân biệt cho rõ
| Lệnh | Làm gì | Còn lại gì |
|---|---|---|
docker stop |
gửi SIGTERM, chờ ân hạn, rồi SIGKILL |
container exited, lớp ghi và log còn |
docker kill |
SIGKILL ngay |
như trên |
docker rm |
xoá container | lớp ghi và log mất |
docker run --rm |
tự rm khi thoát |
không còn gì để khám nghiệm |
--rm rất tiện cho lệnh dùng một lần, nhưng nó cũng xoá luôn bằng chứng. Khi một container thoát mà bạn không hiểu vì sao, hãy chạy lại không có --rm rồi docker inspect và docker logs cái xác của nó.
Muốn biết thời gian ân hạn thật trên máy mình — con số quyết định ứng dụng có kịp dừng tử tế không — đo thẳng bằng một container cố tình phớt lờ SIGTERM:
docker run -d --name thu-tin-hieu alpine sh -c 'trap "" TERM; while true; do sleep 1; done'
time docker stop thu-tin-hieu
docker inspect thu-tin-hieu --format 'StopTimeout={{.Config.StopTimeout}} ExitCode={{.State.ExitCode}}'
docker rm thu-tin-hieu
ExitCode=137 nghĩa là container bị giết chứ không tự dừng. Nếu time báo khoảng một giây, bạn vừa biết mình có ít thời gian hơn tưởng rất nhiều.
Mẫu số chung
Bài học đầu tiên, đọc thẳng từ bảng docker stop: "dừng tử tế" là một cuộc thương lượng có hạn chót — một tín hiệu lịch sự (SIGTERM) rồi một cú giết cứng (SIGKILL) sau khoảng ân hạn — và nó chỉ hoạt động khi cả hai điều kiện đúng: ứng dụng thật sự lắng nghe tín hiệu lịch sự, và khoảng ân hạn đủ dài để làm nốt việc. Thiếu một trong hai là mất dữ liệu đang dang dở trong im lặng, hiện ra dưới dạng mã 137. Cùng khuôn "tín hiệu + hạn chót" ở khắp nơi: terminationGracePeriodSeconds của Kubernetes, rút cạn kết nối trước khi tắt, checkpoint của CSDL lúc shutdown, consumer hàng đợi làm nốt thông điệp đang cầm, shutdown hook của JVM. Nguyên tắc: tắt-cho-sạch là code bạn phải viết và thời gian bạn phải cấp, không phải thứ trời cho — bắt tín hiệu, dọn dẹp, và khai tường minh khoảng ân hạn đủ cho việc dọn đó.
Điều thứ hai, và là lý do tôi phải kiểm ba lần: đừng tin con số mặc định được ghi trong tài liệu — hãy đọc con số thật trên hệ thống của mình. "Ai cũng bảo 10 giây" mà máy tôi đo 1 giây; tôi không đoán mà hỏi thẳng docker inspect, và cũng không vội kết luận "Docker 29 đổi mặc định" chỉ từ một máy. Cùng cái bẫy "mặc định theo lời đồn" ở khắp nơi: giá trị cấu hình trôi giữa các phiên bản, bản phân phối, nhà cung cấp; những timeout và giới hạn khác nhau theo môi trường. Nguyên tắc: một mặc định được nhắc đi nhắc lại là một lời tuyên bố, không phải một sự thật — hãy xác minh trên chính hệ thống, ghim nó tường minh để khỏi phụ thuộc vào cái ngầm định, và đừng suy rộng một quan sát đơn lẻ thành quy luật chung.
Phần sau bàn về registry, tag và digest — và đo chuyện gì xảy ra khi một tag bị đẩy đè.