Nhân Linux đối xử với PID 1 khác mọi tiến trình khác: nó không cài trình xử lý mặc định cho tín hiệu. Với tiến trình thường, SIGTERM không có handler nghĩa là chết. Với PID 1, không có handler nghĩa là bỏ qua.
Trong container, ứng dụng của bạn thường chính là PID 1.
Cùng chương trình, hai vị trí
Một script Python không đăng ký handler nào cho SIGTERM:
| PID 1 là gì | docker stop |
Mã thoát | |
|---|---|---|---|
| Chạy thẳng | python /khong-xu-ly.py |
5,15 s | 137 |
Có --init |
/sbin/docker-init -- python ... |
0,11 s | 143 |
Với --init, Docker chèn một init tối giản làm PID 1 và ứng dụng thành tiến trình con — nơi ngữ nghĩa tín hiệu bình thường áp dụng trở lại. SIGTERM giết nó ngay.
Hai mã thoát cũng nói lên chuyện đó:
- 137 = 128 + 9 =
SIGKILL. Docker hết kiên nhẫn và giết. - 143 = 128 + 15 =
SIGTERM. Tiến trình chết vì tín hiệu dừng, đúng như thiết kế.
Nhanh hơn 47 lần, chỉ bằng một cờ.
Việc thứ hai của init: dọn tiến trình mồ côi
PID 1 còn một nhiệm vụ nữa — nhận nuôi và dọn xác những tiến trình mất cha. Tôi viết một chương trình cố tình sinh ra 20 tiến trình cháu mồ côi rồi không bao giờ gọi wait():
| Zombie | Tổng tiến trình | |
|---|---|---|
Không --init |
20 | 24 |
Có --init |
0 | 5 |
Hai mươi mục zombie chiếm chỗ trong bảng tiến trình của nhân. Chúng không dùng CPU hay bộ nhớ, nhưng chúng chiếm PID — và một container chạy nhiều ngày với kiểu này sẽ chạm giới hạn PID rồi không fork được nữa, với triệu chứng là Resource temporarily unavailable ở một chỗ hoàn toàn không liên quan.
Ứng dụng nào hay dính? Bất cứ thứ gì gọi tiến trình ngoài: shell script chạy curl, ứng dụng gọi ffmpeg, thư viện sinh worker. Nếu ứng dụng của bạn không bao giờ fork thì không sao — nhưng bạn thường không biết chắc thư viện mình dùng có làm hay không.
--init không cứu được dữ liệu
Đây là chỗ dễ hiểu nhầm nhất. Tôi cho một ứng dụng ghi vào tệp có bộ đệm trong 3 giây rồi dừng container, và so số bản ghi ứng dụng đã viết với số thật sự nằm trên đĩa:
docker stop |
Mã | Đã viết | Trên đĩa | Mất | |
|---|---|---|---|---|---|
Không xử lý TERM, không --init |
5,2 s | 137 | 1 476 | 1 094 | 382 |
Không xử lý TERM, có --init |
0,2 s | 143 | 1 527 | 1 094 | 433 |
| Có xử lý TERM | 0,1 s | 0 | 1 520 | 1 520 | 0 |
--init làm container dừng nhanh gấp 26 lần — và mất nhiều dữ liệu hơn, vì nó chết sớm hơn nên còn ít thời gian để bộ đệm tự đầy rồi tràn xuống đĩa.
Chỉ hàng cuối cho số 0. Ứng dụng phải tự xử lý SIGTERM và tự flush:
def dong(a=None, b=None):
tep.flush()
os.fsync(tep.fileno())
tep.close()
sys.exit(0)
signal.signal(signal.SIGTERM, dong)
Nói cách khác: --init giải quyết vấn đề cơ chế (tín hiệu tới được nơi cần tới), còn việc dừng tử tế là vấn đề của ứng dụng. Cần cả hai.
STOPSIGNAL khi ứng dụng nghe tín hiệu khác
Một số chương trình dùng SIGQUIT hay SIGHUP để dừng tử tế — nginx và một số máy chủ cũ chẳng hạn. Tôi thử một ứng dụng chỉ nghe SIGHUP:
docker stop |
Mã thoát | Log cuối | |
|---|---|---|---|
Không khai STOPSIGNAL |
5,14 s | 137 | chi nghe SIGHUP... |
STOPSIGNAL SIGHUP |
0,10 s | 0 | NHAN SIGHUP, dong tu te |
STOPSIGNAL SIGHUP
Một dòng, và container chuyển từ "bị giết sau 5 giây" sang "dừng tử tế trong 0,1 giây".
Khai được ở cả ba chỗ: STOPSIGNAL trong Dockerfile, --stop-signal khi docker run, và stop_signal: trong Compose.
Bốn thứ cần đúng
Gộp lại từ bài này và phần 8:
- Dùng dạng exec cho
CMDvàENTRYPOINT, và entrypoint script kết thúc bằngexec "$@". Thiếu nó thìshlàm PID 1 và nuốt tín hiệu. - Thêm
--init(hoặcinit: truetrong Compose) trừ khi ứng dụng của bạn tự nó là một init tử tế. Nó rẻ và sửa cả hai vấn đề của PID 1. - Xử lý
SIGTERMtrong ứng dụng — đóng kết nối, ghi nốt bộ đệm, rời bộ cân bằng tải. - Đặt
--stop-timeouttường minh. Phần 3 đo được mặc định trên máy tôi là 1 giây, không phải 10 như tài liệu vẫn ghi.
Đọc mã thoát
Bảng nhỏ đáng dán lên tường:
| Mã | Nghĩa |
|---|---|
| 0 | tự thoát bình thường |
| 137 | SIGKILL — hết giờ ân hạn, hoặc bị OOM giết |
| 143 | SIGTERM — chết vì tín hiệu dừng, chưa chắc đã dừng tử tế |
| 139 | SIGSEGV, hoặc V8 chạm giới hạn heap (phần 26) |
Phân biệt hai nguyên nhân của 137 bằng OOMKilled, như phần 25 đã đo.
Thử ba mươi giây
docker run -d --name thu --stop-timeout 10 <anh-cua-ban>
docker exec thu ps -o pid,args | head -3
time docker stop thu
docker inspect thu --format 'ExitCode={{.State.ExitCode}}'
docker rm thu
Ba dấu hiệu cần nhìn:
- PID 1 là
/bin/sh→ thiếu dạng exec hoặc thiếuexectrong entrypoint script. timebáo gần bằng--stop-timeout→ ứng dụng không phản ứng vớiSIGTERM.ExitCode=137→ bị giết, và mọi bộ đệm chưa ghi đã mất.
Phần sau đo bốn chính sách khởi động lại, và chuyện gì xảy ra khi ứng dụng hỏng vĩnh viễn.