Skip to content

Webhook Failure Modes, Performance & Stability — Chống Sập Control Plane

Vì sao file này là file quan trọng nhất chương về vận hành

Webhook là cơ chế admission mạnh nhất nhưng cũng là nguyên nhân hàng đầu của các outage liên quan đến control plane. Lý do nằm ở một sự thật kiến trúc đơn giản nhưng dễ quên: webhook nằm trên đường ghi nóng (hot write path) và đồng bộ. Mỗi request CREATE/UPDATE khớp rules của một webhook đều phải dừng lại, gọi HTTP ra webhook server, và chờ response trước khi tiếp tục. Webhook không phải một bộ lọc bất đồng bộ chạy nền — nó là một bước chặn trong đường đi của mọi request ghi khớp rule.

Hệ quả là hai loại sự cố mà gần như mọi platform team đều gặp ít nhất một lần:

  1. Webhook chết làm chặn cluster. Webhook service down (deploy lỗi, hết Pod, node mất, cert hết hạn). Nếu failurePolicy: Fail và rule không loại trừ kube-system, API server bắt đầu từ chối mọi request khớp — kể cả Pod hệ thống cần để khôi phục chính webhook đó. Cluster rơi vào deadlock: không tạo được Pod để sửa webhook vì webhook đang chặn việc tạo Pod.
  2. Webhook chậm làm tăng latency toàn cluster. Webhook phản hồi chậm (gọi DB ngoài, GC pause, quá tải). Mọi request khớp rule phải chờ tới timeoutSeconds. p99 latency của API server tăng vọt, controller bị nghẽn, và do API Priority and Fairness các luồng request bắt đầu bị xếp hàng.

File này phân tích từng cơ chế và đưa ra các quyết định production để tránh hai loại sự cố trên.

failurePolicy: Fail vs Ignore — đánh đổi security với availability

failurePolicy quyết định API server làm gì khi không gọi được webhook thành công (lỗi mạng, timeout, response sai định dạng, cert không verify được) (failure policy):

  • Fail — coi việc webhook lỗi là từ chối request. Request bị chặn. An toàn về security (không có object nào lọt qua mà không được kiểm), nhưng nguy hiểm về availability (webhook chết = request chết).
  • Ignore — coi việc webhook lỗi là cho qua. Request được chấp nhận như thể webhook không tồn tại. An toàn về availability (webhook chết không chặn cluster), nhưng có lỗ hổng security (object có thể lọt qua trong lúc webhook lỗi).

Đây là một đánh đổi không có đáp án đúng tuyệt đối — nó phụ thuộc webhook đang bảo vệ điều gì:

Bản chất webhookKhuyến nghịLý do
Security tới hạn (chặn privileged, chặn image không tin cậy)Fail + loại trừ kỹ namespace hệ thống + HA mạnhThà chặn còn hơn để object nguy hiểm lọt qua
Mutation tiện ích (inject label, default)IgnoreObject thiếu mutation vẫn chạy được; không đáng đánh đổi availability
Compliance/audit (không chặn vận hành)Ignore hoặc audit-onlyMục tiêu là quan sát, không phải chặn

Nguyên tắc cốt lõi: failurePolicy: Fail chỉ an toàn khi webhook server thực sự HA và được loại trừ khỏi mọi path khôi phục. Nếu không đảm bảo được hai điều đó, Ignore là lựa chọn ít rủi ro hơn cho phần lớn webhook.

timeoutSeconds và tác động lên latency

timeoutSeconds đặt thời gian API server chờ webhook trả lời, mặc định 10 giây, tối đa 30 giây (timeouts). Khi hết timeout, hành vi tuân theo failurePolicy (Fail → chặn, Ignore → cho qua).

Sai lầm phổ biến là để mặc định 10s và nghĩ "webhook của tôi nhanh mà". Vấn đề là timeout không phải latency bình thường — nó là latency lúc webhook gặp sự cố. Khi webhook server quá tải hay GC pause, mọi request khớp rule sẽ chờ tới đúng timeoutSeconds. Với mặc định 10s:

  • Mỗi thao tác ghi khớp rule chậm thêm tới 10s khi webhook trục trặc.
  • Controller (Deployment, ReplicaSet...) tạo Pod hàng loạt sẽ bị nghẽn — mỗi Pod chờ 10s.
  • Hàng đợi request của API server đầy lên; APF bắt đầu từ chối/xếp hàng các luồng khác.

Khuyến nghị production: đặt timeoutSeconds ngắn (1–5s) cho webhook trên đường nóng. Latency budget phải tính ngược: nếu webhook nằm trên path tạo Pod và bạn cần Pod được tạo trong vài giây để autoscaling kịp (xem Chương 9), thì timeout 10s là không chấp nhận được. Một webhook đúng nghĩa "nhanh" nên trả lời trong vài chục mili-giây — timeout chỉ là van an toàn, không phải mục tiêu.

Vì sao webhook nằm trên "đường ghi nóng"

Cần nội tâm hóa: webhook không chạy nền và không bất đồng bộ. Sơ đồ đường đi:

Controller/kubectl  →  API server  →  [chờ HTTP round-trip tới webhook]  →  validate xong  →  ghi etcd

                                  toàn bộ request ghi đứng đây

Mỗi lần webhook được gọi, request ghi đang đợi. Điều này có vài hệ quả không hiển nhiên:

  • Webhook ảnh hưởng cả request không phải của bạn. Nếu webhook bắt pods, thì kube-controller-manager tạo Pod (cho mọi Deployment), Cluster Autoscaler, và mọi người dùng đều phải đi qua nó. Webhook chậm làm chậm tất cả.
  • Số webhook cộng dồn tuyến tính. Nếu một object khớp 5 mutating webhook, request chờ tổng latency của cả 5 (tuần tự). Reinvocation (file 2) có thể nhân thêm.
  • Webhook trên * là thảm họa tiềm ẩn. Bắt mọi resource nghĩa là mọi thao tác ghi trên cluster — kể cả Lease (dùng cho leader election, heartbeat) — đều qua webhook. Webhook chậm có thể làm hỏng leader election của các controller hệ thống.

Anti-pattern chí mạng

1. Bắt kube-system (và managed namespace của GKE)

Đây là sai lầm gây deadlock kinh điển. Tài liệu Kubernetes nói thẳng: không dùng admission webhook để validate/mutate object trong kube-system (avoiding deadlocks). Lý do: các thành phần khôi phục cluster (kube-dns, metrics-server, và chính webhook server của bạn nếu nó chạy trong cluster) thường nằm ở namespace hệ thống. Nếu webhook Fail chặn việc tạo Pod ở đó, bạn không khôi phục được.

Cách đúng: luôn thêm namespaceSelector loại trừ namespace hệ thống. Mọi namespace có label kubernetes.io/metadata.name (label tự động Kubernetes gắn cho mỗi namespace) nên dùng nó:

yaml
namespaceSelector:
  matchExpressions:
  - key: kubernetes.io/metadata.name
    operator: NotIn
    values: ["kube-system", "gke-managed-system", "gmp-system", "kube-node-lease"]

Trên GKE còn có thêm các managed namespace (gke-managed-*, gmp-system cho Managed Prometheus...) — loại trừ chúng để không can thiệp vào thành phần Google quản lý.

2. Webhook tự validate chính cấu hình của nó

Nếu webhook bắt validatingwebhookconfigurations hoặc các object hạ tầng của chính nó, bạn tạo vòng phụ thuộc: để sửa webhook phải qua webhook, mà webhook đang hỏng. Dùng objectSelector để webhook bỏ qua các object có label của chính nó.

3. failurePolicy: Fail mà webhook không HA

Webhook server chạy 1 replica, không PodDisruptionBudget, cùng node với workload. Node mất → webhook mất → (nếu Fail) cluster mất khả năng ghi. Fail đòi hỏi webhook server phải nhiều replica, trải nhiều node/zone, có PDB, và readiness probe đúng.

4. Webhook gọi đồng bộ một hệ thống ngoài chậm

Validating webhook gọi một scanner CVE hay một API duyệt nội bộ ngay trong đường admission. Khi hệ thống ngoài chậm, mọi request ghi chậm theo. Nếu phải làm vậy: cache kết quả tích cực, timeout cực ngắn, và cân nhắc kiến trúc bất đồng bộ (scan trước ở CI, webhook chỉ tra kết quả đã có).

Chiến lược giữ control plane ổn định

Tổng hợp các thực hành để webhook không trở thành điểm sập:

  1. Loại trừ namespace hệ thống qua namespaceSelector — không thương lượng.
  2. Thu hẹp rules tới đúng resource + operation cần thiết. Không *.
  3. HA webhook server: ≥2 replica, trải zone, PDB, readiness probe phản ánh đúng khả năng phục vụ.
  4. Timeout ngắn (1–5s) cho path nóng; latency mục tiêu vài chục ms.
  5. Chọn failurePolicy theo bản chất: Fail chỉ cho security tới hạn + HA mạnh; Ignore cho phần còn lại.
  6. Caching kết quả tính toán đắt; tránh gọi ngoài đồng bộ.
  7. Quan sát: theo dõi metric apiserver_admission_webhook_admission_duration_secondsapiserver_admission_webhook_rejection_count (file 9) để phát hiện webhook chậm/chối nhiều trước khi thành outage.
  8. Break-glass: có sẵn runbook xóa nhanh WebhookConfiguration khi nó gây outage. Vì WebhookConfiguration là object thường, kubectl delete validatingwebhookconfiguration <name> gỡ chặn ngay — nhưng phải đảm bảo người vận hành có quyền và biết tên webhook trước khi sự cố xảy ra.

Latency budget: tính ngược từ SLO của control plane

"Webhook nên nhanh" là lời khuyên vô dụng nếu không có con số. Cách kỹ sư hệ thống nghĩ về nó là latency budget: control plane có một ngân sách độ trễ cho thao tác ghi (ví dụ SLO p99 tạo Pod < 1s), và mỗi webhook trên path đó tiêu một phần ngân sách. Tính ngược:

  • Nếu Pod tạo phải xong trong ~1s p99 để autoscaling kịp phản ứng spike (xem Chương 9), và có 3 webhook trên path tạo Pod, mỗi webhook chỉ được tiêu ~vài chục ms ở p99 — không phải 10s.
  • timeoutSeconds không phải latency mục tiêu; nó là van an toàn cho trường hợp webhook hỏng. Đặt nó ngắn (1–3s) để khi webhook trục trặc, thiệt hại bị giới hạn. Latency bình thường phải thấp hơn nhiều bậc.
  • Webhook mutating cộng dồn (gọi tuần tự, file 2); validating chạy song song. Khi tính budget, cộng latency các mutating webhook nhưng lấy max các validating webhook.

Một sai lầm phổ biến là đo latency webhook lúc bình thường (vài ms) và yên tâm, mà quên đo lúc webhook quá tải hoặc downstream chậm — đó mới là lúc nó tiêu hết timeout và kéo cả cluster. Load test webhook ở tải cao, đo p99/p999, và đặt timeout dựa trên hành vi đuôi, không phải trung vị.

Caching strategies: cắt round-trip và gọi ngoài

Khi webhook phải thực hiện công việc đắt (gọi một API duyệt, tra một policy phức tạp, verify chữ ký image), caching là đòn bẩy hiệu năng chính:

  • Cache kết quả theo input ổn định. Nếu quyết định chỉ phụ thuộc một vài field (ví dụ image digest), cache theo key đó. Cùng image được duyệt nhiều lần chỉ tốn một lần gọi downstream. Đặt TTL hợp lý để cân bằng độ tươi và hiệu năng.
  • Cache phủ định (negative cache) cẩn thận. Cache cả kết quả "từ chối" giúp không gọi lại downstream cho object đã biết là sai — nhưng TTL phải ngắn để policy mới có hiệu lực kịp.
  • Warm cache / prefetch. Với webhook verify image, có thể đồng bộ trước danh sách image được duyệt vào bộ nhớ webhook (qua một informer/watch), biến mỗi quyết định thành tra cứu local — không round-trip nào lúc admission. Đây là kiến trúc lý tưởng: webhook không gọi ngoài đồng bộ trong đường admission, chỉ tra cache đã đồng bộ nền.
  • In-memory cache trong từng replica webhook, không phải cache chia sẻ qua mạng — vì cache chia sẻ lại thêm một round-trip. Mỗi replica giữ cache riêng, chấp nhận trùng lặp để đổi lấy độ trễ tối thiểu.

Nguyên tắc bao trùm: không gọi hệ thống ngoài đồng bộ trong đường admission nếu tránh được. Mọi thứ có thể đồng bộ trước (qua watch/informer/prefetch) nên được đồng bộ trước, để quyết định admission chỉ là phép tính trong bộ nhớ. Nếu buộc phải gọi ngoài, timeout cực ngắn + failurePolicy: Ignore cho phần không-bảo-mật-tới-hạn là van an toàn.

Real-world scenario bổ sung: webhook chậm làm nghẽn leader election

Một webhook bắt resources: ["*"] (mọi loại tài nguyên) để gắn label audit, failurePolicy: Ignore, timeout mặc định 10s. Webhook server gặp GC pause định kỳ kéo dài ~8s. Vì rule bắt *, nó cũng chặn các thao tác ghi lên Lease — đối tượng mà controller hệ thống dùng cho leader election và heartbeat. Mỗi lần GC pause, mọi cập nhật Lease chậm tới ~8s; một số controller tưởng leader đã chết, kích hoạt re-election, gây flapping leadership và reconcile bị gián đoạn trên toàn cluster — dù failurePolicy: Ignore nghĩa là không request nào bị chặn.

Bài học kép: (1) Ignore chống được việc chặn nhưng không chống được latency — webhook chậm vẫn làm chậm mọi request khớp tới khi timeout; (2) resources: ["*"] là gần như luôn sai — thu hẹp tới đúng loại tài nguyên cần. Sửa: đổi rule thành chỉ các loại cần label, loại trừ Lease/Event và các tài nguyên tần suất cao, giảm timeout xuống 2s, và sửa GC pause của webhook server (tăng heap, đổi GC tuning).

Cân nhắc lớn hơn: có cần webhook không?

Nhiều failure mode trong file này biến mất hoàn toàn nếu dùng ValidatingAdmissionPolicy (CEL, in-process — file 8) thay vì webhook: không có service để chết, không có cert để hết hạn, không có round-trip để timeout. Quy tắc: chỉ dùng webhook khi thực sự cần mutation phức tạp hoặc gọi hệ thống ngoài; mọi policy "field này phải thỏa điều kiện kia" nên là CEL policy. Đây là xu hướng kiến trúc rõ ràng của Kubernetes hiện đại.

Real-world scenario

Sự cố deadlock điển hình và cách thoát

Một team triển khai webhook chặn image không đến từ registry nội bộ, failurePolicy: Fail, rule bắt pods mọi namespace, webhook chạy 1 replica trong namespace security. Một đêm node chạy webhook bị node auto-repair thay (xem Chương 6). Webhook Pod chưa kịp reschedule. Trong khoảng đó:

  • Mọi Pod mới (kể cả webhook Pod đang cố reschedule, vì nó cũng tạo Pod trong namespace bị webhook bắt) bị từ chối.
  • ReplicaSet controller không tạo được Pod thay thế cho webhook.
  • Deadlock: cần Pod webhook để cho phép tạo Pod, nhưng không tạo được Pod webhook.

Thoát hiểm: vận hành viên xóa validatingwebhookconfiguration → API server ngừng gọi webhook → Pod webhook được tạo lại → áp lại WebhookConfiguration. Bài học rút ra và áp dụng: thêm namespaceSelector loại trừ kube-system namespace security (để webhook tự khôi phục được), tăng lên 3 replica trải zone, PDB minAvailable: 2, timeout 3s. Phòng xa hơn: chuyển phần "chặn image" sang Binary Authorization (Chương 34) — cơ chế GKE-native không có failure mode kiểu webhook.

Common mistakes / anti-patterns

  • Để timeoutSeconds mặc định 10s trên path nóng — chấp nhận latency 10s mỗi khi webhook trục trặc. Đặt ngắn.
  • failurePolicy: Fail như mặc định cho mọi webhook — chỉ dùng khi thực sự cần và đã HA. Phần lớn nên Ignore.
  • Không có runbook break-glass — khi outage xảy ra, vận hành viên không biết webhook nào gây ra, không có quyền xóa. Chuẩn bị trước.
  • Webhook server cùng node/zone với workload nó bảo vệ — một sự cố node kéo cả hai xuống cùng lúc.

Official references