為什麼 Cilium 需要 Startup Taint,VPC CNI 不需要?[Karpenter Startup Taint]

說明 Node Ready 的判定機制、Karpenter startupTaints,以及 Cilium 與 VPC CNI 在節點初始化上的差異。

發佈 ~8 分鐘 #karpenter#K8s

Why this issue?

回顧一下我在 Cilium debug 紀錄時遇到的問題:

當集群需要自動擴容時,Karpenter 開出來的新節點上沒有 /etc/cni/net.d/05-cilium.conflist,也沒有跑 Cilium Pod,導致 Pod 一直卡在 Pending。

原因:沒有加上 node.cilium.io/agent-not-ready=true:NoSchedule 的 taint。

👉 解法:在 NodePool 加入 startupTaints,確保新節點在 Cilium 還沒 ready 前不會被排程:

startupTaints:
  - key: node.cilium.io/agent-not-ready
    value: "true"
    effect: NoSchedule

如果加在 taints 的話,Karpenter 會因為 cilium 沒辦法把 node.cilium.io/agent-not-ready=true:NoSchedule 移除,導致誤判所有 pod 都無法 schedule,導致無法順利開啟新的 node,因此要加在 startupTaints

延續這個問題深入探究的話,會發現 K8s 本身只支援 Taint 這個關鍵字,那麼 startupTaints 這個機制,Karpenter 又是怎麼辦到的呢?


1. Kubernetes 原生節點初始化流程

1.1 Node Ready 狀態由誰判定?

在深入討論節點初始化之前,我們必須先理解 Node Ready 狀態是如何判定的。

判定者:Kubelet + Node Lifecycle Controller

Node Ready 狀態的判定是一個雙向通訊的過程:

┌─────────────────┐              ┌──────────────────────┐
│                 │  Heartbeat   │                      │
│     Kubelet     │─────────────>│ Node Lifecycle       │
│   (on Node)     │   Status     │ Controller           │
│                 │<─────────────│ (Control Plane)      │
└─────────────────┘  Updates     └──────────────────────┘

重要概念:雖然看起來像是控制平面主動檢查節點,但實際上是節點(kubelet)主動向控制平面報告狀態。

對 Kubelet 而言 Node Ready 取決於:

Kubelet 會定期執行以下檢查,並將結果報告給控制平面:

  1. 容器運行時網路未就緒 (NetworkReady=false):/etc/cni/net.d/ 是否有 cni 設定檔
  2. CSI 提供者未初始化 (CSINode):檢查 CSINode 對象是否已創建和初始化
    • 源碼:pkg/volume/csi/csi_plugin.go
  3. **容器運行時狀態檢查不完整:**確保 container runtime 正常運行
    • 源碼:pkg/kubelet/runtime.go
  4. Pod 生命週期事件生成器宕機 (PLEG):PLEG 負責監控 Pod 狀態變化
    • 源碼:pkg/kubelet/kubelet.go
  5. **節點正在關閉:**檢測節點是否處於 shutdown 狀態
    • 源碼:pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go
  6. **缺少 CPU、記憶體或 max pods 容量信息:**確保資源信息已正確收集
    • 源碼:pkg/kubelet/nodestatus/setters.go

Node Lifecycle Controller 的判定邏輯

Node Lifecycle Controller 接收 kubelet 的報告後,將 Node 的 Ready condition 設置為以下三種狀態之一:

狀態條件說明
True所有檢查通過節點健康,可以接受 Pod 調度
False一個或多個檢查失敗節點不健康,不接受新 Pod
Unknown超過 grace period 未收到心跳節點失聯(默認 50 秒)

1.2 節點註冊與初始化順序

理解了 Node Ready 的判定機制後,我們來看完整的初始化流程:

1. 節點註冊 (kubelet 啟動)
   └─ Kubelet 開始執行健康檢查
   └─ 帶有 node.kubernetes.io/not-ready taint
   └─ 如果使用 external cloud provider,帶有 node.cloudprovider.kubernetes.io/uninitialized

2. Cloud Controller Manager 初始化雲端資源
   └─ 配置雲端 metadata、instance 資訊
   └─ 移除 node.cloudprovider.kubernetes.io/uninitialized ✓

3. CNI DaemonSet 部署並初始化網路
   └─ 可能帶有 node.kubernetes.io/network-unavailable taint
   └─ Kubelet 檢查:runtime network not ready ⚠️
   └─ CNI 配置完成後,kubelet 檢查通過 ✓
   └─ 移除網路相關 taint ✓

4. 節點變為 Ready
   └─ Kubelet 所有檢查通過 ✓
   └─ Node Lifecycle Controller 設置 Ready=True
   └─ 移除 node.kubernetes.io/not-ready taint ✓
   └─ 可以接受一般的工作負載

1.3 Cloud Provider 的初始化 Taint

在使用外部雲端提供商(external cloud provider)的場景下,Kubernetes 有一個內建的初始化機制:

  • Taint: node.cloudprovider.kubernetes.io/uninitialized:NoSchedule
  • 設置者: kubelet(當啟動參數包含 -cloud-provider=external 時)
  • 移除者: cloud-controller-manager
  • 用途: 確保節點在雲端資源配置完成前不接受 Pod 調度

這個 taint 的移除由 cloud-controller-manager 中的控制器負責,而非 DaemonSet。

1.4 Kubernetes 原生沒有統一的 “startup taint” 概念

需要注意的是,Kubernetes 本身並沒有通用的 “startup taint” 機制。上述的 taint 都是由特定組件(kubelet、cloud-controller-manager)管理的,並且有明確的職責範圍:

  • kubelet 負責管理節點生命週期相關的 taint(如 not-ready、unschedulable)
  • cloud-controller-manager 負責雲端初始化相關的 taint
  • CNI 插件可能會管理網路相關的 taint(如 network-unavailable)

2. Karpenter 的節點初始化機制

2.1 Karpenter 的 startupTaint 功能

Karpenter 引入了一個自訂的 startupTaint 功能,這是 Karpenter 自己實作的機制,而非 Kubernetes 原生支援。

特性

  1. **配置位置:**NodePool 的 .spec.template.spec.startupTaints
  2. **用途:**允許在節點初始化期間設置臨時 taint,常用於 DaemonSet 的啟動順序控制
  3. **重要特性:**startupTaints 在調度決策時會被忽略,也就是說 Pod 不需要容忍這些 taint,Karpenter 仍會為它們配置節點
  4. Karpenter 只負責添加,不負責移除
apiVersion: karpenter.sh/v1
kind: NodePool
spec:
  template:
    spec:
      startupTaints:
      - key: example.com/init-required
        effect: NoSchedule
        value: "true"

2.2 Karpenter 的節點初始化判定條件

Karpenter 使用三個因素來確定節點是否已初始化:

  1. 節點就緒狀態(Node Readiness)
    • 檢查 Ready 條件類型,期望值為 True
  2. 預期資源已註冊(Expected Resources Registered)
    • Karpenter 從 ec2:DescribeInstanceTypes 獲取實例類型的所有預期資源
    • 檢查這些資源是否正確註冊到 node.status.allocatable,且數量不為零
    • 常見問題資源:
      • nvidia.com/gpu: GPU 實例但缺少 device plugin
      • vpc.amazonaws.com/pod-eni: Security Groups for Pods 功能未啟用
  3. NodePool Startup Taints 已移除
    • 檢查 .spec.template.spec.startupTaints 中定義的所有 taint
    • 只有當這些 taint 完全從 node.spec.taints 中移除時,節點才被視為已初始化

3. 實際案例:VPC CNI vs Cilium

VPC CNI 與 Kubelet 的網路就緒檢查緊密耦合,Node Ready 狀態會直接等待 VPC CNI 初始化完成。Cilium 則獨立於此檢查機制,可能在 Cilium 完全就緒前就被標記為 Ready。

TL;DR

因為 cilium 的初始化時間比 vpc-cni 長,有可能寫入 node cni 設置檔時,cilium 還沒初始化完成,因此需要用 startupTaint 來控制 Node Ready 的狀態

3.1 為什麼 VPC CNI 不需要 startupTaint?

因為在使用 VPC CNI 的情況下,網路就緒就等於 Node Ready

Kubelet 會檢查容器運行時網路是否就緒:

  1. 檢查失敗 → Kubelet 報告 NetworkReady=false,CNI 插件未初始化
  2. Node 不 Ready → Node Lifecycle Controller 設置 Ready=False,節點帶上 node.kubernetes.io/not-ready:NoExecute taint
  3. VPC CNI 初始化 → DaemonSet 配置 ENI 和 IP 地址
  4. /etc/cni/net.d/ 有 cni 配置檔,檢查通過 → Kubelet 網路檢查成功
  5. Node 變 Ready → Node Ready 狀態為 True

因此 VPC CNI 不需要額外的 startupTaint,Kubelet 的檢查本身就充當了「等待門卡」。

3.2 為什麼 Cilium 需要 startupTaint?

Cilium 的初始化涵蓋更多步驟(載入 BPF 程式、設置網路策略、與其他節點連接),且某些配置下(如 hostNetwork: true)Kubelet 的網路檢查可能提早通過。這導致節點被標記為 Ready,但 Cilium Agent 仍在初始化中。

事件順序:

  1. Cilium DaemonSet 啟動
  2. 開始初始化(載入 eBPF 程式、連接集群等)
  3. 寫入配置檔案到 /etc/cni/net.d/05-cilium.conflist ⚠️
  4. Kubelet 檢測到配置檔案存在 → NetworkReady=true → Node Ready
  5. 但 Cilium 仍在進行第 2 步的複雜初始化…,因此要用 startupTaint 來限制 Node Ready 的狀態
Cilium 的 taint 管理

Cilium Agent 會在初始化完成後自動移除 node.cilium.io/agent-not-ready taint。這是 Cilium 的內建功能,但前提是 Cilium 的 ServiceAccount 必須有正確的 RBAC 權限。

Cilium 官方 Helm Chart 會自動配置這些權限:

# Cilium operator ClusterRole (from official Helm chart)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cilium-operator
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["list", "watch", "patch"]  # patch 用於移除 taint

常見問題:如果手動部署 Cilium 或使用第三方工具(如 kops)而沒有正確配置 RBAC,會遇到 taint 無法移除的問題。

配置建議

在 Cilium 的 Helm values 中,通常已經包含了對這個 taint 的 toleration:

# Cilium Helm values
agent:
  tolerations:
  - operator: Exists  # 容忍所有 taints,包括 startupTaint

同時確保 Karpenter NodePool 有對應的 startupTaint 配置。


4. DaemonSet 的調度機制

4.1 DaemonSet 如何被優先調度?

DaemonSet 並不是透過「優先調度順序」來保證運行,而是透過以下機制:

自動添加的 Tolerations

DaemonSet 控制器會自動為 DaemonSet Pods 添加以下 tolerations:

Toleration KeyEffect說明
node.kubernetes.io/not-readyNoExecute容忍未就緒的節點
node.kubernetes.io/unreachableNoExecute容忍不可達的節點
node.kubernetes.io/disk-pressureNoSchedule容忍磁碟壓力
node.kubernetes.io/memory-pressureNoSchedule容忍記憶體壓力
node.kubernetes.io/unschedulableNoSchedule容忍不可調度的節點
node.kubernetes.io/network-unavailableNoSchedule容忍網路不可用(非 hostNetwork)

這些自動 tolerations 讓 DaemonSet Pods 能夠在節點初始化的早期階段就被調度上去。

調度流程

1. DaemonSet 控制器監控節點列表
   └─ 為每個符合條件的節點創建對應的 Pod

2. 添加 nodeAffinity 指向特定節點
   └─ spec.affinity.nodeAffinity 匹配目標節點名稱

3. 默認調度器接管
   └─ 設置 .spec.nodeName 綁定到目標節點

4.2 優先級機制

透過設置 priorityClassName,可以確保 DaemonSet Pod 不會被其他 Pod 搶占:

apiVersion: apps/v1
kind: DaemonSet
spec:
  template:
    spec:
      priorityClassName: system-node-critical  # 高優先級

4.3 DaemonSet 與 startupTaint 的配合

當使用 Karpenter 的 startupTaint 時,DaemonSet 需要做兩件事:

  1. 添加對應的 toleration

    spec:
      template:
        spec:
          tolerations:
          - key: example.com/init-required
            operator: Equal
            value: "true"
            effect: NoSchedule
  2. 配置 RBAC 權限並實作 taint 移除邏輯

    重要:移除 taint 需要特定的 RBAC 權限!普通的 Pod 或 ServiceAccount 無法隨意移除節點的 taint。


5. DaemonSet 移除 Taint 的標準規範

5.1 沒有統一標準

重要: Kubernetes 並沒有統一的規範要求 DaemonSet 必須在初始化完成後移除 taint。這是一個約定俗成的模式(convention),而非強制要求。

5.2 各組件的實作方式

  • Cloud Controller Manager

    • 有明確的職責:初始化雲端資源後移除 node.cloudprovider.kubernetes.io/uninitialized
    • 這是由控制器自動處理,不需要手動配置
  • CNI 插件

    • VPC CNI: 透過節點 Ready 狀態隱含保證,通常不需要額外 taint 管理
    • Cilium: 內建支援移除 node.cilium.io/agent-not-ready taint
    • Calico: 可能需要自訂腳本來移除 taint
    • 其他 CNI: 需要查閱各自的文檔
  • CSI Driver

    • AWS EBS CSI: 內建支援移除 ebs.csi.aws.com/agent-not-ready
    • AWS EFS CSI: 內建支援移除 efs.csi.aws.com/agent-not-ready
  • 自訂 DaemonSet

    如果需要自己實作 taint 移除,常見方法:

    apiVersion: apps/v1
    kind: DaemonSet
    spec:
      template:
        spec:
          tolerations:
          - key: my-custom-taint
            effect: NoSchedule
    
          # 方法 1: 使用 postStart hook
          containers:
          - name: my-daemon
            lifecycle:
              postStart:
                exec:
                  command:
                  - /bin/sh
                  - -c
                  - |
                    # 等待服務就緒
                    until check_service_ready; do sleep 1; done
                    # 移除 taint
                    kubectl taint nodes $NODE_NAME my-custom-taint:NoSchedule-
    
          # 方法 2: 使用 init container
          initContainers:
          - name: init-taint-remover
            command:
            - /bin/sh
            - -c
            - |
              # 執行初始化工作
              perform_initialization
              # 移除 taint
              kubectl taint nodes $NODE_NAME my-custom-taint:NoSchedule-

6. 補充:CSI Driver 與 startupTaint

除了 CNI,某些 CSI(Container Storage Interface)Driver 也支援 startupTaint 來避免調度競爭:

1. 為什麼需要?

由於 Kubernetes 的一個已知競爭條件(issue #95911),調度器和 CSINode 可能在節點註冊期間發生競爭,導致調度器錯誤估計節點可以掛載的卷數量。

2. 支援 startupTaint 的 CSI Driver

已知支援的 CSI Driver 包括:

  • AWS EBS CSI Driver: ebs.csi.aws.com/agent-not-ready
  • AWS EFS CSI Driver: efs.csi.aws.com/agent-not-ready
配置範例
apiVersion: karpenter.sh/v1
kind: NodePool
spec:
  template:
    spec:
      startupTaints:
      - key: ebs.csi.aws.com/agent-not-ready
        effect: NoExecute

7. 總結

關鍵要點

  1. Node Ready 狀態的判定:
    • 由 Kubelet 執行健康檢查(包括網路就緒性)
    • 由 Node Lifecycle Controller 根據 kubelet 報告設置 Ready condition
    • CNI 未就緒會直接導致 Node Ready = False
    • 這是 VPC CNI 不需要 startupTaint 的根本原因
  2. Kubernetes 原生機制:
    • 沒有統一的 “startup taint” 概念
    • 各組件(kubelet、cloud-controller-manager、CNI)各自管理自己的 taint
    • 節點 Ready 狀態已經包含了重要的初始化檢查(特別是網路)
  3. Karpenter 的 startupTaint:
    • 是 Karpenter 自訂的機制,提供更靈活的初始化控制
    • 不影響 Karpenter 的調度決策,但會阻止普通 Pod 調度
    • 需要由外部組件(DaemonSet、控制器)負責移除
  4. DaemonSet 調度:
    • 透過自動 tolerations 能夠在節點初始化早期就被調度
    • 配合 startupTaint 可以實現精確的啟動順序控制
    • 沒有統一標準要求 DaemonSet 移除 taint,需要各自實作
  5. CNI 差異的技術原因:
    • VPC CNI:kubelet 的網路就緒檢查已經保證了 CNI 初始化完成,Node Ready 狀態可信
    • Cilium:初始化複雜度較高,節點可能在完整就緒前就 Ready,需要 startupTaint 提供額外保護
    • 關鍵差異:是否能依賴 kubelet 的網路檢查來保證完整初始化
  6. 實務建議:
    • 優先依賴節點 Ready 狀態和資源註冊機制
    • 只在必要時使用 startupTaint(如 Cilium、CSI Driver)
    • 確保有明確的 taint 移除邏輯
    • 監控節點初始化狀態,及時發現問題

延伸思考

  • 在多租戶環境中,如何防止惡意 Pod 透過錯誤的 toleration 繞過 startupTaint?
  • 當 DaemonSet 初始化失敗時,如何自動處理殘留的 taint?
  • 未來 Kubernetes 是否會提供更統一的節點初始化機制?

這些問題都值得在實際運維中深入探討和實踐。