K8s 集群升级的灰度策略:节点池分批与 Pod 中断预算平衡

发布时间:2026/7/13 13:21:17
K8s 集群升级的灰度策略:节点池分批与 Pod 中断预算平衡 K8s 集群升级的灰度策略节点池分批与 Pod 中断预算平衡一、全量升级到一半发现新版 kubelet 和新内核不兼容凌晨 2 点K8s 集群从 1.27 升级到 1.28。前 30 个节点顺利通过第 31 个节点升级后 Pod 全部 CrashLoopBackOff。原因新版 kubelet 依赖的 cgroup v2 参数在旧内核上有行为变更部分容器无法正常启动。问题是——我们是全量滚动升级此时已有 30% 的节点跑着新版本回滚意味着双向迁移。这暴露了 K8s 集群升级的核心风险不是升级本身的问题而是升级策略缺乏灰度机制。必须借鉴应用灰度发布的思路节点池分批、金丝雀验证、自动熔断、可回滚。二、K8s 集群升级的灰度模型graph TD A[集群当前版本br/v1.27] -- B[Phase 1: 金丝雀节点池br/2 个节点(低优先级 Pod)] B -- C{金丝雀验证br/(运行 24h)} C --|失败| D[节点回滚br/排出 降级] C --|通过| E[Phase 2: 5% 节点池br/常规工作负载] E -- F{验证通过br/(运行 6h)?} F --|失败| G[自动熔断br/暂停升级br/通知 oncall] F --|通过| H[Phase 3: 25% 节点池] H -- I{验证通过?} I --|否| G I --|是| J[Phase 4: 50% 节点池] J -- K{验证通过?} K --|否| G K --|是| L[Phase 5: 100% 全量] L -- M[升级完成] D -- N[检查失败原因br/修复后重新执行] G -- N style D fill:#FF6B6B,color:#fff style G fill:#FF6B6B,color:#fff style M fill:#50B86C,color:#fff灰度升级的三个关键设计1. 节点池划分原则按 Pod 优先级和业务重要度将节点分为不同的批次。不要按随机选 N 个节点的方式分批——这会随机地把核心业务的 Pod 和新版本绑定出问题时影响面不可控。2. 金丝雀池隔离金丝雀池只承载低优先级的 Pod如开发环境、批处理任务、监控 Agent确保即使新版本有问题也只影响非核心业务。3. 自动熔断机制在每个阶段升级后自动检查集群关键指标Node Ready 率、Pod 重启率、API Server 延迟。任何指标超标自动暂停升级。三、生产级灰度升级实现节点池配置与升级编排# k8s-upgrade-plan.yaml # 集群升级计划定义文件 apiVersion: upgrade.example.com/v1 kind: ClusterUpgradePlan metadata: name: upgrade-1.27-to-1.28 spec: currentVersion: 1.27.8 targetVersion: 1.28.3 # 全局配置 globalSettings: # 升级前自动执行的检查 preUpgradeChecks: - apiDeprecationCheck # API 废弃检查 - nodeHealthCheck # 节点健康检查 - storageMigrationCheck # 存储迁移检查 # Pod 中断预算保护 # 为什么设置 PDB: 确保升级时每个 Deployment 至少保留 N 个可用副本 podDisruptionBudget: enabled: true minAvailable: 1 # 至少保留 1 个副本 maxUnavailablePercent: 33 # 最多允许 33% 不可用 # 节点 cordon drain 策略 nodeDrain: timeoutMinutes: 10 # 单节点 drain 超时 gracePeriodSeconds: 30 # Pod 优雅终止时间 skipDaemonSets: true # DaemonSet 不驱逐 deleteEmptyDirData: false # 保留 EmptyDir 数据 forceEviction: false # 不使用强制驱逐可能丢数据 # 自动熔断 circuitBreaker: enabled: true metrics: - name: node_ready_rate threshold: 0.95 # Node Ready 率 95% → 熔断 - name: pod_restart_rate threshold: 10 # Pod 重启 10 次/分钟 → 熔断 - name: api_server_latency_p99 threshold: 500 # API Server P99 500ms → 熔断 # 分批策略 phases: - name: canary description: 金丝雀验证 - 低优先级 Pod nodeSelector: pool: canary # 仅选择 poolcanary 的节点 maxNodes: 2 validationTimeMinutes: 1440 # 验证 24 小时 metrics: - name: canary_node_ready threshold: 1.0 # 金丝雀节点必须全部 Ready # 金丝雀阶段失败的处置 onFailure: rollback_phase # 回滚当前批次 - name: early_adopters description: 5% 常规负载验证 nodeSelector: pool: early # 非核心业务的节点池 maxNodes: 5 maxPercent: 5 # 不超过总节点的 5% validationTimeMinutes: 360 # 验证 6 小时 onFailure: pause_upgrade # 暂停升级 - name: majority_1 description: 25% 节点升级 nodeSelector: pool: standard maxPercent: 25 validationTimeMinutes: 180 onFailure: pause_upgrade - name: majority_2 description: 50% 节点升级 nodeSelector: pool: standard maxPercent: 50 validationTimeMinutes: 120 onFailure: pause_upgrade - name: full_rollout description: 全量完成 nodeSelector: pool: * maxPercent: 100 validationTimeMinutes: 60升级控制器的核心逻辑 K8s 灰度升级控制器 负责分批升级的编排、验证和熔断 import asyncio import logging from datetime import datetime, timedelta from dataclasses import dataclass from enum import Enum from typing import List, Dict, Optional from kubernetes import client, config from prometheus_api_client import PrometheusConnect, Metric logger logging.getLogger(__name__) class PhaseStatus(Enum): PENDING pending IN_PROGRESS in_progress VALIDATING validating PASSED passed FAILED failed ROLLING_BACK rolling_back class UpgradeStatus(Enum): IDLE idle RUNNING running PAUSED paused CIRCUIT_BROKEN circuit_broken COMPLETED completed FAILED failed dataclass class UpgradePhase: 单个升级阶段的状态 name: str node_pool: str max_nodes: int validation_minutes: int status: PhaseStatus PhaseStatus.PENDING nodes_processed: List[str] None started_at: Optional[datetime] None completed_at: Optional[datetime] None def __post_init__(self): if self.nodes_processed is None: self.nodes_processed [] class GrayUpgradeController: 灰度升级控制器 def __init__(self, plan_path: str, prometheus_url: str): self.plan self._load_plan(plan_path) self.k8s self._init_k8s_client() self.prom PrometheusConnect(urlprometheus_url, disable_sslTrue) self.status UpgradeStatus.IDLE async def execute_upgrade(self): 执行灰度升级主流程 logger.info(f开始升级: {self.plan.current_version} → {self.plan.target_version}) self.status UpgradeStatus.RUNNING for phase in self.plan.phases: logger.info(f进入阶段: {phase.name}) # Step 1: 升级前检查 if not await self._preflight_checks(): self.status UpgradeStatus.FAILED return # Step 2: 选择本阶段的节点 nodes await self._select_nodes(phase) if not nodes: logger.warning(f阶段 {phase.name} 无可用节点跳过) continue # Step 3: 逐节点升级 phase.status PhaseStatus.IN_PROGRESS phase.started_at datetime.now() for node in nodes: # 检查是否触发熔断 if self.status UpgradeStatus.CIRCUIT_BROKEN: logger.error(触发熔断停止升级) return # 升级单个节点 success await self._upgrade_node(node, self.plan.target_version) if not success: phase.status PhaseStatus.FAILED await self._handle_phase_failure(phase) return phase.nodes_processed.append(node.metadata.name) # Step 4: 验证阶段 phase.status PhaseStatus.VALIDATING phase.completed_at datetime.now() validation_passed await self._validate_phase( phase, timeout_minutesphase.validation_minutes ) if not validation_passed: phase.status PhaseStatus.FAILED await self._handle_phase_failure(phase) return phase.status PhaseStatus.PASSED logger.info(f阶段 {phase.name} 验证通过) self.status UpgradeStatus.COMPLETED logger.info(集群升级完成) async def _upgrade_node(self, node, target_version: str) - bool: 升级单个 K8s 节点的流程 为什么用 cordon → drain → upgrade → uncordon 的顺序 cordon 让新 Pod 不调度到本节点drain 排出已有 Pod 确保升级时节点上没有业务负载 node_name node.metadata.name try: # 1. Cordon: 标记节点不可调度 logger.info(fCordon 节点: {node_name}) await self._cordon_node(node_name) # 2. Drain: 排出 Pod尊重 PDB logger.info(fDrain 节点: {node_name}) success await self._drain_node( node_name, timeout_minutesself.plan.global_settings.node_drain.timeout_minutes, grace_periodself.plan.global_settings.node_drain.grace_period_seconds, ) if not success: logger.error(f节点 {node_name} drain 超时或失败) await self._uncordon_node(node_name) return False # 3. 升级 kubelet/kubeadm由外部升级工具执行 logger.info(f升级节点组件: {node_name} → {target_version}) await self._upgrade_node_components(node_name, target_version) # 4. 验证节点就绪 if not await self._wait_node_ready(node_name, timeout_sec300): logger.error(f节点 {node_name} 升级后未就绪) return False # 5. Uncordon: 恢复调度 await self._uncordon_node(node_name) logger.info(f节点 {node_name} 升级完成) return True except Exception as e: logger.exception(f节点 {node_name} 升级异常: {e}) return False async def _validate_phase(self, phase, timeout_minutes: int) - bool: 验证阶段升级结果 为什么用持续监控而非单次检查 K8s 是最终一致系统节点刚恢复后可能需要几分钟 来重新建立 Pod 间的连接、更新 Service Endpoints 等。 deadline datetime.now() timedelta(minutestimeout_minutes) check_interval 30 # 每 30 秒检查一次 self.status UpgradeStatus.PAUSED # 验证期间暂停后续升级 first_pass False consecutive_pass_count 0 required_consecutive_passes 3 # 需要连续 3 次检查通过 while datetime.now() deadline: metrics_ok await self._check_circuit_breaker_metrics() if metrics_ok: consecutive_pass_count 1 if consecutive_pass_count required_consecutive_passes: if not first_pass: first_pass True logger.info(f阶段 {phase.name} 首次通过验证) else: logger.info(f阶段 {phase.name} 连续 {required_consecutive_passes} 次验证通过) self.status UpgradeStatus.RUNNING return True else: consecutive_pass_count 0 logger.warning(f阶段 {phase.name} 指标未达标继续等待) await asyncio.sleep(check_interval) # 超时未通过 logger.error(f阶段 {phase.name} 验证超时 ({timeout_minutes} 分钟)) return False async def _check_circuit_breaker_metrics(self) - bool: 检查熔断指标 从 Prometheus 查询关键集群指标并与阈值对比 任何一个指标超限即触发熔断 for metric_config in self.plan.global_settings.circuit_breaker.metrics: try: value await self._query_metric(metric_config.name) threshold metric_config.threshold if metric_config.name node_ready_rate: if value threshold: logger.warning(fNode Ready 率 {value:.2%} 阈值 {threshold:.2%}) self.status UpgradeStatus.CIRCUIT_BROKEN return False elif metric_config.name pod_restart_rate: if value threshold: logger.warning(fPod 重启率 {value}/min 阈值 {threshold}) self.status UpgradeStatus.CIRCUIT_BROKEN return False elif metric_config.name api_server_latency_p99: if value threshold: logger.warning(fAPI Server P99 {value}ms 阈值 {threshold}ms) self.status UpgradeStatus.CIRCUIT_BROKEN return False except Exception as e: logger.error(f查询指标 {metric_config.name} 失败: {e}) # 指标查询失败不应阻塞升级避免监控系统自身故障误熔断 continue return True async def _handle_phase_failure(self, phase): 处理阶段失败 if phase.name canary: # 金丝雀失败回滚 logger.info(f金丝雀阶段失败开始回滚 {len(phase.nodes_processed)} 个节点) for node_name in phase.nodes_processed: await self._rollback_node(node_name, self.plan.current_version) else: # 后续阶段失败暂停升级通知 oncall logger.error(f阶段 {phase.name} 失败暂停升级) self.status UpgradeStatus.PAUSED await self._notify_oncall(fK8s 升级在 {phase.name} 阶段失败) # —— 各辅助方法简化实现 —— async def _preflight_checks(self) - bool: # 升级前置检查 pass async def _select_nodes(self, phase) - List: # 根据 NodeSelector 选择本阶段节点 pass async def _cordon_node(self, name: str): # kubectl cordon pass async def _drain_node(self, name: str, timeout_minutes: int, grace_period: int) - bool: # kubectl drain pass async def _uncordon_node(self, name: str): # kubectl uncordon pass async def _upgrade_node_components(self, name: str, version: str): # 调用 kubeadm upgrade pass async def _wait_node_ready(self, name: str, timeout_sec: int) - bool: # 等待节点 Ready pass async def _rollback_node(self, name: str, version: str): # 节点降级 pass async def _query_metric(self, name: str) - float: # Prometheus 查询 pass async def _notify_oncall(self, message: str): # 通知 oncall pass def _load_plan(self, path: str): pass def _init_k8s_client(self): pass四、灰度升级的边界缺点升级周期长完整的 5 阶段灰度可能需要 48 小时以上。安全补丁需要快速升级的场景这个周期不可接受。需要支持加速模式跳过部分验证期。节点池划分需要持续维护如果业务 Pod 的节点亲和性配置不严格低优先级的 Pod 可能跑到高优先级节点上金丝雀隔离失效。API 版本废弃检测跨大版本升级如 1.26→1.29可能有 API 废弃需要提前用kubentkube-no-trouble扫描。禁用场景3 节点以下的小集群灰度分组的统计意义不足。紧急安全补丁如 CVE 9.8 的内核漏洞必须先全量升级再加灰度验证。五、总结K8s 集群升级的灰度策略本质是把应用发布的金丝雀-分批-熔断模型应用到基础设施层。节点池按业务优先级分批次金丝雀池只承载低优先级 Pod熔断器监控 Node Ready 率、Pod 重启率等关键指标。两个必须落实的工程点drain 时尊重 PDB 确保业务不中断验证阶段需要连续多次检查通过才认为稳定。生产集群升级灰度不是可选项是必选项。