Ba lớp QoS nghe như một cái nhãn hành chính — "loại A, loại B, loại C" — dễ tưởng là quy ước mềm mà Kubernetes tự bịa ra. Nhưng bên dưới cái nhãn ấy là một con số kernel Linux thật, cộng thẳng vào phép tính chọn ai chết khi hết bộ nhớ. Biết con số đó biến một cuộc thảo luận mơ hồ về "ưu tiên" thành một bảng đo chính xác tới từng đơn vị. Phần 53 nêu ba lớp QoS và thứ tự bị đuổi; bài này đo con số kernel thật sự dùng để quyết định thứ tự đó.

Con số thật: oom_score_adj

Đọc /proc/1/oom_score_adj bên trong từng container:

Pod Cấu hình oom_score_adj
g-guar requests = limits = 300Mi -997
g-burst requests = 100Mi 994
g-best không khai gì 1000

Kernel tính điểm cho mỗi tiến trình khi hết bộ nhớ, và oom_score_adj cộng thẳng vào điểm đó. Điểm cao nhất chết trước. Guaranteed mang số âm — gần như không bao giờ được chọn. BestEffort mang 1000, giá trị cao nhất có thể. Đây không phải quy ước mềm của Kubernetes. Nó là con số kernel Linux thật sự dùng, và Kubernetes chỉ đặt nó khi tạo container.

Burstable: xin càng nhiều, càng khó bị giết

requests = 50Mi    ->  oom_score_adj = 997
requests = 100Mi   ->  oom_score_adj = 994
requests = 8Gi     ->  oom_score_adj = 488

Công thức:

oom_score_adj = 1000 - (1000 × requests.memory / RAM node)

Kiểm chứng trên node 16.747.192.320 byte:

1000 - 1000 ×    52.428.800 / 16.747.192.320  =  997
1000 - 1000 ×   104.857.600 / 16.747.192.320  =  994
1000 - 1000 × 8.589.934.592 / 16.747.192.320  =  488

Cả ba khớp chính xác con số đo được. Nghĩa là: requests.memory không chỉ dùng để bộ lập lịch chọn node (phần 47). Nó trực tiếp hạ điểm chết của pod trong kernel — pod xin nhiều được bảo vệ nhiều hơn, tỉ lệ với phần nó xin trên tổng RAM node. Điều này cũng giải thích tại sao Burstable xin rất nhiều (8Gi trên node 16 GB) có điểm 488 — thấp hơn cả một số cấu hình khác, dù nó không phải Guaranteed.

Ba lớp, và cách rơi vào từng lớp

Guaranteed — mọi container trong pod khai cả requests và limits cho cả CPU lẫn bộ nhớ, và requests = limits. Điều kiện chặt hơn nhiều người nghĩ. Chỉ cần một container trong pod thiếu limits.cpu là cả pod tụt xuống Burstable.

Burstable — có khai ít nhất một requests hoặc limits, nhưng không đủ điều kiện Guaranteed.

BestEffort — không khai gì cả.

kubectl get pod <ten> -o jsonpath='{.status.qosClass}'

Hệ quả thực dụng

Pod quan trọng  ->  requests = limits  ->  Guaranteed  ->  -997
Pod phụ trợ     ->  requests thấp      ->  Burstable   ->  gần 1000
Không khai gì   ->  BestEffort         ->  1000, chết đầu tiên

Với dịch vụ then chốt, đặt requests = limits là cách rẻ nhất để nó gần như không bao giờ bị chọn làm nạn nhân. Cái giá: bạn mất khả năng dùng vượt — limits bằng requests nghĩa là không có dư địa. Với dịch vụ phụ — công việc nền, xử lý theo lô — BestEffort là hợp lý. Chúng nên là thứ chết trước.

Cái không nên có là BestEffort do vô tình. Phần 53 đã nêu: đó là pod không ai khai requests, thường vì manifest được viết vội, và nó nhận vé chết trước mà không ai chọn cho nó. LimitRange (phần 53) chặn chuyện này ở mức namespace bằng cách gán giá trị mặc định.

Hai cơ chế dùng chung một thứ hạng

Thứ hạng QoS được dùng ở hai chỗ khác nhau, và phân biệt chúng quan trọng:

Kernel OOM killer — khi cgroup của node hết bộ nhớ. Dùng oom_score_adj như trên. Nhanh, không có cảnh báo, và pod bị giết tại chỗ (phần 54).

kubelet eviction — khi node chạm ngưỡng evictionHard. kubelet chọn pod theo lớp QoS và mức dùng vượt requests, rồi đuổi pod đi bằng cách xoá nó. Chậm hơn, có event Evicted, và pod được tạo lại ở node khác.

kubelet cố gắng đuổi trước khi kernel phải giết — đó là điểm khác biệt giữa một pod chuyển nhà và một pod bị bắn. Cùng thứ hạng, hai đường thoát khác nhau.

Đo mà không cần làm cạn node

Ép một node 16 GB cạn bộ nhớ để quan sát thứ tự đuổi là tốn kém và khó lặp lại. Đọc oom_score_adj cho cùng thông tin, trong một giây, và lặp lại được:

for p in $(kubectl get pods -o jsonpath='{.items[*].metadata.name}'); do
  echo "$p  $(kubectl get pod $p -o jsonpath='{.status.qosClass}')  \
$(kubectl exec $p -- cat /proc/1/oom_score_adj 2>/dev/null)"
done

Mẫu số chung: đo con số quyết định, và một nút thường có tác dụng thứ hai

Bài này gói hai bài học dùng ở mọi hệ. Thứ nhất, một mẹo đo lường đáng thành phản xạ: nếu hiện tượng khó dựng hoặc tốn kém để kích hoạt, hãy tìm và đo con số quyết định nó thay vì dựng lại hiện tượng. Ép một node 16 GB cạn RAM là bẩn và khó lặp; đọc oom_score_adj cho đúng cùng câu trả lời trong một giây, chính xác và không phá gì. Cùng nguyên tắc khắp nơi: muốn kiểm một race, đừng "chạy nghìn lần cầu may" mà soi thẳng thứ tự khoá; muốn biết cache đuổi gì, đọc chính sách và bộ đếm thay vì ép đầy; muốn xác nhận một timeout, đọc giá trị cấu hình thay vì ngồi chờ. Khi cái kết quả đắt hoặc chập chờn để tái hiện, hãy chuyển sang kiểm cái đầu vào tất định sinh ra nó — vừa nhanh, vừa chắc, vừa không hỏng môi trường.

Thứ hai: một tham số bạn chỉnh vì một lý do thường mang thêm một tác dụng thứ hai ít ai để ý. requests.memory nghe như chỉ để bộ lập lịch chọn node, nhưng nó đồng thời hạ điểm chết của pod trong kernel — pod xin nhiều được kernel bảo vệ nhiều hơn. Cùng bản chất với -Xmx của JVM (đặt trần heap và ảnh hưởng nhịp GC), với kích thước một thread pool (đặt mức song song và lượng bộ nhớ và hành vi hàng đợi), với fetch_size của một truy vấn CSDL (số vòng đi-về và bộ nhớ). Nguyên tắc: liệt kê mọi tác dụng của một nút trước khi vặn nó, vì cái mục đích được ghi trong tài liệu hiếm khi là tác dụng duy nhất.

Xen giữa là một lưu ý: cùng một thứ hạng có thể dẫn tới hai con đường hậu quả rất khác nhau — kernel giết tại chỗ (một phát, không cảnh báo) so với kubelet đuổi tử tế (có event, được tạo lại nơi khác); biết đường nào đang tác động mới đoán đúng mức nghiêm trọng.

Mẫu số chung: khi kết quả đắt để tái hiện, đo con số tất định quyết định nó; và trước khi chỉnh một tham số, liệt kê hết các tác dụng của nó — vì thứ bạn không biết nó làm mới là thứ làm bạn ngạc nhiên.

Muốn tự soi, xếp hạng pod của bạn theo mức độ dễ bị giết:

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

Nhóm BestEffort ở đầu danh sách là nhóm chết trước. Nếu có dịch vụ quan trọng nằm trong đó, đó là một dòng resources bị quên — và nó chỉ lộ ra vào lần node cạn RAM tiếp theo.

Phần sau đo Service và kube-proxy: một ClusterIP mà không tiến trình nào lắng nghe.