iT邦幫忙

0

搞懂 Kubernetes Controller, Operator Pattern 和 client-go 的內部機制

  • 分享至 

  • xImage
  •  

最近在讀 Kubernetes controller,以下是我的摘要筆記:

Operator Pattern 是什麼

Operator Pattern 的核心只有一句話:

持續觀察某個資源的「期望狀態」(desired state)與「實際狀態」(actual state),並不斷讓實際狀態趨近期望狀態。

這個持續比對、修正的迴圈,叫做 reconcile loop。要實作這件事,通常需要兩塊拼圖:

  • CRD(Custom Resource Definition):定義一種新的資源類型,讓使用者可以用 kubectl apply 描述「我想要什麼」
  • Controller:一段持續跑在背景的程式,負責監聽這種資源的變化,並執行對應的動作
Operator = Controller + CRD

KubeRay 就是最典型的例子:它定義了 RayClusterRayJobRayService 這些 CRD,controller 則負責根據 CR 的 spec 去建立/管理對應的 Pod。

Argo CD 也是同樣的模式,只是它的「期望狀態」不是寫在 CR 的欄位裡,而是存在 Git repo 的 manifests 中,這就是 GitOps 的核心:把 Git 當作唯一真相來源(single source of truth),而 Argo CD 的 controller 持續比對 Git 內容跟 cluster 實際狀態,發現不一致就同步。可以說 GitOps = Operator Pattern + Git 作為期望狀態的來源

要實作一個 Go 語言的 controller,幾乎都會用到官方的 client-go library,它封裝了「怎麼跟 kube-apiserver 溝通」這件事。值得注意的是,client-go 本身不是 cluster 內建的東西,而是你程式裡引用的一個 library;apiserver 對外暴露的其實是標準 REST API,任何語言只要能發對應格式的 HTTP request 都能操作 K8s 資源(Python 有 kubernetes client、Java 有對應 SDK),client-go 只是 Go 語言的官方實作。

client-go 的核心架構

下圖是理解 client-go controller 機制最經典的一張圖,整體可以分成上下兩個區塊:

Kubernetes Controller
(Source: Writing Kubernetes Custom Controllers)

  • 上半部(client-go 內部):Reflector → Delta FIFO → Informer → Indexer,這一整套是自動化機制,你幾乎不用自己動手寫
  • 下半部(Custom Controller):Resource Event Handlers → Workqueue → Process Item → Handle Object,這是你需要實作業務邏輯的地方

1. Reflector:List & Watch

Reflector 是整條流程的起點,負責跟 Kubernetes API Server 要資料。它做兩件事:

  • List:先拿一次全部現有的物件(建立初始狀態)
  • Watch:之後持續監聽變化(新增/修改/刪除),而不是不斷輪詢(polling)

這個機制讓 controller 能即時得知資源變化,同時不會對 apiserver 造成過大壓力。

2. Delta FIFO Queue:不只是緩衝

Reflector 收到的每個變化會被包裝成一個 Delta,放進 Delta FIFO Queue。Delta FIFO 做的事情包含:

  • 解耦 (decouple) 生產者與消費者:讓 Reflector(收資料)跟 Informer(處理資料)可以用各自的節奏運作,不互相卡住
  • 保證處理順序 (Ordering guarantee):FIFO 確保事件按照發生順序被消費,避免「先處理了 Delete、後處理了 Update」這種邏輯錯亂
  • Deduplication:如果同一個物件在短時間內連續變化多次,Delta FIFO 會把同一個 key 的多筆 delta 合併,避免 queue 無限膨脹,也避免 controller 處理一堆過時、沒意義的中間狀態
  • 支援 resync/re-list:定期重新同步,確保即使 watch 連線斷過,本地狀態最終還是能跟 apiserver 對齊

3. Informer 與 Indexer:職責分離

Informer 從 Delta FIFO 把物件取出來後,會交給 Indexer 處理,同時把事件分派給 Resource Event Handlers。

為什麼 Informer 跟 Indexer 要拆成兩個獨立的元件?

因為兩者處理的是完全不同性質的問題:

Informer Indexer
職責 事件分派(Event Distribution) 儲存與索引(Storage & Indexing)
關心的問題 發生了什麼事、要通知誰 資料現在長怎樣、怎麼快速查到
存取模式 依序處理事件流(write path) 隨機查詢(read path)

把兩者拆開的好處:

  • Indexer 維護的 thread-safe store 不只服務 Informer,Controller 在後面的 Process Item 階段,可以直接透過 Indexer reference 查資料,完全不需要透過 Informer,彈性更高
  • 兩者可以獨立測試、獨立演化,互不干擾
  • 概念上更清楚:Informer 是「觀察者模式」,Indexer 是「快取層」

Indexer 把物件存進一個 thread-safe 的本地快取(in-memory cache),並建立索引。這一步是整個機制最省資源的關鍵:之後 Controller 查資料是查這份本地快取,不需要每次都打 API Server

4. Resource Event Handlers → Workqueue:從通知到處理

Informer 分派事件時,會呼叫你註冊的 AddFunc / UpdateFunc / DeleteFunc。這幾個 handler 有個重要限制:它們是在 Informer 的主 goroutine 裡被同步呼叫的,絕對不能在裡面做重活。如果在 handler 裡直接處理業務邏輯(例如呼叫外部 API、等待回應),會卡住整個事件分派流程,導致後續事件全部塞車。

所以正確的做法是:handler 只做一件事:把物件的 key 丟進 Workqueue,然後立刻返回。這裡故意只丟 key、不丟整個物件,是因為物件之後可能又變了,用 key 去查最新狀態(透過 Indexer)才準確。

Workqueue 存在的理由:

  • Deduplication:底層用 Set,同一個 key 重複 enqueue 多次,在還沒被取出處理前只會存在一份
  • Retry with backoff:處理失敗時可以用 exponential backoff 重新排入佇列,避免瘋狂重試打爆系統;成功後清除重試計數,讓 reconcile loop 具備自我修復能力
  • 支援平行處理:多個 worker goroutine 可以同時消費,同時保證同一個 key 不會被兩個 worker 同時處理
  • Rate limit:控制處理速率,避免短時間大量事件湧入時對下游系統造成過大壓力

5. Process Item / Handle Object:真正的業務邏輯

Worker 從 Workqueue 取出 key 後,進入實際的 reconcile 邏輯:

  1. 透過 Indexer reference,查出這個 key 目前快取的實際狀態(唯讀查詢)
  2. 跟期望狀態(通常來自 CR 的 spec 欄位)比較
  3. 如果不一致,採取行動讓兩者趨近一致
  4. 通常也會更新 CR 的 Status subresource(例如標記 Ready: true)

修改資源:另一條獨立的路徑

Indexer 是唯讀快取,不能拿來寫入資源。Informer/Indexer 這整套機制的用途只有「讓 controller 知道現在是什麼狀態」,完全不負責「怎麼改變狀態」。

真正修改資源時,Controller 會用 client-go 提供的 Clientset,直接對 apiserver 發 REST request:

clientset.CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{})
clientset.CoreV1().Pods(namespace).Update(ctx, pod, metav1.UpdateOptions{})

// 針對 CRD,例如 kuberay 的 RayCluster
rayClientset.RayV1().RayClusters(namespace).Update(ctx, rc, metav1.UpdateOptions{})

所以整條資料流其實是兩條平行路徑:

  • 讀路徑:apiserver → Reflector → Delta FIFO → Informer → Indexer(快取)→ Controller 讀取
  • 寫路徑:Controller → Clientset → 直接打 apiserver(Create / Update / Patch / Delete)

寫入生效後,這個改動最終會經由 Reflector 的 Watch 機制重新流回 Delta FIFO → Informer → Indexer,讓快取更新成最新狀態——形成一個完整的閉環,這正是 reconcile loop 能持續、可靠運作的原因。

小結

理解這套架構之後,再去讀任何 operator 的程式碼,不管是 kuberay、Argo CD,還是你自己想寫的 controller,骨架幾乎都大同小異: 一個 controller struct 裡通常同時持有 Lister/Indexer(唯讀查詢用)跟 Clientset(讀寫用)兩種引用,而所有的客製化邏輯,幾乎都濃縮在 Handle Object 這一小塊「比對期望與實際狀態」的程式碼裡。client-go 把跟 apiserver 溝通、快取管理、事件分派、重試機制都封裝起來了,讓開發者可以專注在業務邏輯的處理。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言