Có kiểu chết được trăng trối, và có kiểu chết bị bắn giữa câu. Phần lớn code dọn dẹp của chúng ta — đóng kết nối, ghi nốt trạng thái, flush bộ đệm — ngầm giả định kiểu thứ nhất: rằng sẽ có một khoảnh khắc được báo trước để thu xếp. OOMKilled là kiểu thứ hai, và nó phơi bày mọi giả định đó là hư cấu. 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, restartPolicy quyế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 evictionHard của kubelet
  • 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.

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.

Mẫu số chung: thứ có thể chết không báo trước thì độ bền phải liên tục, và bạn chỉ thấy vấn đề trên kênh mình canh

Bài này gói hai bài học vượt khỏi Kubernetes. Thứ nhất, và sâu nhất: khi một tiến trình có thể bị giết không báo trước — SIGKILL, OOM, mất điện, node chết — thì mọi logic "dọn dẹp lúc tắt" là hư cấu, và độ bền phải được bảo đảm liên tục chứ không dồn vào phút cuối. Ứng dụng ở trên bắt đủ tín hiệu mà không nhận được cái nào. Đây chính là triết lý crash-only: thiết kế để an toàn khi bị giết ở bất kỳ khoảnh khắc nào, và phục hồi từ trạng thái đã ghi lúc khởi động, thay vì tin vào một lần tắt sạch. Cơ sở dữ liệu không flush lúc đóng mà ghi write-ahead log liên tục chính vì lẽ đó; một shutdown hook, một try/finally, một atexit đều không chạy khi kill -9 hay mất điện. Nguyên tắc: giả định bạn có thể chết giữa hai lệnh bất kỳ, nên hãy ghi bền khi đi, đừng hứa sẽ dọn khi về.

Thứ hai: bạn chỉ nhìn thấy vấn đề trên đúng cái kênh bạn đang canh — và "không có cảnh báo" không phải "không có vấn đề" nếu bạn canh nhầm kênh. OOMKilled không sinh event nào; nó chỉ nằm trong một trường status, nên mọi hệ theo dõi dựa vào event sẽ mù tuyệt đối với nó, và đội vận hành chỉ tình cờ phát hiện qua cột RESTARTS. Cùng cái bẫy với việc cảnh báo dựa trên log trong khi lỗi đi vào một metric, hay ngược lại; với việc canh "đường hạnh phúc" mà quên kênh nơi thất bại thật sự hiện ra. Trước khi tin hệ thống khoẻ vì im ắng, hãy chắc mình đang lắng nghe đúng kênh mà sự cố sẽ nói.

Xen giữa là hai lưu ý quen: khởi động lại không sửa được lỗi cấu trúc (một rò rỉ cứ rò lại sau mỗi lần restart, y như thử lại một lỗi cố hữu); và hai tên gần giống nhau — OOMKilled với Evicted — có cơ chế và cách chữa ngược nhau, phân biệt bằng đúng một câu hỏi "pod có đổi node không".

Mẫu số chung: thiết kế để chết an toàn bất cứ lúc nào và bền bỉ liên tục thay vì dọn-lúc-tắt; và canh đúng kênh mà sự cố hiện ra, vì im lặng trên kênh sai chỉ là sự yên tâm giả.

Muốn tự soi, 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.