Kubernetes 声明式编排:从控制器到调度与网络模型

举报
yd_210610071 发表于 2026/11/06 22:19:18 2026/11/06
【摘要】 Kubernetes 声明式编排:从控制器到调度与网络模型本文不列 YAML 字段清单,而是回答四个问题:为什么声明式比脚本可靠、控制器到底在循环什么、Pod 为什么被调度到某台机器上、Service 的一个虚拟 IP 是怎么变成真实转发的。最后附上探针、优雅终止与滚动发布的正确配法。 声明式:你写期望,系统负责逼近命令式(脚本):做 A、再做 B、再做 C 中途失败 → 状态停在半路,...

Kubernetes 声明式编排:从控制器到调度与网络模型

本文不列 YAML 字段清单,而是回答四个问题:为什么声明式比脚本可靠、控制器到底在循环什么、Pod 为什么被调度到某台机器上、Service 的一个虚拟 IP 是怎么变成真实转发的。最后附上探针、优雅终止与滚动发布的正确配法。

声明式:你写期望,系统负责逼近

命令式(脚本):做 A、再做 B、再做 C
  中途失败 → 状态停在半路,重跑可能重复执行,也可能漏掉

声明式(K8s):写下"我要 3 个副本、镜像 v2"
  系统持续比较 期望(spec) 与 实际(status),不断消除差异
  中途失败 → 下一轮循环会重新尝试,天然幂等、可自愈

这是理解 K8s 的第一性原理:它不是一个"部署工具",而是一组不断把现实拉向期望的控制循环。所有"神奇"的行为(自动重启、自动扩缩、自动重新调度)都是这个循环的副产品。

数据流:唯一真源与三级缓存

kubectl apply ─► kube-apiserver ─► etcd(唯一真源,带 resourceVersion)
                      │
                      │  watch 推送增量
                      ▼
        ┌─────────────┴──────────────┐
        │                            │
  controller-manager            kube-scheduler
  (Deployment→ReplicaSet        (选节点,写 spec.nodeName)
    →Pod 的层层收敛)                  │
        │                            ▼
        │                        kubelet(监听绑定到本机的 Pod)
        │                            │
        │                            ├─► CRI:拉起容器
        │                            ├─► CNI:分配 Pod IP
        │                            └─► CSI:挂载卷
        ▼
   写回 status(Pod 处于 Pending/Running/Ready 等)

两个工程要点:

  • Informer 的本地缓存。控制器不是每次去问 apiserver,而是 watch 增量后维护一份内存副本,业务逻辑读缓存。这让控制循环可以高频运行而不打爆 apiserver。
  • 工作队列 + 限速。事件先入队,失败按指数退避重入,避免同一个对象出问题时把循环拖死。

控制器:调谐函数只做一件事

// 伪代码,所有控制器的心智模型都一样
for {
    key := queue.Get()             // 拿到"某个对象的名字"
    obj := cache.Get(key)          // 从 informer 缓存读最新状态
    if obj == nil { continue }     // 已被删除,忽略
    desired := buildDesired(obj)   // 期望状态
    actual  := observe(obj)        // 实际状态
    if !equal(desired, actual) {
        reconcile(desired, actual) // 只做差异动作,必须幂等
        queue.AddAfter(key, backoff) // 需要再次确认时延迟重入
    }
}

自定义资源(CRD)+ 自定义控制器 = Operator。判断要不要写 Operator 的标准只有一条:这个中间件是否有"运维知识"需要固化(分片扩缩、主从切换、备份恢复)。只是部署它,用 StatefulSet + Helm 就够。

调度:先过滤,再打分

调度一个 Pod 分两步:

Filter(硬性条件,不满足直接排除节点)
  ├─ 资源:剩余可分配 CPU/内存 ≥ requests(注意:只看 requests)
  ├─ 亲和:nodeSelector / nodeAffinity / podAffinity
  ├─ 污点:节点有 taint,Pod 没有对应 toleration → 排除
  └─ 卷:PV 的可用区、ReadWriteOnce 是否已被别的节点占用

Score(软性偏好,0~100 加权求和)
  ├─ LeastAllocated / BalancedAllocation:挑最空闲的节点
  ├─ ImageLocality:镜像已在节点上,启动更快
  └─ InterPodAffinity / 拓扑分布:尽量打散或聚拢

Bind:把选中的节点写进 spec.nodeName,kubelet 接手
无可调度节点且优先级够高 → 抢占(驱逐低优先级 Pod)

必须记住的一条:requests 才是调度依据,limits 只影响运行时。

资源配置 调度阶段 运行时行为
只设 requests 按 requests 预留 CPU 可超用;内存可超用,节点压力大时被动
requests = limits 按该值预留 Guaranteed:CPU 超限被节流(CFS quota),内存超限被 OOMKill
都不设 按 0 预留,随便塞 BestEffort:最先被驱逐,节点一紧张就死

QoS 与驱逐顺序直接相关:BestEffort → Burstable → Guaranteed。核心服务务必配成 Guaranteed,否则节点内存吃紧时它会先被牺牲。

探针:三个探针解决三个不同问题

startupProbe   启动慢的应用专用。成功前不执行 liveness/readiness,
               避免"启动 30 秒的服务被 liveness 在第 10 秒杀掉"

livenessProbe  容器还活着吗?失败 → 重启容器
               只放"重启真能修好"的检查:进程死锁、端口不通。
               绝不能把依赖检查(数据库、下游)放进来——
               依赖抖一下,全部 Pod 一起重启,灾难放大

readinessProbe 能接流量吗?失败 → 从 Service endpoints 摘除,
               不重启。适合放"依赖就绪、热身完成、限流器初始化"

优雅终止:时序比配置更重要

kubectl delete / 滚动更新触发删除
   │
   ├─(1) apiserver 标记 deletionTimestamp,Pod 变 Terminating
   ├─(2) EndpointSlice 控制器摘除端点(与 (3) 并行,且是异步的!)
   ├─(3) kubelet 执行 preStop hook
   ├─(4) preStop 结束 → 发送 SIGTERM
   ├─(5) 等待 terminationGracePeriodSeconds(默认 30s)
   └─(6) 超时未退出 → SIGKILL

最常见的 502 来源:(2) 还没生效,流量还在进来,而 (4) 已经让进程退出
正确的 preStop:sleep 5~10s(等端点传播)→ 应用捕获 SIGTERM
                → 停止接新请求 → 处理完存量请求再退出
务实的做法:preStop sleep 短于 grace period,应用内再做 shutdown 等待

网络:一个虚拟 IP 如何变成转发

K8s 网络模型的三条约束:
  1. 每个 Pod 有独立 IP,容器间直接通信
  2. 同节点 Pod 通过 veth 对 + 网桥(或 eBPF)互通
  3. 跨节点 Pod 不加 NAT 也能互通 → 由 CNI 实现
     Flannel(VXLAN 封装)| Calico(BGP 路由)| Cilium(eBPF)

访问 Service 的流量路径(iptables 模式):
  Pod ──► 10.96.0.10:80(ClusterIP,谁都没监听这个 IP)
        └─► 命中 iptables 规则链 KUBE-SERVICES
              └─► 随机选中一个 Endpoint IP:Port → DNAT
                    └─► 走 CNI 把包送到目标 Pod,
                        回包时再做反向 NAT 改回 ClusterIP

  问题:Service 数量上万后 iptables 规则是线性增长的,
       每次增删端点都要全量重写规则,延迟与 CPU 都顶不住
  → IPVS:哈希表查找,规则 O(1),大规模集群更稳
  → eBPF(Cilium):绕过 kube-proxy,在内核层面做负载均衡

几个容易混淆的点:

  • Headless Service(clusterIP: None)不分配虚拟 IP,DNS 直接返回所有 Pod IP,客户端自己选。StatefulSet 靠它拿到稳定的 pod-0.svc 域名。
  • DNS 搜索域:容器内 /etc/resolv.conf 带 search <ns>.svc.cluster.local svc.cluster.local cluster.local。跨命名空间必须写全名,否则会解析到本命名空间的同名服务——这是一个非常隐蔽的"调错环境"故障。
  • NetworkPolicy 只是声明,真正生效取决于 CNI 是否支持。默认策略是全部允许,要默认拒绝必须显式写一条空 PodSelector 的 deny。

发布与扩缩容:Readiness 决定成败

Deployment 滚动更新:
  maxSurge=25%       最多超出期望副本多少(先建后删)
  maxUnavailable=25% 最多有多少副本不可用(保守场景设 0)

  新 ReplicaSet 起 Pod → 通过 readiness 才计入可用
  → 每通过一个,就删除一个旧 Pod → 直到全部替换

  没有 readiness 探针 = Pod 一 Running 就接流量
  → 进程还没初始化完、连接池还没建好就开始报错

扩缩容的三个注意:

  • HPA 是滞后的。基于 CPU 指标时,指标本身有采集周期,扩容后 Pod 还要冷启动。对突发流量必须预留最小副本数,或提前定时扩容。
  • PDB(PodDisruptionBudget) 只约束自愿驱逐(节点维护、drain),不约束崩溃和驱逐风暴;设 minAvailable: 2 能防止运维操作把服务打挂。
  • 缩容会打断长连接。缩容前要配合优雅终止,并把长连接服务拆到独立的 Deployment。

存储:PV、PVC 与有状态副本

StorageClass(能力定义,如 ssd/多可用区)
      │ 动态供给
      ▼
PVC(我要 20Gi 读写一次)──绑定──► PV(真实存储,CSI 驱动创建)
                                      │
                                 挂载到 Pod 的 volumeMounts

StatefulSet 才有:
  - 稳定网络标识  web-0.web.default.svc
  - 稳定存储      volumeClaimTemplates 给每个副本一个独立 PVC
  - 有序创建/删除 0→1→2 启动,2→1→0 删除

坑点集中在这里:ReadWriteOnce 在多数 CSI 上意味着只能挂到一个节点,滚动更新时会因为旧 Pod 还没释放卷而卡在 ContainerCreating,需要 RollingUpdate 策略或改用支持多挂的存储。

常见坑

  1. 不设 requests:调度器以为节点很空,把几十个 Pod 塞上去,运行时互相抢 CPU,P99 集体爆炸。
  2. 用依赖检查做 liveness:数据库抖一下,全集群 Pod 一起重启,故障自我放大。
  3. grace period 与 preStop 配反了:preStop 里 sleep 30 秒而 grace 也是 30 秒,进程永远收不到 SIGTERM 就被 SIGKILL。
  4. 忽略 DNS 搜索域:同名服务跨命名空间解析错误,测试环境正常、生产串到别的服务。
  5. Service 没有 Endpoint:Selector 写错或 readiness 一直失败,表现为"连接被拒绝"而不是超时,容易被误判成网络问题。
  6. ConfigMap 更新了但应用没变:环境变量注入的配置不会热更新,挂载的文件也只是延迟同步,需配合滚动重启(校验和注解)。
  7. CPU limit 设太低:CFS 配额按 100ms 周期切分,限制过紧会导致持续节流,表现为"CPU 使用率不高但请求很慢"。
  8. 单节点承载过多同应用副本:没有配 topologySpreadConstraints 或反亲和,一台机器挂掉就损失大部分副本。

总结

机制 本质 记住这一句
声明式 spec 与 status 的持续收敛 失败会重试,所以逻辑必须幂等
控制器 watch + 缓存 + 队列 + reconcile Operator 的价值是固化运维知识
调度 过滤再打分,requests 是唯一依据 limits 管运行时,requests 管落点
探针 三个探针三种语义 liveness 只放"重启能修好"的检查
优雅终止 摘流量早于杀进程 preStop 等到传播完成再退出
Service iptables/IPVS/eBPF 做 DNAT 规模大了必须换 IPVS 或 eBPF
发布 readiness 决定可用性 滚动更新的质量取决于就绪判定

K8s 的复杂度不在 API 有多少字段,而在多级控制器各自收敛、时序彼此交错。排障时先问三个问题:对象期望状态是什么、谁负责把它变成现实、这一步的时序有没有和别的组件抢跑。大部分"玄学问题"都能落在这三问里。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。