Skip to content

API Priority and Fairness: Flow Schemas, Priority Levels

Vì Sao Quan Trọng Ở Production

Trước khi có API Priority and Fairness (APF), kube-apiserver chỉ có hai bộ đếm phẳng: --max-requests-inflight--max-mutating-requests-inflight — tổng số request đọc và ghi được xử lý đồng thời, không phân biệt request đó tới từ kubelet báo cáo trạng thái node hay từ một kubectl get thủ công của một kỹ sư đang debug. Ở cluster nhỏ điều này không thành vấn đề, nhưng ở cluster lớn, một client "ồn ào" (một controller lỗi gọi API loop vô hạn, hoặc một script debug quét toàn bộ resource) có thể chiếm hết token, khiến kubelet không gửi được heartbeat, kéo theo hàng loạt node bị đánh dấu NotReady dù thực chất node hoàn toàn khỏe mạnh — sự cố lan tầng kinh điển do thiếu cách ly.

APF giải quyết đúng vấn đề đó bằng cách chia tổng concurrency thành nhiều priority level cách ly lẫn nhau, và trong mỗi priority level, tiếp tục chia công bằng giữa các flow (nhóm request theo user/namespace) để một client đơn lẻ không thể chiếm trọn một priority level. Hiểu đúng cơ chế "seat" và "queue" là điều kiện tiên quyết để tune APF đúng — tune sai gây ra chính vấn đề mà APF được sinh ra để giải quyết: bóp nghẹt controller quan trọng trong khi có băng thông rảnh ở nơi khác.

Hai Khái Niệm Cấu Hình Lõi

PriorityLevelConfiguration: Hồ Chứa Concurrency Cách Ly

yaml
apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: PriorityLevelConfiguration
metadata:
  name: workload-high
spec:
  type: Limited
  limited:
    nominalConcurrencyShares: 40
    limitResponse:
      type: Queue
      queuing:
        queues: 64
        handSize: 12
        queueLengthLimit: 50

Mỗi PriorityLevelConfiguration định nghĩa một hồ chứa concurrency riêng biệt, độc lập với các priority level khác — đây chính là cơ chế cách ly. type: Exempt bỏ qua hoàn toàn mọi giới hạn (dùng cho request sống còn như health check hoặc chính etcd operation nội bộ); type: Limited bị giới hạn theo nominalConcurrencyShares.

FlowSchema: Bộ Phân Loại Request Vào Priority Level

yaml
apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: FlowSchema
metadata:
  name: workload-controllers
spec:
  matchingPrecedence: 800
  priorityLevelConfiguration:
    name: workload-high
  rules:
  - subjects:
    - kind: ServiceAccount
      serviceAccount: {name: my-controller, namespace: kube-system}
    resourceRules:
    - apiGroups: ["*"]
      resources: ["*"]
      verbs: ["*"]
  distinguisherMethod:
    type: ByUser

Mỗi request được đánh giá qua toàn bộ FlowSchema hiện có, theo thứ tự matchingPrecedence tăng dần (số càng nhỏ càng ưu tiên được xét trước) — FlowSchema đầu tiên khớp sẽ quyết định request thuộc priority level nào, các FlowSchema còn lại không còn được xét tới cho request đó nữa.

Nominal Concurrency Shares: Không Phải Số Tuyệt Đối, Mà Là Tỷ Lệ

Điểm hay bị hiểu lầm nhất: nominalConcurrencyShares không phải một con số concurrency cứng — nó là tỷ trọng trong tổng concurrency của toàn API server:

Tổng concurrency = --max-requests-inflight + --max-mutating-requests-inflight
Giới hạn của một priority level = (shares của level đó / tổng shares mọi level) × Tổng concurrency

Ví dụ: tổng concurrency 400, priority level workload-highshares: 40 trên tổng 100 shares toàn hệ thống → giới hạn cứng cho level này là (40/100) × 400 = 160 request đồng thời. Điều quan trọng hơn nữa: các priority level dưới mức sử dụng được phép "cho mượn" concurrency chưa dùng tới cho các level khác đang quá tải, theo cơ chế điều chỉnh động dựa trên utilization thực tế — nghĩa là trong điều kiện tải thấp, một priority level có thể tạm thời vượt quá giới hạn danh nghĩa của nó nếu các level khác đang rảnh. Đây là lý do gọi là "nominal" (danh nghĩa) chứ không phải giới hạn cứng tuyệt đối tại mọi thời điểm — cơ chế cho mượn giúp tận dụng tối đa tài nguyên trong điều kiện bình thường, nhưng vẫn đảm bảo mỗi level có sàn tối thiểu được bảo vệ khi tranh chấp xảy ra đồng thời.

Kubernetes tạo sẵn một bộ PriorityLevelConfiguration/FlowSchema mặc định không thể xóa, phản ánh đúng triết lý cách ly theo mức độ quan trọng của actor:

Priority LevelShares mặc địnhActor điển hình
system1Leader election, hoạt động nội bộ cấp thấp
kube-controller-manager18Bộ controller lõi của cluster
kube-apiserver18Chính API server (self-request)
kubelet12Toàn bộ kubelet trong cluster
workload-high40Namespace hệ thống, controller quan trọng
workload-low5Batch job, request ít nhạy cảm về độ trễ
catch-all6Bất kỳ request nào không khớp FlowSchema nào khác

Việc workload-high có shares gấp nhiều lần kubelet phản ánh chủ đích thiết kế: cách ly không đồng nghĩa với chia đều — hệ thống ưu tiên đảm bảo request kiểm soát trạng thái cluster (workload controller) không bị đói, đồng thời vẫn dành sàn tối thiểu chắc chắn cho kubelet heartbeat dù workload-high có bị quá tải tới đâu.

Seat: Đơn Vị Concurrency, Không Phải "Một Request = Một Slot"

Đây là điểm tinh vi nhất và thường bị hiểu sai. APF không coi mọi request là ngang nhau về chi phí — một request LIST trả về năm triệu object rõ ràng tốn tài nguyên xử lý (CPU serialize, băng thông) lớn hơn nhiều một request GET đơn lẻ, dù cả hai chỉ tính là "1 request" nếu đo bằng bộ đếm inflight cũ.

APF giải quyết bằng khái niệm seat — số đơn vị concurrency mà một request chiếm dụng, ước lượng dựa trên chi phí xử lý dự kiến:

  • Request thông thường (Get/Create/Update/Delete): chiếm 1 seat.
  • Request List có kích thước phản hồi lớn: chiếm ceil(kích thước phản hồi ước tính / 2^20) seat — request LIST trả về càng nhiều object càng chiếm nhiều seat, phản ánh đúng chi phí thực tế của việc serialize một danh sách khổng lồ.
  • Request Watch: chiếm seat trong giai đoạn bùng nổ thông báo ban đầu (initial burst khi mới mở watch, lúc client cần nhận toàn bộ state hiện có) — sau đó gần như không tốn thêm seat cho việc duy trì kết nối stream ở trạng thái nhàn rỗi.

Ý nghĩa thực hành của cơ chế seat: giới hạn nominalConcurrencyShares thực chất giới hạn tổng "khối lượng công việc", không giới hạn số lượng request đếm đầu người. Một priority level có giới hạn 160 seat có thể đang phục vụ 160 request nhỏ đồng thời, hoặc chỉ vài request LIST khổng lồ đã chiếm gần hết 160 seat đó — hai kịch bản này tạo cảm giác "đầy" hoàn toàn khác nhau nếu chỉ nhìn vào số lượng request thô.

Fair Queuing: Shuffle Sharding Để Cách Ly Giữa Các Flow Trong Cùng Priority Level

Cách ly giữa các priority level chưa đủ — nếu 100 người dùng cùng gửi request vào chung catch-all, một người dùng gửi request dồn dập vẫn có thể chiếm phần lớn concurrency của level đó, làm đói những người dùng còn lại trong cùng level. distinguisherMethod (ByUser, ByNamespace, hoặc bỏ trống nghĩa là mọi request coi như cùng một flow) chia request trong một priority level thành các flow riêng biệt, rồi dùng shuffle sharding để gán mỗi flow vào một tập con ngẫu nhiên các hàng đợi (queues), với số lượng hàng đợi được chọn mỗi lần là handSize.

Lý do dùng shuffle sharding thay vì hash trực tiếp theo modulo là để giảm xác suất hai flow "ồn ào" cùng va vào chung một tập hàng đợi hoàn toàn trùng khớp: với hash trực tiếp, hai flow trùng modulo sẽ luôn cạnh tranh trực tiếp trên đúng một hàng đợi mãi mãi; với shuffle sharding, mỗi flow được gán một tập con hàng đợi (kích thước handSize) rút ngẫu nhiên nhưng ổn định (deterministic theo flow ID) từ tổng số queues — xác suất hai flow có tập con trùng khớp hoàn toàn giảm theo cấp số nhân khi queueshandSize đủ lớn, nên phần lớn trường hợp một flow ồn ào chỉ ảnh hưởng tới các flow khác cũng vô tình rơi vào đúng những hàng đợi đó, không ảnh hưởng toàn bộ priority level.

Khi hàng đợi của một flow đầy (vượt queueLengthLimit, mặc định 50 request đang chờ trên mỗi flow), request mới của flow đó bị từ chối ngay với:

HTTP/1.1 429 Too Many Requests
Retry-After: <giây>
{
  "status": "Failure",
  "message": "Request flow control rate limited due to priority level limit",
  "reason": "Forbidden",
  "code": 429
}

Nhận diện đúng nguyên nhân 429 này rất quan trọng khi debug: nó không đồng nghĩa API server hết tài nguyên toàn cục — nó chỉ nghĩa là flow cụ thể đó (một user, một namespace, hoặc "mọi request" nếu không có distinguisher) đã vượt giới hạn hàng đợi dành riêng cho nó, trong khi các flow khác, thậm chí cùng priority level, có thể hoàn toàn không bị ảnh hưởng.

Quan Sát Và Chẩn Đoán

APF phơi bày metric Prometheus chi tiết cho từng priority level và endpoint debug trực tiếp:

apiserver_flowcontrol_request_concurrency_limit{priority_level="..."}
apiserver_flowcontrol_request_queue_length_after_enqueue{priority_level="...", flow_schema="..."}
apiserver_flowcontrol_request_wait_duration_seconds{priority_level="..."}
apiserver_flowcontrol_rejected_requests_total{priority_level="...", reason="..."}
bash
kubectl get flowschemas
kubectl get prioritylevelconfigurations
kubectl describe flowschema workload-controllers
curl -k https://<control-plane>:6443/debug/api_priority_and_fairness/dump_requests

Khi debug một client đang bị 429, bước đầu tiên bắt buộc là xác định chính xác FlowSchema nào đang match request đó (qua matchingPrecedence) và giới hạn hàng đợi hiện tại của flow đó — không phải nhìn tổng thể "API server có quá tải không", vì hai câu hỏi này có thể có câu trả lời hoàn toàn trái ngược nhau cùng lúc.

Ràng Buộc Và Failure Mode Ở Scale

  • Custom FlowSchema với matchingPrecedence đặt sai có thể vô tình chặn trước một FlowSchema mặc định quan trọng hơn (ví dụ chặn trước kube-controller-manager), khiến traffic lẽ ra được cách ly cẩn thận lại rơi vào catch-all chung với mọi thứ khác — đây là lỗi cấu hình khó phát hiện vì cluster vẫn hoạt động bình thường cho tới khi có đợt tải cao đồng thời từ nhiều nguồn.
  • Đặt nominalConcurrencyShares quá cao cho một priority level tùy chỉnh (ví dụ cấp riêng cho một Operator nội bộ) không tăng tổng concurrency của cluster — nó chỉ lấy bớt từ ngân sách của các priority level khác (bao gồm cả các level mặc định), vì tổng shares luôn được chia trên cùng một --max-requests-inflight cố định. Đây là hiểu lầm phổ biến: đội ngũ nghĩ "tăng shares cho service của mình" là một hành động cô lập, nhưng thực chất nó tái phân bổ toàn bộ ngân sách concurrency của cluster.
  • Request LIST không dùng limit/continue (xem phần Watch Mechanism) trên resource lớn chiếm nhiều seat hơn cần thiết trong khoảng thời gian dài hơn cần thiết, làm giảm concurrency khả dụng cho các request khác cùng priority level — một nguyên nhân gián tiếp nhưng thực tế của tình trạng 429 mà thoạt nhìn không liên quan gì tới chính request đang bị từ chối.
  • Không set distinguisherMethod khiến toàn bộ request khớp một FlowSchema bị coi là một flow duy nhất — mất hoàn toàn khả năng cách ly giữa các user/namespace trong cùng priority level, dù bản thân priority level đó vẫn được cách ly đúng với các level khác. Đây là cấu hình dễ mắc khi copy nguyên mẫu FlowSchema mà quên điều chỉnh cho đúng ngữ cảnh actor đa dạng.

References