為什麼 Cilium 需要 Startup Taint,VPC CNI 不需要?[Karpenter Startup Taint]
說明 Node Ready 的判定機制、Karpenter startupTaints,以及 Cilium 與 VPC CNI 在節點初始化上的差異。
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 會定期執行以下檢查,並將結果報告給控制平面:
- 容器運行時網路未就緒 (
NetworkReady=false):/etc/cni/net.d/是否有 cni 設定檔 - CSI 提供者未初始化 (
CSINode):檢查 CSINode 對象是否已創建和初始化- 源碼:
pkg/volume/csi/csi_plugin.go
- 源碼:
- **容器運行時狀態檢查不完整:**確保 container runtime 正常運行
- 源碼:
pkg/kubelet/runtime.go
- 源碼:
- Pod 生命週期事件生成器宕機 (
PLEG):PLEG 負責監控 Pod 狀態變化- 源碼:
pkg/kubelet/kubelet.go
- 源碼:
- **節點正在關閉:**檢測節點是否處於 shutdown 狀態
- 源碼:
pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go
- 源碼:
- **缺少 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 原生支援。
特性
- **配置位置:**NodePool 的
.spec.template.spec.startupTaints - **用途:**允許在節點初始化期間設置臨時 taint,常用於 DaemonSet 的啟動順序控制
- **重要特性:**startupTaints 在調度決策時會被忽略,也就是說 Pod 不需要容忍這些 taint,Karpenter 仍會為它們配置節點
- Karpenter 只負責添加,不負責移除
apiVersion: karpenter.sh/v1
kind: NodePool
spec:
template:
spec:
startupTaints:
- key: example.com/init-required
effect: NoSchedule
value: "true"
2.2 Karpenter 的節點初始化判定條件
Karpenter 使用三個因素來確定節點是否已初始化:
- 節點就緒狀態(Node Readiness)
- 檢查
Ready條件類型,期望值為True
- 檢查
- 預期資源已註冊(Expected Resources Registered)
- Karpenter 從
ec2:DescribeInstanceTypes獲取實例類型的所有預期資源 - 檢查這些資源是否正確註冊到
node.status.allocatable,且數量不為零 - 常見問題資源:
nvidia.com/gpu: GPU 實例但缺少 device pluginvpc.amazonaws.com/pod-eni: Security Groups for Pods 功能未啟用
- Karpenter 從
- 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 會檢查容器運行時網路是否就緒:
- 檢查失敗 → Kubelet 報告
NetworkReady=false,CNI 插件未初始化 - Node 不 Ready → Node Lifecycle Controller 設置
Ready=False,節點帶上node.kubernetes.io/not-ready:NoExecutetaint - VPC CNI 初始化 → DaemonSet 配置 ENI 和 IP 地址
/etc/cni/net.d/有 cni 配置檔,檢查通過 → Kubelet 網路檢查成功- Node 變 Ready → Node Ready 狀態為
True
因此 VPC CNI 不需要額外的 startupTaint,Kubelet 的檢查本身就充當了「等待門卡」。
3.2 為什麼 Cilium 需要 startupTaint?
Cilium 的初始化涵蓋更多步驟(載入 BPF 程式、設置網路策略、與其他節點連接),且某些配置下(如 hostNetwork: true)Kubelet 的網路檢查可能提早通過。這導致節點被標記為 Ready,但 Cilium Agent 仍在初始化中。
事件順序:
- Cilium DaemonSet 啟動
- 開始初始化(載入 eBPF 程式、連接集群等)
- 寫入配置檔案到
/etc/cni/net.d/05-cilium.conflist⚠️ - Kubelet 檢測到配置檔案存在 → NetworkReady=true → Node Ready
- 但 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 Key | Effect | 說明 |
|---|---|---|
node.kubernetes.io/not-ready | NoExecute | 容忍未就緒的節點 |
node.kubernetes.io/unreachable | NoExecute | 容忍不可達的節點 |
node.kubernetes.io/disk-pressure | NoSchedule | 容忍磁碟壓力 |
node.kubernetes.io/memory-pressure | NoSchedule | 容忍記憶體壓力 |
node.kubernetes.io/unschedulable | NoSchedule | 容忍不可調度的節點 |
node.kubernetes.io/network-unavailable | NoSchedule | 容忍網路不可用(非 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 需要做兩件事:
-
添加對應的 toleration
spec: template: spec: tolerations: - key: example.com/init-required operator: Equal value: "true" effect: NoSchedule -
配置 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-readytaint - 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
- AWS EBS CSI: 內建支援移除
-
自訂 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. 總結
關鍵要點
- Node Ready 狀態的判定:
- 由 Kubelet 執行健康檢查(包括網路就緒性)
- 由 Node Lifecycle Controller 根據 kubelet 報告設置 Ready condition
- CNI 未就緒會直接導致 Node Ready = False
- 這是 VPC CNI 不需要 startupTaint 的根本原因
- Kubernetes 原生機制:
- 沒有統一的 “startup taint” 概念
- 各組件(kubelet、cloud-controller-manager、CNI)各自管理自己的 taint
- 節點 Ready 狀態已經包含了重要的初始化檢查(特別是網路)
- Karpenter 的 startupTaint:
- 是 Karpenter 自訂的機制,提供更靈活的初始化控制
- 不影響 Karpenter 的調度決策,但會阻止普通 Pod 調度
- 需要由外部組件(DaemonSet、控制器)負責移除
- DaemonSet 調度:
- 透過自動 tolerations 能夠在節點初始化早期就被調度
- 配合 startupTaint 可以實現精確的啟動順序控制
- 沒有統一標準要求 DaemonSet 移除 taint,需要各自實作
- CNI 差異的技術原因:
- VPC CNI:kubelet 的網路就緒檢查已經保證了 CNI 初始化完成,Node Ready 狀態可信
- Cilium:初始化複雜度較高,節點可能在完整就緒前就 Ready,需要 startupTaint 提供額外保護
- 關鍵差異:是否能依賴 kubelet 的網路檢查來保證完整初始化
- 實務建議:
- 優先依賴節點 Ready 狀態和資源註冊機制
- 只在必要時使用 startupTaint(如 Cilium、CSI Driver)
- 確保有明確的 taint 移除邏輯
- 監控節點初始化狀態,及時發現問題
延伸思考
- 在多租戶環境中,如何防止惡意 Pod 透過錯誤的 toleration 繞過 startupTaint?
- 當 DaemonSet 初始化失敗時,如何自動處理殘留的 taint?
- 未來 Kubernetes 是否會提供更統一的節點初始化機制?
這些問題都值得在實際運維中深入探討和實踐。