Memory còn trống nhưng Kubernetes container vẫn OOMKilled

Một issue cũng khá dễ gặp trong Kubernetes là OOMKilled mà chưa thấy nhiều bài viết chia sẻ nên viết bài bác nào chưa dính có thể xem thử nhé, bài chia sẻ cứ mỗi hôm thêm một vài ý nên cũng khá dài thôi thì bác nào cần thì càng đầy đủ nhé.

2a614aa7-6211-41e7-8cbc-31444fbf6bb7

Lỗi này cơ bản nhìn trên dashboard vẫn ổn, node vẫn còn RAM, nhưng pod lại bị restart liên tục. Chạy kubectl describe pod thì thấy lý do là:

Last State:     Terminated
Reason:         OOMKilled
Exit Code:      137

Anh em gặp lần đầu có thể vô thức hỏi luôn là: node còn RAM sao container lại OOM nhỉ?

Chỗ dễ nhầm chính là ở đây container không được dùng toàn bộ RAM còn trống của node. Nó chỉ được dùng trong giới hạn memory riêng của nó.

Nói đơn giản: Node còn RAM không có nghĩa container còn RAM.

Vì vậy khi anh em gặp OOMKilled, câu chính xác hơn cần hỏi là: container đã chạm limit của nó chưa?

OOMKilled thật ra nghĩa là gì?

OOMKilled hiểu đơn giản là process trong container bị kernel kill vì dùng quá phần memory được phép dùng.

Trong k8s mỗi container có thể có

resources:
  requests:
    memory: "512Mi"
  limits:
    memory: "1Gi"

request giống như lời nhắn với scheduler: container này cần tối thiểu chừng này memory để được xếp lên node.

limit mới là mức tối đa container được phép dùng. Vượt mức này thì container có thể bị kernel kill.

Container có thể dùng vượt request nếu node còn tài nguyên. Nhưng nó không được vượt limit.

Vì vậy một pod có cấu hình như này:

resources:
  requests:
    memory: "512Mi"
  limits:
    memory: "768Mi"

thì dù node còn nhiều RAM, container vẫn có thể OOMKilled khi memory chạm gần 768Mi.

Case dễ gặp

Một service API có endpoint export báo cáo. Bình thường chỉ export vài nghìn dòng nên chạy ổn. Pod được cấu hình:

resources:
  requests:
    memory: "512Mi"
  limits:
    memory: "1Gi"

Sau đó có user export báo cáo rất lớn, ví dụ dữ liệu 6 tháng hoặc 1 năm. Code trong app lại làm kiểu này:

  • Lấy toàn bộ data từ database
  • Giữ tất cả trong một list lớn trong RAM
  • Tạo file Excel/CSV trong RAM
  • Sau đó mới trả file về cho user

Hay dễ hình dung hơn là container chỉ được dùng tối đa 1Gi memory, nhưng app lại cố nhét cả báo cáo lớn vào RAM cùng lúc.

Báo cáo càng lớn, hoặc càng nhiều request export chạy cùng lúc, memory càng dễ chạm 1Gi và bị kill.

Cái này mới quan trọng là từ góc nhìn application, có khi không có stack trace rõ ràng. Chỉ là container biến mất, rồi được restart lại.

Và đây là lúc dashboard có thể làm mình hiểu sai vấn đề.

Vì sao dashboard dễ làm chúng ta bị nhầm?

Vì OOM thường xảy ra rất nhanh, rồi container được restart ngay sau đó.

Lúc request export chạy, memory có thể tăng sát 1Gi rồi container bị kill. Nhưng khi mở dashboard sau đó, thứ mình thấy lại là container mới sau restart:

  • Node vẫn còn nhiều RAM
  • Pod hiện chỉ dùng vài trăm Mi
  • Mọi thứ nhìn có vẻ bình thường

Nên rất dễ thắc mắc: Node còn RAM mà sao lại OOMKilled

kubectl top pod sau restart cũng vậy:

kubectl top pod report-api-v2-primary-65f8d -n prod
NAME                              CPU(cores)   MEMORY(bytes)
report-api-v2-primary-65f8d      120m         320Mi

Nhìn như pod chỉ dùng 320Mi, nhưng đó là memory của container mới sau restart.

Vì vậy với OOMKilled, đừng chỉ nhìn trạng thái hiện tại. Phải xem lịch sử metric quanh lúc restart để biết memory có từng spike sát limit hay không.

Kiểm tra pod có bị OOMKilled không

Sau khi biết dashboard hiện tại có thể gây hiểu nhầm, bước đầu tiên là xác nhận trực tiếp trong Kubernetes xem pod restart vì lý do gì:

kubectl describe pod report-api-v2-primary-65f8d -n prod

Tìm đoạn:

Containers:
  report-api:
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
            Started:      Sat, 28 Mar 2026 10:12:14 +0700
            Finished:     Sat, 28 Mar 2026 10:18:41 +0700
    Restart Count:  5

Nếu muốn xem nhanh hơn bằng jsonpath:

kubectl get pod report-api-v2-primary-65f8d -n prod \
  -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'

Kết quả:

OOMKilled

Xem event gần đây:

kubectl get events -n prod --sort-by='.lastTimestamp' | grep -i oom

Có thể thấy:

Warning   OOMKilling   pod/report-api-v2-primary-65f8d   Container report-api was killed due to memory limit

Không phải cluster nào cũng giữ event đủ lâu. Nếu incident đã qua lâu, event có thể mất. Lúc đó mình phải dựa vào metrics, log runtime hoặc monitoring.

Kiểm tra request và limit

Sau khi đã xác nhận pod bị OOMKilled, bước tiếp theo là xem container được phép dùng tối đa bao nhiêu memory.

kubectl get pod report-api-v2-primary-65f8d -n prod -o yaml | grep -A8 resources

Ví dụ output:

resources:
  limits:
    memory: 1Gi
  requests:
    memory: 512Mi

Hoặc xem trực tiếp trong deployment:

kubectl get deploy report-api -n prod -o yaml | grep -A12 resources

Có 3 câu nên trả lời ngay:

  • Memory limit là bao nhiêu?
  • App có đang load dữ liệu lớn vào RAM không?
  • Metric trước lúc restart có chạm gần limit không?

Nếu limit là 1Gi và biểu đồ memory trước khi restart lên 980Mi rồi rơi về 200Mi, khả năng OOMKilled là rất cao.

Kiểm tra trực tiếp cgroup trong container

Nếu muốn kiểm tra sâu hơn, và container hiện tại còn sống, có thể vào pod đọc thông tin cgroup.

Cgroup là cơ chế Linux dùng để giới hạn tài nguyên của container. Nói đơn giản, đây là nơi Linux ghi lại container đang dùng bao nhiêu memory và được dùng tối đa bao nhiêu.

Với cgroup v2:

kubectl exec -it report-api-v2-primary-65f8d -n prod -- sh

Trong container:

cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.events

Ví dụ:

$ cat /sys/fs/cgroup/memory.max
1073741824

$ cat /sys/fs/cgroup/memory.current
934281216

$ cat /sys/fs/cgroup/memory.events
low 0
high 0
max 128
oom 3
oom_kill 3

Đọc như này:

memory.max      là memory limit của cgroup
memory.current  là memory đang dùng hiện tại
oom_kill        tăng nghĩa là đã có process bị OOM kill trong cgroup

Nếu dùng cgroup v1, đường dẫn có thể là:

cat /sys/fs/cgroup/memory/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/memory.failcnt

Không phải image nào cũng có đủ tool để kiểm tra, nhưng các file cgroup thường có sẵn.

Phân biệt OOMKilled và Evicted

Chỗ này mình cũng nên tách rõ 2 trường hợp, vì nhìn qua khá giống nhau.

TH1: container vượt memory limit

Reason: OOMKilled
Exit Code: 137

Đây thường là container bị kill vì dùng vượt limit của chính nó.

TH2: pod bị kubelet evict vì node thiếu tài nguyên

Status: Failed
Reason: Evicted
Message: The node was low on resource: memory.

Hai trường hợp này khác nhau.

  • Với OOMKilled, vấn đề thường nằm ở memory usage của container so với limit.
  • Với Evicted, vấn đề nằm ở node memory pressure, QoS class, request, priority, và mức sử dụng tài nguyên trên node.

Kiểm tra node:

kubectl describe node <node-name> | grep -A5 -i MemoryPressure

Ví dụ:

MemoryPressure   False

Nếu MemoryPressure=False nhưng container vẫn OOMKilled, thường vấn đề nằm ở limit riêng của container, không phải node hết RAM.

Những nguyên nhân thường gặp

1. Limit đặt quá thấp so với workload thật

Đây là nguyên nhân dễ gặp nhất.

Ban đầu app chạy ổn vì traffic còn thấp. Sau đó user nhiều hơn, request nặng hơn, export lớn hơn, cache nhiều hơn. Memory tăng lên nhưng limit vẫn giữ nguyên.

Lúc này pod bắt đầu restart. Không phải Kubernetes tự nhiên lỗi, chỉ là mức memory tối đa không còn đủ cho workload hiện tại.

Lúc debug, nên nhìn lại:

  • Memory trước lúc restart
  • Traffic lúc đó có tăng không
  • Có request export, batch job, report nặng không
  • Memory có spike lên sát limit không

Đừng chỉ nhìn memory trung bình. OOM thường xảy ra ở một spike ngắn.

2. App giữ quá nhiều dữ liệu trong RAM

Ví dụ với report-api, app export file theo kiểu:

  • Lấy toàn bộ data từ database
  • Giữ hết trong list
  • Tạo file Excel/CSV trong RAM
  • Xong mới trả file về cho user

Cách này vẫn ổn với file nhỏ. Nhưng khi report lớn, memory phình rất nhanh. Nếu nhiều user export cùng lúc, container càng dễ chạm limit và bị kill.

3. Cache không giới hạn

Cache giúp app nhanh hơn, nhưng cache không giới hạn thì rất dễ thành vấn đề.

Ban đầu cache nhỏ nên không sao. Chạy lâu hơn, cache lớn dần, memory tăng dần, cuối cùng pod bị OOMKilled.

4. Memory leak tăng chậm

Một dạng khác là memory không tăng đột ngột, mà cứ leo lên từ từ.

Ví dụ:

08:00   350Mi
10:00   460Mi
12:00   590Mi
14:00   720Mi
16:00   890Mi
16:20   restart

Nếu biểu đồ có dạng này, nên nghi app đang giữ lại object không cần thiết, cache không được dọn, buffer không release, hoặc connection/session bị giữ quá lâu.

Tăng limit có thể giúp pod chết chậm hơn, nhưng không xử lý gốc.

5. Sidecar cũng dùng memory

Một pod không phải lúc nào cũng chỉ có app container. Có thể còn có:

  • istio-proxy
  • log agent
  • metrics exporter

Vì vậy khi debug, nên xem memory theo từng container:

kubectl top pod report-api-v2-primary-65f8d -n prod --containers

Nếu chỉ nhìn tổng pod, rất dễ bỏ sót container nào đang tăng.

Runbook nhanh khi gặp OOMKilled

Khi pod restart liên tục, đừng đoán vội. Cứ đi theo vài bước này, thường sẽ ra hướng khá nhanh.

1. Xác nhận đúng là OOMKilled

kubectl describe pod 
<pod> -n <namespace>

Tìm đoạn:

Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Restart Count: ...

Nếu reason là Evicted, hướng debug sẽ khác. Lúc đó cần xem node có bị thiếu memory hay không.

2. Xem container được phép dùng bao nhiêu memory

kubectl get deploy 
<deploy> -n <namespace> -o yaml | grep -A12 resources

Ghi lại vài thứ quan trọng:

memory request
memory limit
container nào bị restart

Điểm quan trọng là memory limit. Nếu container chỉ có limit 1Gi, thì dù node còn RAM, container vẫn có thể bị kill khi chạm gần 1Gi.

3. Xem memory trước thời điểm restart

kubectl top sau khi pod restart chỉ để tham khảo, vì nó đang hiển thị container mới.

Mở biểu đồ monitoring và so 3 thứ này:

container memory usage
container memory limit
restart time

Ví dụ:

10:10   memory 420Mi
10:14   memory 760Mi
10:18   memory 980Mi   gần limit 1Gi
10:19   restart
10:20   memory 220Mi   container mới sau restart

Nhìn kiểu này thì khá rõ: container cũ đã leo sát limit rồi bị kill. Con số 220Mi sau restart chỉ là memory của container mới.

4. Nhìn pattern memory

Nhìn biểu đồ memory quanh thời điểm restart:

  • Tăng vọt: nghi request nặng, export file, batch job, payload lớn.
  • Tăng từ từ: nghi memory leak hoặc cache không giới hạn.
  • Tăng theo traffic: nghi concurrency cao, nhiều request chạy cùng lúc.

Với report-api, nếu spike xảy ra đúng lúc có request export lớn, hướng điều tra khá rõ: app đang giữ quá nhiều dữ liệu trong RAM.

5. Xử lý tạm thời

Nếu đang incident, ưu tiên đưa hệ thống ổn lại trước:

  • Tăng memory limit tạm thời nếu node còn đủ tài nguyên.
  • Tăng số replica nếu app có thể scale ngang.
  • Giảm hoặc tắt tạm endpoint export/report nặng.
  • Giảm concurrency của worker.
  • Giảm page size hoặc giới hạn khoảng thời gian export.
  • Rollback nếu OOM bắt đầu sau một bản deploy mới.

Ví dụ với report-api, cách chữa cháy có thể là:

Tạm giới hạn export tối đa 31 ngày
Giảm số job export chạy cùng lúc
Tăng limit từ 1Gi lên 1536Mi nếu node còn đủ tài nguyên

Nhưng nhớ: tăng limit chỉ giúp mua thời gian. Nếu app vẫn load cả report lớn vào RAM, pod vẫn có thể chết lại.

Fix dài hạn

Về dài hạn, tăng RAM chỉ nên xem là cách mua thêm thời gian. Muốn incident không lặp lại, mình phải sửa chỗ làm app dễ chạm limit.

1. Sửa luồng export/report

Nếu lỗi đến từ export report, việc đầu tiên nên nhìn lại là cách app tạo file.

Nên tránh kiểu load toàn bộ dữ liệu vào RAM rồi mới trả file. Thay vào đó có thể:

  • Stream từng phần dữ liệu ra file.
  • Phân trang khi đọc database.
  • Giới hạn khoảng thời gian export, ví dụ tối đa 31 ngày.
  • Với report lớn, chuyển sang job chạy nền rồi gửi link tải sau.

2. Chặn request quá nặng từ đầu

App nên tự bảo vệ mình trước những request quá lớn:

  • Giới hạn page size.
  • Giới hạn request body.
  • Giới hạn số dòng export.
  • Giới hạn số job export chạy cùng lúc.
  • Trả lỗi rõ ràng nếu report quá lớn.

Ví dụ trả về kiểu “report quá lớn, vui lòng chọn khoảng thời gian ngắn hơn” vẫn tốt hơn để pod chết rồi user thấy lỗi lạ đó anh em.

3. Chỉnh lại request và limit

Sau khi đã hiểu app dùng memory thế nào, lúc đó hãy chỉnh lại resource dựa trên:

  • Memory p95/p99.
  • Peak trong giờ cao điểm.
  • Memory lúc chạy report lớn.
  • Node còn đủ tài nguyên hay không.

Nếu report lớn có thể đẩy memory lên quá cao, tăng limit chỉ là tạm thời. Gốc vẫn là cách app xử lý report.

4. Thêm alert sớm

Đừng chờ pod restart rồi mới biết. Nên có alert khi:

  • Memory usage vượt 85% limit trong vài phút.
  • Container restart tăng bất thường.
  • Last terminated reason là OOMKilled.
  • Memory tăng đều trong nhiều giờ.

5. Test lại bằng tình huống từng gây lỗi

Sau khi fix, nên chạy lại đúng workload từng làm pod chết:

  • Export report lớn.
  • Nhiều request export cùng lúc.
  • Traffic cao hơn bình thường.
  • Batch job chạy cùng thời điểm.

Nếu memory không còn spike sát limit, lúc đó fix mới đáng tin hơn.

Kết luận

OOMKilled trong Kubernetes không nên hiểu đơn giản là “server hết RAM”.

Nhiều khi server vẫn còn RAM. Node nhìn vẫn ổn. Cluster cũng chưa cảnh báo gì lớn.

Nhưng container thì đã chạm giới hạn của chính nó.

Vì vậy, khi gặp OOMKilled, nên đổi câu hỏi từ:

Node còn memory không?

sang:

Container còn memory trong limit của nó không?

Vì trong Kubernetes, app không sống trong toàn bộ node. Nó sống trong phần tài nguyên được cấp cho container.

Và khi phần đó hết memory, kernel sẽ kill process. Không chờ app cleanup, cũng không quan tâm dashboard node bên ngoài nhìn vẫn đẹp.

Chia sẻ bài viết:
Theo dõi
Thông báo của
0 Góp ý
Được bỏ phiếu nhiều nhất
Mới nhất Cũ nhất

Tiêu điểm chuyên gia