华为云容器引擎 CCE:Kubernetes 生产集群搭建与运维

托管 K8s 把控制平面变成"别人的服务",代价是把看得见的复杂度换成了看不见的耦合。这篇的重点是分清:哪些故障你还能自己查,哪些只能等。

技术文章约 13 分钟6 次阅读

#托管的分界线:复杂度没有消失,只是换了位置

从自建 Kubernetes 集群迁到华为云 CCE(Cloud Container Engine,云容器引擎)之后,第一周的感觉通常是"轻松":安装集群、维护 etcd、轮换证书这些体力活全都消失了。第二周开始会冒出一个新的不安——kubectl 还能用,但你不太确定它在跟谁说话;出了事,也不知道自己到底能查到哪一层。

这个不安是对的。托管没有消灭复杂度,它只是把复杂度从一个地方搬到另一个地方:把"看得见的运维负担"换成"看不见的耦合边界"。理解这条边界在哪里、边界两侧各自负责什么,是使用 CCE 的第一步。

#CCE 为什么基于 Kubernetes

Kubernetes 能成为托管容器平台的基础,源于它自身的分层设计:声明式 API 与控制器循环把"期望状态"与"实际状态"之间的调和过程标准化,控制面(Control Plane)与数据面(Data Plane)天然可分。CCE 正是沿着这条分界线做托管——控制面由华为云维护,用户拥有的是数据面(工作节点)与工作负载的完整操作权。

要准确理解托管集群,先记住 Kubernetes 集群里两组角色的分工:

层次组件职责
控制面etcd、kube-apiserver、kube-scheduler、kube-controller-manager状态存储、API 入口、调度决策、期望状态调和
数据面kubelet、容器运行时、kube-proxy、CNI 插件在节点上拉起并守护容器、维护网络规则

etcd 是控制面的地基:所有对象的期望状态都落在其中,apiserver 是对外唯一的读写入口。自建集群里,这一层从安装、高可用到证书轮换都由你负责;在 CCE 中,这一层由平台托管(通常以多可用区部署保证高可用),你拿到手的是一组经过鉴权的 API 地址,而不是那几台机器。

#自建与托管:你能查到哪里为止

把"能查多深"列成对照,托管的分界线会非常清晰:

层次自建集群托管集群(CCE 这类)
控制平面(apiserver/etcd/scheduler)你装、你修、你能看日志厂商维护;你能看指标,通常看不到主机
数据面节点你装 kubelet、你修你管节点池,节点上的问题你能进去排查
网络插件你选、你调通常平台集成,可调项是子集
版本生命周期你自己决定由平台的受支持版本决定
故障时能查的深度全栈到控制平面入口为止

最后一行是托管最本质的代价:你能查的东西变少了,而且减少的部分恰好是最难自建的那部分。这不是说托管不好——对绝大多数团队而言,自己维护 etcd 的风险远大于"少看到一层日志"的不便。但要接受它,并在架构上为"查不到"留出余量:客户端侧要有足够的可观测性,不能把全部诊断希望寄托在 apiserver 日志上。

#节点池:把机器变成可丢弃的单位

节点池是托管集群里最重要的抽象之一。它的价值不在"批量管理",而在于把节点从"资产"变成了"规格":一个节点池是一份节点模板——机型、镜像、磁盘、标签与污点、期望数量——节点只是这份规格的实例化结果,随时可以整批销毁重建。

#按负载特征分池,而不是按机型分池

用节点池的关键是:按负载特征分池,而不是按机器型号分池。

池的划分依据例子好处
工作负载类型通用 / GPU / 内存密集调度约束清晰,扩缩容互不干扰
稳定性等级在线服务 / 批处理 / 抢占式故障与回收的影响面被隔离
生命周期长期 / 临时升级时可以整池替换

第三行值得单独强调:"整池替换"是滚动升级真正能落地的前提。如果一个池里混着不同年代、不同配置的节点,升级就退化成逐个且各不相同的维护动作;如果池内是同类节点,升级可以做成"新池建好、流量切过去、旧池销毁"——从"修机器"变成"换机器"。

这也是云上最该被用起来的特性:把机器当成可以随时丢弃的东西,前提是任何一台机器上都没有不可重建的状态。日志、缓存这类易失数据要落在集群外的持久化位置,或能在启动时重建,而不是藏在某个节点的本地磁盘里。

#用 kubectl 核对节点池的划分结果

节点池的标签通常由平台自动打上。升级、扩缩容或排障前,先确认节点确实落在预期的池里,再动手。下面命令用来查看节点标签、按池过滤节点并核对节点上的污点:

kubectl get nodes --show-labels  # 查看全部节点标签,找节点池标识(如 nodepool=general)
kubectl get nodes -l nodepool=general -o wide  # 按池过滤节点,确认该池规模与就绪状态
kubectl describe node <node-name> | grep -A5 Taints  # 查看节点污点,确认调度约束

注意:这里的 nodepool=general 只是示例标签,实际标签名与取值以 CCE 控制台或对应版本文档为准。

#可用性预算:你允许自己同时弄坏几台

Kubernetes 里有一组机制专门用来表达"现在可以坏几台",理解它们之间的关系比记住名字有用:

机制表达什么用错时会发生什么
副本数需要几个实例单副本服务在节点维护时直接中断
反亲和/拓扑分布这些实例别挤在一起三副本全落在同一台机器上
PodDisruptionBudget(PDB)自愿中断时最多坏几个配得太严,节点永远驱逐不干净
就绪探针什么时候算能接流量探针太松,把坏实例也接进流量

其中反亲和与 PDB 是一对互补机制:反亲和解决"物理上会不会一起坏",PDB 解决"运维动作会不会一次坏太多"。两者缺一,高可用就是靠运气。

#反亲和:物理上别挤在一起

把实例尽量打散到不同节点,用 podAntiAffinity 表达。下面是一段常见的反亲和配置,让 web 的每个副本尽量落在不同主机上:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3  # 三副本,配合反亲和打散到不同节点
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web }
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels: { app: web }
                topologyKey: kubernetes.io/hostname

更细的控制可以用 topologySpreadConstraints 按区域(region/zone)做拓扑分布,替代"尽量不挤一台"的软约束,具体字段以所用 Kubernetes 版本为准。

#PDB:自愿中断时最多坏几个

PDB 约束的是"自愿中断"(节点排空、集群升级、主动驱逐),而不是节点宕机这类非自愿中断——后者 Kubernetes 无能为力,只能靠副本数与反亲和兜底。下面是一份三副本服务的 PDB,要求任意时刻至少 2 个副本可用:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web
spec:
  minAvailable: 2  # 三副本、至少 2 个可用,即一次自愿中断最多坏 1 个
  selector:
    matchLabels: { app: web }

PDB 最容易配错的方向是"太严"。看起来保守,实际会出现一个很具体的现象:节点排空(drain)卡住不动,因为驱逐会被 PDB 拦住。此时人容易做两件事——加大 --timeout(没用),或者直接把 PDB 删掉(危险)。正确的做法是理解"排空是自愿中断",然后按业务窗口安排它:先确认 PDB 当前允许驱逐几个,是扩容副本、调整 PDB 还是等待业务低峰,都应在排空前想清楚。

排空节点并在排空前后核对 PDB 状态:

kubectl get pdb  # 排空前查看各 PDB 允许的可用副本余量
kubectl describe pdb web  # 查看 web 这条 PDB 的详细状态
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data  # 排空节点,容忍 DaemonSet、清理 emptyDir
kubectl get events --sort-by=.lastTimestamp | grep -i disruption  # 排空被卡住时,看是哪条 PDB 在拦截

如果 drain 长时间不结束,最后一条命令能看到类似 "Disruption budget...prevents..." 的事件,据此判断是扩容副本、还是把维护窗口移到业务低峰,而不是强行删 PDB。

#托管集群的网络,边界在哪

网络是托管集群里最容易出现"我觉得是这样"的地方。三条常见边界,每一条都直接决定排查"包到底有没有到"的路径。

#overlay 与 underlay:Pod 的 IP 在哪个路由域

容器网络模型的差异,本质是"Pod 的 IP 和节点 IP 是否处于同一路由域":

  • overlay 思路(如容器隧道网络):在物理网络上叠加一层虚拟网络,节点间的 Pod 流量经过封装(如 VXLAN 隧道)转发。Pod IP 只在集群内可见,集群外无法直接访问;Pod 访问外部时通常由节点做 SNAT。代价是每跳多一层封装开销,换来的好处是 IP 规划灵活、与底层 VPC 解耦。
  • underlay 思路(如 VPC 网络、云原生网络 2.0):Pod 直接使用 VPC 内的网段(或为 Pod 挂接弹性网卡),Pod IP 在 VPC 路由域内可见,集群外只要安全组放行就能直接访问,SNAT 场景少,性能更接近裸机。代价是 IP 规划必须与 VPC 一致,地址空间更紧。

CCE 具体提供哪几种网络模式、参数如何配置,随版本演进,以官方文档对应版本为准。但排查逻辑是通用的:先明确所在模式的转发路径,再决定该去节点上看隧道封装,还是去 VPC 路由表里看路由。

#负载均衡的双层健康检查错配

LoadBalancer 类型的 Service 背后是厂商的负载均衡实例(如 ELB),这个实例有它自己的健康检查与会话保持策略,与 kubelet 的存活/就绪探针是两套独立系统。于是会出现一个经典错配:Kubernetes 认为健康、LB 认为不健康(或相反)。

任何"两层健康检查"的系统,错配是迟早的事。排查时先确认"谁在判死",再去看被判定的一方:LB 判死,先查它的健康检查端口/路径是否与容器监听一致;Pod 判死,再回到探针配置。

#NetworkPolicy 的默认行为必须实测

NetworkPolicy 是白名单式的:匹配到的流量按规则放行或拒绝,未匹配的流量默认怎么走,各实现略有差异(有的默认允许,有的默认拒绝)。上线前必须实际验证一次,而不是按理解假设。验证方法可以是在目标 Pod 里跑一个探测容器,分别测放行与不放行的两个方向,确认策略生效:

kubectl get networkpolicy -A  # 查看集群中已定义的网络策略
kubectl describe networkpolicy <policy-name>  # 查看某条策略的具体规则
kubectl run probe --rm -it --image=busybox --restart=Never -- sh -c "wget -qO- http://web.default.svc:80 && echo REACHABLE || echo BLOCKED"  # 起临时 Pod 做连通性探测

注意:探测镜像与命令按你集群的镜像仓库可用性调整;REACHABLE/BLOCKED 只是示例输出,真实结果取决于策略配置。

#服务访问不通时的分层验证

Service 访问不通,按从下往上的顺序逐层确认:端点是否就绪、目标端口是否监听、网络策略是否放行、负载均衡是否健康。下面命令覆盖前三层:

kubectl get svc <service-name>  # 第 1 层:Service 是否解析到后端
kubectl get endpoints <service-name>  # 第 1 层:端点是否就绪
kubectl get pods -l app=<app-name> -o wide  # 第 2 层:Pod 就绪状态与所在节点
kubectl get networkpolicy -n <namespace>  # 第 3 层:网络策略是否放行来源

云负载均衡那一层通常看不到实例内部细节,确认前三层无误后仍不通,再结合平台侧控制台(监听器、健康检查、安全组)判断。

#能自己查的与只能等的:故障排查的分层

把故障按"你能不能自己往下查"分一下类,心里会踏实很多:

现象你能不能自己往下查
Pod 起不来、反复重启能。kubectl describe、容器日志、事件
节点 NotReady部分能。节点本身能进,kubelet 日志能看到一部分
Service 访问不通能查到网络策略与端点,但云负载均衡那一层可能看不到
apiserver 报 5xx / 限流基本只能等。能看到的是客户端侧的错误率与延迟
调度不上去能。看事件里的调度失败原因,通常是资源或污点
存储挂载失败部分能。CSI 侧日志可见性取决于平台

Pod 侧的排查命令如下。先看描述与事件,再按需取日志,不要一上来就重启:

kubectl describe pod <pod-name>  # 查看 Pod 状态与最近事件(镜像拉取失败、探针失败都会体现)
kubectl logs <pod-name> --previous -f  # 查看容器日志;--previous 看上一实例日志,适合 CrashLoopBackOff
kubectl get events --sort-by=.lastTimestamp  # 按时间排序查看命名空间事件,定位调度失败或驱逐原因
kubectl describe pod <pod-name> | grep -A10 Events  # 调度失败时快速定位 Unschedulable 事件

对"只能等"的两类(控制面故障、依赖平台侧组件的故障),正确的应对方式不是想办法"看到",而是在设计阶段就不依赖它:

  • 给 apiserver 的调用方(controller、operator、CI)加退避与降级,控制面抖动时客户端能自保。
  • 关键路径不要每次都实时查询 apiserver(比如用缓存或影子配置),避免把控制平面当数据面用。
  • 在客户端侧建立独立的可观测性:错误率、延迟、队列积压,这些指标不依赖控制面也能看到。

#成本:节点池的浪费长什么样

托管集群的账单浪费通常不是"节点太大",而是三种结构性的:

  1. 池的碎片:多个小池,每个都有闲置余量。池越多,余量被重复预留的次数越多。
  2. requests 与 limit 的差距:调度按 requests,实际用 limit 附近的量很少。requests 虚高是云上最普遍的隐性浪费。
  3. 缩容不下来的负载:没有用 HPA,或者用了但伸缩指标选错了——例如按 CPU 伸缩一个 IO 密集的服务,永远不触发。

第一条和第三条都能靠"先把池合并、再按负载分池"解决;第二条需要的是基于实际用量的 requests 校准,而不是一次调完就完事——它需要一个定期校准的机制:用监控数据统计每个工作负载的真实用量分布,把 requests 收敛到贴近实际水位,同时保留一定的突发余量。

HPA 的正确姿势是先确认指标与业务信号匹配,再配置伸缩范围。下面命令为 deployment web 配置基于 CPU 的自动伸缩:

kubectl autoscale deployment web --cpu-percent=70 --min=2 --max=10  # 目标 CPU 70%,副本范围 2-10
kubectl get hpa  # 查看 HPA 当前状态
kubectl describe hpa web  # 查看触发原因与最近伸缩动作

CPU 指标只对 CPU 敏感型服务有效。IO 密集、连接数敏感或排队型服务,应改用平台支持的自定义指标(如 QPS、连接数、队列深度),具体以官方文档对应版本支持的指标源为准。指标与业务信号不匹配,HPA 要么从不触发,要么持续抖动,成本与稳定性两头吃亏。

#边界:这篇没写什么

  • 集群创建的具体步骤与控制台参数:界面与选项随版本变化,以官方文档对应版本为准。
  • CSI/CNI 各实现的对比:差异大且演进快,结论很容易过期。
  • Service Mesh 与 Ingress 细节:那是构建在集群之上的另一层,独立成篇更清楚。
  • 集群规模数字:"多大的集群该用托管"取决于团队的自维护能力,不是节点数。

一句话收束:托管把控制平面变成了服务,代价是你的诊断能力在某个深度上被截断。承认这条边界、并围绕它设计客户端侧的可观测性,比假装它不存在要安全得多。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

华为云容器引擎 CCE:Kubernetes 生产集群搭建与运维 · LXH·BLOG