OOMKilled là trạng thái pod hay gặp nhất và cũng hay bị chẩn đoán sai nhất. Bài này dựng nó có chủ ý và đo chính xác chuyện gì xảy ra.
Ứng dụng không nhận được tín hiệu nào
Một ứng dụng bắt đủ ba tín hiệu và in ra khi nhận được:
for s in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
signal.signal(s, h)
Giới hạn 128Mi, cấp phát dần 10 MB mỗi lần:
100 MB
110 MB
120 MB
(hết log)
số dòng "NHAN TIN HIEU": 0
kết thúc: OOMKilled exitCode=137
Không một tín hiệu nào.
137 = 128 + 9, tức SIGKILL. Kernel OOM killer không thương lượng: không có giai đoạn dọn dẹp, không ghi nốt log, không đóng kết nối, không flush bộ đệm.
Điều này khác hẳn với việc pod bị xoá (phần 44), nơi container nhận SIGTERM và có terminationGracePeriodSeconds để tự dọn.
Hệ quả thực dụng: mọi cơ chế tắt sạch bạn viết đều vô dụng khi bị OOMKill. Nếu ứng dụng cần ghi nốt trạng thái trước khi chết, nó phải làm việc đó liên tục, không phải lúc tắt.
Log cuối cùng dừng ở 120 MB với giới hạn 128 MB — nên bạn thấy được nó tiến gần tới trần, nhưng không có dòng nào nói "sắp hết".
Kubernetes không sinh event nào
kubectl get events --field-selector involvedObject.name=sig
Scheduled Pulled Created Started
Không có dòng nào nói tới OOM.
Thông tin chỉ nằm ở trạng thái pod:
kubectl get pod <ten> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
# OOMKilled
Bảng theo dõi dựa vào event sẽ không bao giờ thấy OOMKilled. Đây là lý do rất nhiều đội chỉ phát hiện ra vấn đề khi có người tình cờ chạy kubectl get pods và thấy cột RESTARTS cao bất thường.
Cách theo dõi đúng là quét trạng thái pod:
kubectl get pods -A -o json | python3 -c '
import sys,json
for p in json.load(sys.stdin)["items"]:
for cs in p["status"].get("containerStatuses",[]):
for st in (cs.get("state",{}), cs.get("lastState",{})):
t = st.get("terminated",{})
if t.get("reason")=="OOMKilled":
print(p["metadata"]["namespace"], p["metadata"]["name"],
cs["name"], "restarts=%d"%cs["restartCount"])
'
Hoặc dùng chỉ số kube_pod_container_status_last_terminated_reason của kube-state-metrics.
OOMKilled khác Evicted
Hai trạng thái tên gần giống nhau, cơ chế khác hẳn, và cách chữa cũng khác.
OOMKilled — ở mức container:
- cgroup của một container vượt
limits.memory - kernel giết đúng tiến trình đó,
exitCode 137 - pod vẫn tồn tại,
restartPolicyquyết định có chạy lại không - pod ở lại đúng node đó và cứ khởi động lại
Evicted — ở mức node:
- cả node thiếu bộ nhớ, vượt ngưỡng
evictionHardcủakubelet kubeletđuổi pod khỏi node- pod bị xoá, và ReplicaSet tạo bản mới ở chỗ khác
- thứ tự đuổi theo lớp QoS:
BestEffort→Burstable→Guaranteed(phần 53)
Phân biệt bằng một câu hỏi: pod có đổi node không? Không đổi và RESTARTS tăng → OOMKilled. Biến mất rồi xuất hiện ở node khác → Evicted.
Cách chữa cũng ngược nhau. OOMKilled là ứng dụng của bạn dùng quá limits — nâng giới hạn hoặc sửa rò rỉ. Evicted là node thiếu chỗ — thêm node, hoặc chỉnh requests để bộ lập lịch không nhồi quá nhiều.
Vòng lặp không tự thoát
RESTARTS = 2, rồi 3, rồi tiếp
restartPolicy: Always (mặc định của Deployment) khiến container khởi động lại mãi. Khởi động lại không sửa được rò rỉ bộ nhớ — ứng dụng mới lên, lại rò, lại chết.
Kubernetes có CrashLoopBackOff giãn dần khoảng cách (10 giây, 20, 40, tới tối đa 5 phút), nhưng vòng lặp không kết thúc.
Đây là lý do RESTARTS là cột đáng theo dõi nhất trong kubectl get pods. Một con số tăng đều nghĩa là có thứ đang hỏng lặp lại, và OOM là nguyên nhân phổ biến nhất.
Bốn nguyên nhân, theo thứ tự hay gặp
Giới hạn đặt quá thấp. Ứng dụng vốn cần 500 MB mà limits.memory: 256Mi. Chữa bằng cách đo mức dùng thật rồi nâng.
Rò rỉ bộ nhớ thật. RESTARTS tăng đều theo giờ, và thời gian giữa hai lần chết gần như không đổi. Nâng giới hạn chỉ kéo dài chu kỳ.
Runtime không biết giới hạn của container. Đây là nguyên nhân âm thầm nhất. JVM cũ nhìn RAM của node chứ không nhìn cgroup, nên đặt heap theo con số sai. Java 10 trở lên đã sửa bằng UseContainerSupport (bật mặc định), nhưng ảnh cũ hoặc cấu hình -Xmx cứng vẫn vấp. Node.js, Go và Python có vấn đề tương tự với bộ đệm và bộ dọn rác.
Đỉnh tải ngắn. Ứng dụng bình thường dùng 200 MB nhưng một yêu cầu nặng đẩy lên 400 MB. kubectl top lấy mẫu mỗi vài chục giây nên hoàn toàn có thể bỏ lỡ đỉnh đó — bạn thấy mức dùng thấp mà pod vẫn chết.
Điều tra một ca cụ thể
# lần chết gần nhất
kubectl get pod <ten> -o jsonpath='{.status.containerStatuses[0].lastState.terminated}' | python3 -m json.tool
# log TRƯỚC khi chết — không có cờ này thì bạn xem log của container mới
kubectl logs <ten> --previous
# giới hạn đang đặt
kubectl get pod <ten> -o jsonpath='{.spec.containers[0].resources}'
# mức dùng hiện tại
kubectl top pod <ten> --containers
Cờ --previous là thứ hay bị quên nhất. Không có nó, bạn đang đọc log của container vừa khởi động lại, và nó không chứa gì về nguyên nhân.
Thử ba mươi giây
Tìm mọi thứ từng bị OOMKill trong cụm:
kubectl get pods -A --no-headers \
-o custom-columns=NS:.metadata.namespace,TEN:.metadata.name,\
R:.status.containerStatuses[0].restartCount,\
LY_DO:.status.containerStatuses[0].lastState.terminated.reason \
| grep OOMKilled
Mỗi dòng là một ứng dụng đang bị giết lặp lại mà không sinh ra event nào — và nếu cột R là con số lớn, nó đã bị giết nhiều lần trước khi bạn chạy lệnh này.
Phần sau đo Service: cách một cái tên DNS biến thành lưu lượng tới đúng pod.