Kubernetes 设计哲学折腾手记

YAML 写对了,Pod 还在 CrashLoopBackOff——从 Swarm 迁过来之后,我才真正搞懂 K8s 说的「声明式」和「控制器」不是在炫概念。

Swarm 时期习惯的是「一条命令把容器拉起来」:docker service scale web=5,当场生效。迁到 Kubernetes 之后,团队还是这套脑子——上线前 kubectl scale deployment web --replicas=5,过一会儿又变回 3;有人手工 kubectl delete pod 清故障,Deployment 立刻补一个新的,删错版本的话旧 Pod 又回来。

这些都不是 K8s 坏了,是设计哲学和 Swarm 根本不一样。这篇不打算复述官方架构图,只记迁移后真正帮上忙的几件事:声明式到底声明了什么、控制器在后台干什么、以及 specstatus 为什么经常对不上。

命令式脑子撞上声明式系统

Swarm 的 docker service update 本质是发指令:你现在给我改成 5 个副本。Kubernetes 的 Deployment 是交期望状态:我希望始终有 5 个副本在跑,至于中间删了 Pod、节点挂了、镜像拉失败,控制器自己想办法凑齐。

第一次被教育,是有人手工扩缩容:

# 手工改成 5 副本
kubectl scale deployment web --replicas=5
kubectl get deployment web
# READY 5/5

# 十分钟后
kubectl get deployment web
# READY 3/3  —— 又回去了

根因是 YAML 里还写着 replicas: 3。Deployment Controller 一直在跑 reconcile 循环:实际状态 ≠ spec,就改实际状态。手工 scale 只改了集群里的对象,没改 Git 里的 manifest,下一轮 Argo CD 同步或者有人 kubectl apply -f,数字就被打回去。

graph LR A[Git / YAML<br/>replicas: 3] -->|apply| B[API Server<br/>spec.replicas=3] B --> C[Deployment Controller] C --> D{实际 Pod 数 = 3?} D -->|否| E[创建或删除 Pod] D -->|是| F[什么都不做] E --> D

这和 Swarm 最大的体感差异:改集群不算完,改「期望状态」才算。后来我们定规矩:生产变更只走 PR 改 YAML,禁止裸 kubectl scale(紧急止血除外,且必须回头补 YAML)。

控制器:不是魔法,是死循环

文档里写 Controller 监控资源、消除差异——听起来很抽象。排障时看 Event 才具体:

kubectl describe deployment web
# Events:
#   ScalingReplicaSet  deployment/web  Scaled up replica set web-7d4f9 to 3
#   ScalingReplicaSet  deployment/web  Scaled down replica set web-6a2b1 to 0

Deployment 自己不直接管 Pod,它管 ReplicaSet;ReplicaSet 再管 Pod。发布新镜像时,旧 ReplicaSet 缩到 0、新 ReplicaSet 拉到 3——不是原地改容器,是换一套对象。我们有一次滚动更新卡住,查半天发现是新 ReplicaSet 的 Pod 一直 ImagePullBackOff,旧 Pod 已经被缩掉了,服务半死不活。

理解控制器之后,排查路径就固定了:

  1. kubectl get deploy,rs,pod -l app=web —— 看三层对象是否对齐
  2. kubectl describe 看 Events —— 控制器最后一步想干什么
  3. 改 spec,别跟 status 较劲 —— status 是控制器写回来的

自定义 CRD / Operator 也是同一套:你声明期望,它写 reconcile 逻辑。我们后来给 Redis 集群上了 Operator,备份窗口、主从切换不再靠 Cron + 手工脚本,但调试思路一样——看 CR 的 status.phase,看 Operator 日志里 reconcile 失败原因。

metadata / spec / status:三个字段各管什么

API 统一模型听起来像面试题,写 YAML 时老踩坑:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
    env: prod          # 给 Service selector、NetworkPolicy 用
  annotations:
    deployment.kubernetes.io/revision: "3"   # 别手改,控制器维护
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web     # 必须和 selector 对得上,否则 ReplicaSet 认不出 Pod
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
status:              # 用户一般不写,kubectl get 看到的是这里
  replicas: 3
  readyReplicas: 2   # 2 个 Ready —— 和 spec 不一致时说明还在收敛或有问题

我们踩过两个典型坑:

label selector 对不上 —— 改了 Pod template 的 label 忘了改 Deployment selector,新 ReplicaSet 创建 0 个 Pod,旧 Pod 还在跑,看起来「服务正常、发布没生效」。

把 status 当 spec 改 —— 有人 export 现网 Deployment,status 整段贴进 Git,apply 时直接 ignored 或报 warning。正确做法是只版本管理 spec;现网状态用 kubectl get -o yaml 看,别往回写。

最终一致:apply 成功 ≠ 立刻能用

Swarm 里 docker service ls 看到 RUNNING 基本就能打流量。K8s 里 kubectl apply 返回 success,只说明 API Server 接受了 spec,Pod 调度、拉镜像、探针通过还要时间。

我们有过一次「发布成功但 502」:

kubectl rollout status deployment/web --timeout=120s
# error: timed out waiting for the condition
kubectl get pod -l app=web
# web-xxx   0/1   CrashLoopBackOff

Deployment 的 status.conditionsAvailable=False,Ingress 后端已经切到新 Pod,但 readinessProbe 没过。教训:对外宣告发布完成,要看 Ready 副本数,不是看 apply 有没有报错

这也解释了为什么 K8s 选最终一致而不是处处强一致——API 要快、控制面要可扩展,收敛延迟交给控制器和探针去兜。

哪些「哲学」真的影响日常写法

概念以前怎么理解现在怎么用
声明式写 YAML 代替 shell所有变更进 Git;手工改集群会被打回
控制器自动化的黑盒排障看 Events + 三层对象(Deploy/RS/Pod)
不可变基础设施口号改镜像 = 新 ReplicaSet,不 ssh 进容器改文件
自愈Pod 挂了会自动重启先查为什么挂;盲目 delete pod 可能滚到更糟的版本
水平扩展HPA 很酷无状态服务先上 HPA;有状态得先想 PVC 和拓扑

没上来就用 Service Mesh、没急着写 Operator。三节点集群、二十几个 Deployment,把 Deployment + Service + Ingress + ConfigMap/Secret 用顺,比背 Borg 论文管用。

和 Swarm 对照:不是谁更强,是默认假设不同

Docker SwarmKubernetes
心智模型命令改集群状态提交期望,控制器收敛
发布service update 原地改新 RS 滚动替换
扩缩容scale 命令即时改 spec;HPA 改 spec
排障docker service psdescribe events + 多层对象
适合容器少、团队小、要快要多环境、多租户、生态和扩展

我们迁移到 K8s 不是因为 Swarm「不能用」,而是灰度、监控、网络策略、团队技能栈——这些问题 K8s 默认就带假设,Swarm 得自己拼。理解设计哲学,本质是理解这些默认假设,少跟系统对着干。

收束

Kubernetes 的设计哲学不是 PPT 里的「云原生赋能」,而是几件事反复撞墙后才记住:

  • 改 spec,别跟 status 搏斗
  • 控制器一直在 reconcile,手工 patch 集群是临时方案
  • apply 成功只是开始,Ready 才是真的

Borg 论文、API 版本策略那些,知道有这回事就行;日常运维真正用的是 kubectl describe 里的 Events 和 Deployment 三层对象。哲学落地了,CrashLoopBackOff 才不会每次都像玄学。

版权声明: 本文首发于 指尖魔法屋-Kubernetes 设计哲学折腾手记https://blog.thinkmoon.cn/post/4-kubernetes-design-philosophy-notes/) 转载或引用必须申明原指尖魔法屋来源及源地址!