Website chạy trên Kubernetes chậm dần sau vài ngày, thỉnh thoảng trả 504, log ứng dụng trống trơn. Bạn mở biểu đồ CPU của nhà cung cấp và thấy các node chỉ ở mức 10 đến 20%. CPU của node thấp chưa loại trừ được CPU, vì từng container vẫn có thể bị throttle theo limit, nhưng RAM là giả thuyết cần xem song song. Tài liệu này hướng dẫn phương pháp chẩn đoán khi nghi node đầy RAM mà CPU vẫn thấp: đọc dòng overcommit trong kubectl describe node, loại trừ giả thuyết có kiểm soát, hiểu exit code 137 và trạng thái NotReady. Phần cuối nói thẳng điều gì chưa được chứng minh.

Nếu bạn vừa gặp pod không xếp lịch được vì thiếu CPU, hãy đọc trước Pod Pending vì Insufficient cpu: tính chỗ trống thật từ kubectl describe node. Bài này đi theo chiều ngược lại: pod đã chạy, nhưng node bị đặt quá nhiều việc.

Triệu chứng của node đầy RAM: web chậm, CPU thấp, log trống

  • Trang quản trị rất chậm, trang công khai vẫn nhanh. Hoặc cả hai chập chờn sau vài ngày.
  • Log Laravel tại thời điểm treo không có dòng nào đáng chú ý.
  • Thỉnh thoảng có 504. Triệu chứng này còn có nguyên nhân khác: một phụ thuộc thiếu hoặc chậm (ví dụ Redis chưa triển khai), nên đừng quy hết cho RAM.

Log trống là manh mối: nếu PHP chết vì memory_limit, Laravel thường ghi lỗi "Allowed memory size exhausted". Log im lặng gợi ý tiến trình bị giết từ bên ngoài, ví dụ khi node đầy RAM.

Vì sao web chậm khi CPU node thấp: giả thuyết cần loại trừ song song

CPU và RAM không đổi chỗ cho nhau. Thiếu CPU thì tiến trình chạy chậm đi nhưng vẫn sống. Hết RAM thì kernel phải thu hồi page cache, đẩy ra swap (nếu có) hoặc giết tiến trình. Một node đầy RAM có thể rỗi CPU: việc đọc tệp mất cache, tiến trình chờ cấp phát, và web chậm mà biểu đồ CPU không phản ánh.

Nhưng đây là giả thuyết, không phải kết luận. Một pod có CPU limit nhỏ (ví dụ 200m) bị CFS throttle dù node chỉ dùng 10 đến 20% CPU, và đó là nguyên nhân kinh điển của "web chậm mà CPU node thấp". Hãy loại trừ cả hai:

# Prometheus: tỉ lệ chu kỳ bị throttle của từng container
rate(container_cpu_cfs_throttled_periods_total[5m])
  / rate(container_cpu_cfs_periods_total[5m])

# hoặc đọc trong container (cgroup v2): nr_throttled tăng nghĩa là bị throttle
cat /sys/fs/cgroup/cpu.stat

Nếu tỉ lệ throttle cao, hãy xem lại CPU limit trước khi đổ lỗi cho node đầy RAM.

Đọc describe node để nhận ra node đầy RAM: requests, limits và overcommitted

Chạy kubectl describe node <ten-node> và nhìn phần Allocated resources. Ví dụ minh hoạ cho một node 2 vCPU, 4 GB có tổng limits vượt Allocatable:

Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource  Requests      Limits
  cpu       1500m (75%)   2400m (120%)
  memory    2400Mi (62%)  4800Mi (125%)

Ba điều cần đọc từ khối này:

  • Requests là thứ scheduler dùng để xếp pod. Memory requests 62% nghĩa là còn chỗ để xếp thêm, nên scheduler vẫn tiếp tục đặt pod lên node.
  • Limits là trần của từng container, và tổng limits được phép vượt 100%. Dòng "Total limits may be over 100 percent" là cảnh báo overcommit chuẩn của Kubernetes, nó không báo lỗi.
  • Cả hai đều là con số khai báo, không phải mức dùng thật; mức dùng thật nằm ở kubectl top node hoặc free -m.

Lưu ý kubectl top cần metrics-server; nếu chưa có sẽ gặp "Metrics API not available", khi đó dùng Prometheus (ví dụ node_memory_MemAvailable_bytes). Để chạy free và dmesg trên node, dùng kubectl debug node/<ten-node> -it --image=busybox rồi chroot /host.

Overcommit RAM nghĩa là nếu nhiều pod cùng tiến sát limit của mình, tổng sẽ lớn hơn RAM thật của node. Khi đó node đầy RAM, kernel phải giết tiến trình hoặc node ngừng phản hồi. Pod có request thấp hơn limit (lớp Burstable) và pod không khai gì (BestEffort) là nhóm bị thiệt đầu tiên. BestEffort có requests bằng 0 nên không làm tăng con số requests, và con số 62% có thể che đi phần dùng thật. Khái niệm control plane, node và pod: xem Kubernetes là gì: control plane, node và pod cho dev PHP.

Loại trừ giả thuyết có kiểm soát

Khi web không ổn định, người vận hành thường thử nhiều thay đổi liên tiếp. Hãy ghi chúng thành bảng loại trừ với nhận xét trung tính thay vì kết luận vội, nhất là khi nghi node đầy RAM:

Thử nghiệmKết quảNhận xét
Bật control plane HAKhông tác dụngĐúng dự đoán: HA lo API server và etcd, không đổi RAM của worker
Chỉnh limit theo biểu đồ GrafanaVẫn không ổnLimit theo biểu đồ chưa chắc khớp đỉnh tải
Bỏ cronjob và queue, bớt một nodeAdmin chậm, web nhanhCó thể do pod admin nằm trên node khác tải hoặc phụ thuộc dịch vụ chậm; chưa xác nhận
Thêm node, chuyển workload sangAdmin vẫn chậmNút thắt có thể nằm ở dịch vụ phụ thuộc (DB, search) hoặc throttling; cần đo từng thành phần

Dùng nodeAffinity để thử giả thuyết

Để kiểm tra giả thuyết "database nằm cạnh workload nặng làm node đầy RAM", bạn có thể ghim tạm một workload sang node khác bằng nodeAffinity. Cú pháp đúng như sau:

spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: kubernetes.io/hostname
                    operator: In
                    values:
                      - <ten-node-thu>

Đây là công cụ thử nghiệm, không phải cấu hình vận hành: ghim theo hostname làm workload mất khả năng tự dời khi node hỏng. Thử xong phải gỡ khối affinity; gỡ và apply lại sẽ roll lại pod, và pod không tự quay về node cũ. Muốn ràng buộc vị trí lâu dài, hãy dùng nhãn của node pool chứ không dùng tên máy.

Một kịch bản điển hình: dời database sang một node đang dùng ít RAM hơn, biểu đồ RAM hạ xuống và web ổn một lúc, rồi lại treo. RAM hạ có thể do tác nhân khác chứ chưa chắc vì dời, và việc "lại treo" cho thấy vị trí của database không phải nguyên nhân duy nhất. Chuyển cả database và admin sang node nhiều RAM hơn mà vẫn chậm cũng cho thấy nguyên nhân không nằm riêng ở một workload.

Lệnh exec dài bị exit code 137

Một hiện tượng liên quan nhưng không nhất thiết do node đầy RAM: lệnh chạy lâu qua kubectl exec (ví dụ lập chỉ mục tìm kiếm) kết thúc với command terminated with exit code 137. Số 137 là 128 + 9, tức tiến trình nhận SIGKILL. Nguyên nhân hay gặp là cgroup của container vượt limit bộ nhớ nên kernel giết tiến trình, nhưng đó chưa phải kết luận chắc chắn. Hãy xác nhận trước khi nâng limit:

kubectl describe pod <pod> | grep -A3 "Last State"   # tìm Reason: OOMKilled
kubectl get events --field-selector involvedObject.name=<pod>
# trong container, cgroup v2: oom_kill tăng nghĩa là có tiến trình bị giết
cat /sys/fs/cgroup/memory.events
# cgroup v1: cat /sys/fs/cgroup/memory/memory.failcnt
# trên node: dmesg | grep -i "killed process"
# trong lúc chạy lệnh: kubectl top pod --containers

Lưu ý: OOMKilled trong describe pod chỉ xuất hiện khi tiến trình chính (PID 1) của container bị giết. Nếu chỉ tiến trình do kubectl exec bị kill thì PID 1 vẫn sống, container không restart và lệnh describe có thể cho kết quả rỗng; đừng vì thế mà kết luận không phải OOM, hãy đọc memory.events hoặc dmesg.

Tiến trình exec chạy trong cgroup của container, nên dùng chung limit với tiến trình chính. Ví dụ với limit 200Mi lệnh bị 137; nâng lên 300Mi thì chạy xong, nhưng một lần chạy thành công không chứng minh 300Mi là đủ cho mọi dữ liệu. Nếu container chạy cả PHP-FPM, queue và cron, limit là của cả container, như image all-in-one trong Docker hoá Laravel: một image all-in-one cho Website AI.

Node NotReady khi quá tải

Khi node đầy RAM trầm trọng, kubelet có thể không kịp gửi heartbeat và control plane đánh dấu node NotReady. Node controller gắn taint not-ready hoặc unreachable (NoExecute); pod bị evict sau tolerationSeconds, mặc định 300 giây, nên web gián đoạn. Nếu nhiều node cùng NotReady mà bằng chứng chỉ là "CPU và RAM đều quá tải", nguyên nhân vẫn chưa chốt. Cách tự kiểm tra khi gặp:

kubectl describe node <ten-node> | grep -A8 "Conditions"
# xem MemoryPressure, DiskPressure, PIDPressure, Ready và Message
kubectl get events -A --field-selector involvedObject.kind=Node

Condition MemoryPressure=True là tín hiệu kubelet đang đuổi pod vì thiếu RAM. Đọc thêm trong tài liệu node-pressure eviction.

Điều chưa chốt về node đầy RAM và cách làm lần sau

Trong nhiều sự cố kiểu này, nguyên nhân treo định kỳ chưa được xác định. Các câu hỏi thường còn bỏ ngỏ về node đầy RAM:

  • RAM đầy do Elasticsearch, MySQL hay thành phần giám sát. Thành phần chiếm nhiều nhất trên biểu đồ chưa chắc là thủ phạm.
  • Bằng chứng thường thiếu free, kubectl top và Condition MemoryPressure, nên số liệu đọc từ biểu đồ dạng ảnh chưa đủ tin cậy.

Lần sau gặp node đầy RAM, hãy thu thập bằng chứng trước khi đoán. Cảnh báo RAM của node ở mức 85 đến 90% dùng thật, theo dõi tỉ lệ tổng limits trên Allocatable cho từng node, và lưu kết quả describe node theo thời gian. Nếu bạn quen quản lý RAM trên VPS, so sánh với Tầng Database: MySQL đa tenant trên VPS cấu hình nhỏ sẽ thấy cùng một bài toán: tổng bộ nhớ khai báo không được vượt RAM thật.

Checklist khi nghi node đầy RAM

  • Nghi node đầy RAM thì đừng kết luận từ biểu đồ CPU. Xem RAM dùng thật bằng kubectl top node (hoặc Prometheus) và free -m.
  • Loại trừ throttling bằng tỉ lệ cfs_throttled song song với RAM.
  • Chạy kubectl describe node: so sánh requests, limits và dòng overcommitted.
  • Kiểm tra Conditions của node đầy RAM: MemoryPressure, Ready.
  • Liệt kê pod trên node đầy RAM, tìm pod BestEffort hoặc Burstable có limit lớn.
  • Exit 137: kiểm tra memory.events và dmesg trước khi nâng limit.
  • Dùng nodeAffinity chỉ để thử giả thuyết, gỡ sau khi thử.
  • Đặt cảnh báo node đầy RAM và tỉ lệ limits trên Allocatable.

Bài tiếp theo

Lỗi cert-manager khi cài đặt: ownership Helm, CRD v1beta1 và account key. Sau phần node đầy RAM, bài sau chuyển sang HTTPS: các lỗi hay gặp khi cài cert-manager và cấp chứng chỉ trên cluster.