Requests và limits chỉ nói về CPU và RAM. Nhưng container còn hai cái trần nữa, và container thường chạm chúng trước khi chạm trần RAM.
Bốn chỗ đo, bốn con số
| Đo ở đâu | Số tiến trình | Số tệp mở |
|---|---|---|
ulimit trong container |
unlimited | 1.048.576 |
cgroup của container (pids.max) |
19.162 | — |
| cgroup của pod (slice của kubelet) | max | — |
threads-max của node |
127.753 | — |
Dòng đầu và dòng hai mâu thuẫn nhau. ulimit -u báo không giới hạn, trong khi trần thật là 19.162.
Không phải ulimit sai — nó báo đúng cái nó biết: RLIMIT_NPROC, một cơ chế có từ trước cgroup. Cơ chế thật đang chặn là pids controller của cgroup, mà ulimit không nhìn thấy.
Đây là kiểu lỗi tệ nhất khi gỡ rối: công cụ trả lời một cách tự tin, và câu trả lời không liên quan tới thứ đang chặn bạn.
Chỗ đúng để đọc:
kubectl exec <pod> -- cat /sys/fs/cgroup/pids.max
kubectl exec <pod> -- cat /sys/fs/cgroup/pids.current
Con số 19.162 ở đây bằng đúng 15% của threads-max (127.753 × 0,15 = 19.162,95). Tôi không tìm được cấu hình nào khai con số này — podPidsLimit trong cấu hình kubelet để trống, và cgroup của pod thì hoàn toàn không giới hạn. Trần này do trình chạy container đặt cho từng container, không do Kubernetes.
Kiểm chứng: một container Docker thường có pids.max là max. Nên đây là hành vi của đường CRI chứ không phải của Docker nói chung.
Chạm trần trông thế nào
Tôi hạ pids.max của một pod xuống 60 rồi cho nó fork liên tục:
dừng ở 57 tiến trình con: [Errno 11] Resource temporarily unavailable
Ba slot còn lại là shell, tiến trình python, và bộ máy của kubectl exec.
Chỗ đáng để ý là tên lỗi: EAGAIN, "resource temporarily unavailable". Nó nói "thử lại sau".
Nhưng nó không bao giờ hết. Và phần lớn thư viện coi EAGAIN là lỗi tạm thời rồi thử lại vô hạn — đó là cách một pod chạm trần PID biến thành một vòng lặp đốt CPU thay vì một lỗi báo ra. Trên bảng điều khiển bạn thấy CPU tăng vọt, không thấy lỗi nào.
Pod khác trên cùng node chạy bình thường suốt phép thử. cgroup cách ly đúng như quảng cáo: trần PID là trần của riêng container đó.
Ai ăn hết PID
Không phải fork bomb. Ba nguồn quen thuộc hơn nhiều:
- Luồng. JVM, Go runtime, mọi thư viện tạo pool đều tính vào
pids. cgroup không phân biệt tiến trình với luồng — 19.162 là tổng của cả hai. - Tiến trình zombie. PID 1 không phải init thật thì không ai
wait()con đã chết, và zombie vẫn chiếm slot. Đây là lý do thứ hai để không dùng shell làm PID 1, sau chuyện tín hiệu ở phần trước. - Đẻ tiến trình cho mỗi yêu cầu. Gọi
convert,ffmpeg,gitcho mỗi request — chậm một chút là chồng lên nhau.
Đo pids.current theo thời gian là cách phát hiện sớm nhất. Nó tăng đều mà không giảm thì bạn đang rò tiến trình, và bạn còn vài ngày trước khi nó chạm trần.
Số tệp mở: lời khuyên cũ đã hết việc
mặc định (soft = hard = 1.048.576)
mở 200.000 tệp — không lỗi
sau khi tự hạ ulimit -n 1024
dừng ở 1.021 tệp: [Errno 24] No file descriptors available
Trình chạy container đã đặt sẵn một triệu. "Nhớ tăng ulimit -n" là lời khuyên cho thời container còn dùng mặc định 1024 của hệ điều hành; bây giờ nó không còn việc gì để làm.
Nhưng vẫn nên kiểm, vì hai lý do. Thứ nhất, không phải trình chạy nào cũng rộng rãi thế — hãy đo trên cụm của bạn. Thứ hai, image có thể tự hạ nó xuống: một dòng ulimit -n trong script khởi động, hoặc một tệp /etc/security/limits.conf copy từ máy chủ vật lý cũ.
Cũng lưu ý soft = hard ở đây. Khi hai giá trị bằng nhau, tiến trình chỉ hạ được chứ không nâng lại được — hạ nhầm là phải sửa image chứ không sửa được lúc chạy.
Kubernetes không cho khai hai trần này
Pod spec không có trường nào cho pids hay nofile. Docker có --pids-limit và --ulimit, Kubernetes thì không.
Cần lấy lại được, có ba chỗ:
podPidsLimittrong cấu hình kubelet — áp cho mọi pod trên node đó, không riêng ai.- Cấu hình của trình chạy container — cũng ở tầng node.
ulimit -ntrong entrypoint — chỉ hạ được, không nâng.
Vì không khai được theo từng pod, pids cũng không đưa vào bài toán xếp lịch được: scheduler không biết pod nào sẽ ngốn tiến trình. Đó là điểm khác hẳn với CPU và RAM, và là lý do trần này thường chỉ lộ ra lúc sự cố.
Thử ba mươi giây
Xem pod nào đang tiến gần trần PID:
for p in $(kubectl get pods -A -o jsonpath='{range .items[?(@.status.phase=="Running")]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}'); do
ns=${p%%/*}; n=${p##*/}
cur=$(kubectl exec -n "$ns" "$n" -- cat /sys/fs/cgroup/pids.current 2>/dev/null)
max=$(kubectl exec -n "$ns" "$n" -- cat /sys/fs/cgroup/pids.max 2>/dev/null)
[ -n "$cur" ] && echo "$cur / $max $p"
done | sort -rn | head
Dòng đầu bảng là pod dùng nhiều tiến trình nhất. Chạy lại sau vài giờ: con số nào tăng mà không giảm là chỗ đang rò.
Phần sau: đọc log và sự kiện — vì sao kubectl logs mất dữ liệu, và chỗ nào còn giữ lại.