OOMKilled — Pod bị kill dù RAM còn thừa
Pod chết, mà node vẫn dư RAM
Cảnh này ai xài Kubernetes cũng gặp: kubectl describe pod ghi OOMKilled, exit code 137, nhưng kubectl top node báo node còn 6GB RAM rảnh. Vậy ai giết pod?
Không phải scheduler. Là kernel, thông qua cgroup. Node còn RAM chẳng có nghĩa gì nếu cgroup của container đã chạm trần.
Request vs Limit — hai thứ khác nhau hoàn toàn
requests: chỉ để scheduler biết xếp pod lên node nào. Không giới hạn gì cả.limits: ghi thẳng vàocgroup.memory.max(cgroup v2). Vượt là bị kill ngay, không thương lượng.
Nên chuyện "node còn RAM" và "container bị kill" là hai chuyện độc lập. Trần nằm ở container, không nằm ở node.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
spec:
template:
spec:
containers:
- name: api
image: ghcr.io/phonghy/payment-api:1.4.2
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
env:
- name: GOMEMLIMIT
value: "400MiB"
- name: GOGC
value: "100"
Cái bẫy: runtime ngôn ngữ giữ heap lớn hơn bạn nghĩ
Go, Java, Node đều không trả RAM về OS ngay. Go GC mặc định (GOGC=100) chỉ chạy khi heap tăng 100% so với lần trước, và RSS thường cao hơn "memory thật" rất nhiều vì runtime giữ vùng nhớ đã cấp phát.
Thực tế: service Go dùng thật 80MB, nhưng RSS phình 300-400MB sau vài giờ dưới tải. Container limit 256Mi → OOMKilled. Bạn sẽ đi tìm leak, trong khi vấn đề chỉ là GC chưa được thúc.
Từ Go 1.19 có GOMEMLIMIT — soft limit cho heap. Đặt nó ~80% memory limit là Go sẽ GC siêng hơn khi tới gần trần, thay vì phình tới chết:
// main.go — không cần code gì, chỉ cần biến môi trường.
// GOMEMLIMIT=400MiB khi memory limit 512Mi
// pprof để soi heap khi cần
import _ "net/http/pprof"
func main() {
srv := &http.Server{Addr: ":8080", Handler: mux()}
log.Println("listen :8080")
log.Fatal(srv.ListenAndServe())
}
Debug trong đúng 4 lệnh
# 1. Lý do chết: OOMKilled hay Error?
kubectl get pod payment-api-7d9f -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
# 2. Đang ăn bao nhiêu (cần metrics-server)
kubectl top pod payment-api-7d9f --containers
# 3. Trần cgroup thật sự là bao nhiêu (cgroup v2)
kubectl exec -it payment-api-7d9f -- cat /sys/fs/cgroup/memory.max
kubectl exec -it payment-api-7d9f -- cat /sys/fs/cgroup/memory.current
So memory.max với memory.current: nếu current sát trần trong lúc rảnh, thì không phải tải cao mà là leak thật.
Kinh nghiệm xương máu
Service Go của mình từng bị OOM đều đặn mỗi ~2 ngày, limit 512Mi. Đọc heap profile bằng pprof thì ra thủ phạm: http.ResponseWriter bị bọc trong buffer không bao giờ Close(), lớn dần theo mỗi request lỗi. GOMEMLIMIT chỉ giúp sống thêm vài giờ, leak vẫn phải fix bằng code.
Bài học: GOMEMLIMIT giảm đau, không chữa bệnh. Bật pprof trước khi đoán.
Vài nguyên tắc mình áp dụng tới giờ:
limits.memory= 1.5-2x mức dùng p99 thật, đừng lấy số đẹp cho dễ nhìn.GOMEMLIMIT= ~80% memory limit (Go 1.19+), đặt luôn từ đầu chứ đừng chờ sự cố.- Với Go:
requests == limitsđể pod vào QoS classGuaranteed, ổn định hơn dưới áp lực node. - Đừng hạ limit cho "tiết kiệm tài nguyên". Vòng lặp
CrashLoopBackOfftốn kém hơn nhiều so với vài trăm MB RAM.
Exit code 137 = 128 + 9 (SIGKILL). Không phải app tự chết, mà là bị bắn hạ."