How the controller-runtime Cache Actually Works, and Why Your Controller Does Not Crash the API Server

Bài tóm tắt Kubernetes 2026 3. Quy mô lớn Độ khó: Khó

Kubernetes · 2026 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Bài giải thích cơ chế bên trong (blog của dự án Kubernetes), không phải câu chuyện của một công ty. Đặt ở đây vì nó cho thấy một quyết định kiến trúc nền của Kubernetes: đọc từ cache cục bộ cập nhật bằng luồng thay đổi.

Bối cảnh

  • Hiểu lầm phổ biến: r.Get()/r.List() trong reconciler gọi thẳng API server và luôn thấy trạng thái mới nhất. Thực tế ngược lại.
  • Nếu mỗi reconcile gọi API server, hàng chục controller × hàng trăm reconcile/giây sẽ làm sập API server và etcd. Vì vậy Kubernetes được dựng quanh mô hình watch từ đầu chứ không poll.

Cơ chế

  • List + watch: lấy snapshot một lần (kèm resourceVersion), rồi mở watch từ đúng version đó nên không hở khoảng nào giữa snapshot và luồng. Bản hiện nay gộp hai bước bằng streaming list (watch với sendInitialEvents=true, server phát sự kiện ADDED giả cho toàn bộ trạng thái hiện tại rồi chuyển sang thay đổi thật).
  • Reflector là thành phần duy nhất trong cache nói chuyện với API server. Mất kết nối thì nối lại từ version cuối; server trả 410 Gone (đã quá xa trong lịch sử) thì relist lấy snapshot mới. Relist chỉ xảy ra khi lỗi, không theo lịch.
  • Delta queue: trước là DeltaFIFO (gom delta theo key, có gộp); nay mặc định là RealFIFO (từ client-go 1.36 không bật lại cái cũ được): danh sách phẳng, giữ thứ tự toàn cục, mỗi lần lấy một delta, không gộp.
  • Indexer: kho trong bộ nhớ + các index, một khoá đọc/ghi chung. Store được cập nhật trước khi gọi handler, nên handler không bao giờ thấy store chậm hơn sự kiện của chính nó.
  • workqueue chỉ giữ key (namespace/name), có khử trùng lặp và giới hạn tốc độ: mười thay đổi liên tiếp của một object có thể thành một lần reconcile.
  • Controller chỉ bắt đầu reconcile khi mọi informer đã nạp xong và dựng xong index: không có chuyện "lần đầu chậm".
  • Đọc từ cache, ghi thẳng vào API server. Ghi qua cache sẽ sinh tình trạng cache tưởng đã ghi trong khi server từ chối. Ghi dùng resourceVersion làm optimistic concurrency: lệch thì 409 Conflict.

Lỗi hay gặp (theo bài)

  • Mong đọc ngay thấy cái vừa ghi: cache là eventually consistent. Đọc thẳng server cũng không chữa được tính đúng; phải thiết kế reconcile chịu được dữ liệu cũ (idempotent, dựa vào conflict).
  • Sửa object lấy từ cache dùng chung trong Predicate/EventHandler làm hỏng index của mọi controller khác; object trả từ Get/List là bản sao, sửa thoải mái.
  • Resync không phải relist: resync chỉ phát lại object đang có trong store, không gọi server.
  • List lọc theo trường mà không có index thành quét O(n): cụm 50.000 Pod, hàng trăm reconcile/giây, mỗi lần quét toàn bộ dưới khoá đọc và chặn luồng ghi vào store. Sửa bằng index ngược đăng ký trước (giá trị được tính từ object, không cần là trường thật); index được cập nhật ở mỗi sự kiện nên lúc List đã sẵn.

Kiểm soát bộ nhớ

  • Mặc định informer kéo mọi object của một loại trong phạm vi; Secret, ConfigMap, Event trong cụm lớn có thể tốn nhiều GB.
  • Các cần gạt: giới hạn namespace; selector lúc nạp cache (đẩy lọc về server, tiết kiệm cả mạng); metadata-only (PartialObjectMetadata) khi chỉ cần label/annotation, với Secret có thể giảm bộ nhớ một bậc; Transform bỏ trường nặng (riêng managedFields đã đáng kể); tắt cache cho một loại và đọc thẳng server (APIReader), đổi bộ nhớ lấy request mạng và chi phí giải mã.

Bài học cho SWE

  • "Đọc từ bản sao cục bộ được cập nhật bằng luồng thay đổi" là cách đỡ tải đọc cho một nguồn sự thật duy nhất; cái giá là dữ liệu có thể cũ và bộ nhớ ở mỗi client (Cache và invalidation, Scale dọc trước khi scale ngang).
  • Snapshot + luồng từ đúng version, có đường relist khi tụt quá xa: đây là mẫu chung cho mọi cache được đẩy cập nhật (giống CDC vào index search).
  • Hàng đợi theo key có gộp biến bão sự kiện thành ít việc; logic phải hội tụ về trạng thái mong muốn thay vì xử lý từng sự kiện.
  • Cache cục bộ không có index chỉ dời chi phí từ mạng sang CPU; bộ nhớ của cache phải được giới hạn chủ động.
  • Đọc nhiều lần qua API server là thứ đã làm OpenAI 2018 quá tải (agent giám sát poll apiserver); watch + cache là cách nền tảng tránh điều đó.

Liên quan