Có hai loại tài nguyên, và bản chất vật lý của chúng quyết định số phận của cái vượt trần. Loại chia nhỏ được — như thời gian CPU hay băng thông — thì khi hết phần, bạn chỉ bị làm chậm lại: cho ăn ít hơn, đợi lượt sau. Loại tất cả-hoặc-không — như bộ nhớ — thì không có "một nửa": xin thêm mà không có thì cách duy nhất là bị giết. requests và limits trông như một cặp tham số, nhưng chúng là hai cơ chế khác nhau, và CPU với bộ nhớ lại được xử lý theo đúng hai số phận trên. Bài này đo cả bốn tổ hợp.

Vượt CPU: bị bóp, không bị giết

Một vòng lặp bận dùng 100% CPU, đổi limits.cpu:

limits.cpu Chu kỳ bị bóp Tỉ lệ RESTARTS
100m 360/360 100% 0
500m 406/406 100% 0
1 33/348 10% 0

Với limit=100m, container bị bóp ở mọi chu kỳ — nó chỉ được chạy 10% thời gian. Nhưng nó vẫn Running, và RESTARTS vẫn là 0. Kernel dùng cgroup cpu.max để cấp một hạn ngạch cho mỗi chu kỳ 100 mili giây. Hết hạn ngạch thì tiến trình bị đình lại tới chu kỳ sau.

Vượt CPU không bao giờ làm container chết. Nó chỉ làm mọi thứ chậm lại — và chậm theo cách rất khó chẩn đoán, vì mọi chỉ số sức khoẻ đều xanh.

Vượt bộ nhớ: bị giết

Cùng cụm, giới hạn memory: 128Mi, ứng dụng cấp phát dần:

pod: 0/1   OOMKilled   RESTARTS=2
lần trước kết thúc: OOMKilled exitCode=137

RESTARTS tăng đều — 2 rồi 3. Bộ nhớ không bóp được: khi tiến trình xin thêm mà cgroup không cho, cách duy nhất là giết nó. Đây là khác biệt cốt lõi và nó quyết định cách bạn đặt hai loại giới hạn.

requests và limits làm hai việc khác nhau

requests — con số bộ lập lịch cộng lại để chọn node (phần 47), và là con số quyết định thứ tự bị đuổi khi node cạn tài nguyên. Nó không giới hạn gì lúc chạy.

limits — con số kernel áp lúc chạy. CPU thì bóp, RAM thì giết.

Phần 47 đã đo hệ quả của vế đầu: node có 16 CPU nhưng pod requests: cpu: "10" thì chỉ một pod mỗi node — dù nginx thật sự dùng vài phần nghìn CPU. Bộ lập lịch không nhìn mức dùng thật.

Ba lớp QoS

Kubernetes tự gán lớp dựa trên quan hệ giữa requests và limits:

Pod Cấu hình Lớp
qos1 requests = limits Guaranteed
qos2 requests < limits Burstable
qos3 không khai gì BestEffort

Thứ tự bị đuổi khi node cạn RAM:

  1. BestEffort — đuổi đầu tiên
  2. Burstable — đuổi khi đang dùng vượt requests
  3. Guaranteed — đuổi cuối cùng

Không khai requests không phải là "linh hoạt". Nó là tự nhận vé chết trước.

Đây là lý do thực dụng nhất để khai requests, và nó thuyết phục hơn mọi lập luận về lập lịch.

Khuyến nghị

requests: đặt bằng mức dùng thật ở p95. Đo, đừng đoán:

kubectl top pods --containers

Quá cao thì lãng phí node; quá thấp thì pod bị đuổi sớm và bị tranh tài nguyên.

limits.memory: luôn đặt, và đặt rộng. Nó là lưới chặn rò rỉ bộ nhớ làm chết cả node. Đặt khoảng 1,5–2 lần mức dùng đỉnh — vì vượt là chết, không có cảnh báo.

limits.cpu: cân nhắc bỏ hẳn. Đây là khuyến nghị gây tranh cãi, và phép đo ở trên là lý do. Bóp CPU gây độ trễ mà không cứu ai cả: nếu node còn CPU rảnh, việc chặn pod dùng nó chẳng bảo vệ ai. Còn nếu node hết CPU thì kernel đã tự chia theo requests rồi — đó chính là việc cpu.weight làm.

Với requests khai đúng, bỏ limits.cpu cho ứng dụng dùng CPU rảnh khi cần, và vẫn được bảo đảm phần của mình khi node bận. Đổi lại, một pod xấu tính có thể ăn hết CPU rảnh — nên nó không phải quy tắc tuyệt đối, mà là đánh đổi đáng cân nhắc. Trường hợp limits.cpu nên có: môi trường nhiều bên dùng chung mà bạn cần chi phí đoán trước được, hoặc ứng dụng có lỗi làm CPU tăng vọt.

Bốn đơn vị hay nhầm

cpu: "1"      = 1 nhân
cpu: "500m"   = nửa nhân        (m = milli, 1000m = 1)
memory: "1Gi" = 1.073.741.824 byte   (Gi = 2^30)
memory: "1G"  = 1.000.000.000 byte   (G  = 10^9)

Gi và G chênh nhau 7%. Với giới hạn bộ nhớ, 7% là khoảng cách giữa "chạy được" và OOMKilled — và nó là lỗi gõ rất khó thấy khi đọc lại manifest.

LimitRange và ResourceQuota

Hai công cụ để không phụ thuộc vào việc mọi người nhớ khai:

apiVersion: v1
kind: LimitRange
metadata: {name: mac-dinh}
spec:
  limits:
    - type: Container
      default: {cpu: 500m, memory: 512Mi}          # limits nếu không khai
      defaultRequest: {cpu: 100m, memory: 128Mi}   # requests nếu không khai

LimitRange gán giá trị mặc định cho pod không khai — nghĩa là không còn pod BestEffort nào lọt vào namespace. ResourceQuota chặn trần cho cả namespace, và nó có một tác dụng phụ hữu ích: khi namespace có quota về CPU hoặc RAM, Kubernetes bắt buộc mọi pod phải khai requests tương ứng, nếu không sẽ từ chối tạo.

Mẫu số chung: tài nguyên nén được thì bóp, không nén được thì giết — và một cái trần chỉ có ích khi nó bảo vệ được ai đó

Bài này gói một phân biệt nền tảng dùng ở mọi hệ. Bản chất của một tài nguyên quyết định một cái trần hành xử ra sao: tài nguyên nén được (CPU, băng thông, tốc độ yêu cầu) khi chạm trần thì xuống cấp êm — bị bóp, chậm lại, xếp hàng; tài nguyên không nén được (bộ nhớ, đĩa, file descriptor, kết nối) thì chỉ có thể từ chối cứng — bị giết, ENOSPC, "too many open files", pool cạn. CPU vượt trần vẫn Running mà chậm; RAM vượt trần thì OOMKilled ngay. Và vì hai loại hỏng theo hai kiểu, bạn phải canh chúng theo hai cách: loại nén được hỏng âm thầm-chậm nên phải đo mức bị bóp (không có event nào báo); loại không nén được hỏng đột ngột nên phải đặt trần và bắt lỗi. Trong Java, cùng phân biệt: một thread pool có thể xếp hàng (nén được) trong khi heap thì OutOfMemoryError (cứng); băng thông thì backpressure, còn connection pool thì hoặc có kết nối hoặc ném lỗi.

Hệ quả trực tiếp là một quy tắc chống trực giác: một cái trần chỉ đáng đặt khi việc giữ lại tài nguyên bảo vệ được ai đó. limits.cpu bóp mà không cứu ai — CPU còn rảnh thì chặn chẳng lợi cho ai, CPU đã cạn thì kernel tự chia theo trọng số rồi. Trước khi thêm một giới hạn (rate-limit, timeout, khoá), hãy hỏi ai được bảo vệ nhờ nó — nếu câu trả lời là "không ai", cái trần đó chỉ tạo ra độ trễ.

Và một lưu ý phụ: requests với limits trông như một cặp nhưng lái hai cơ chế không liên quan (xếp lịch/đuổi so với áp-lúc-chạy) — đừng suy luận về chúng như một nút; cũng như Gi với G chênh 7% là khe hở đủ để lật "chạy được" thành "bị giết".

Mẫu số chung: phân loại tài nguyên là nén-được hay không, rồi áp trần và giám sát theo đúng kiểu hỏng của nó; và chỉ đặt một giới hạn khi bạn nêu được ai nó bảo vệ.

Muốn tự soi, tìm pod BestEffort trong cụm của bạn:

kubectl get pods -A -o custom-columns=\
NS:.metadata.namespace,TEN:.metadata.name,QOS:.status.qosClass --no-headers \
  | grep BestEffort

Mỗi dòng là một pod sẽ bị đuổi đầu tiên khi node cạn bộ nhớ — kể cả khi nó là thứ quan trọng nhất bạn đang chạy.

Phần sau đo OOMKilled kỹ hơn: ứng dụng không nhận được tín hiệu nào, và Kubernetes không sinh event nào.