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 trần PID cho bốn con số khác nhau, và kết quả khi chạm trần

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.maxmax. 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, git cho 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--ulimit, Kubernetes thì không.

Cần lấy lại được, có ba chỗ:

  • podPidsLimit trong 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 -n trong 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.