Kubernetes:声明意图,控制器维持世界

一、引言

容器解决了”可重复交付”——同一个镜像,在任何机器上跑出来的环境都一样。但它没有回答一个更大的问题:一百个容器,谁来管?

容器崩了谁来重启?流量该发给哪个容器?版本升级时怎么做到不停服?新机器加入时怎么把容器分配过去?这些问题,docker run 一条命令回答不了。

Kubernetes 是这个问题的系统性回答。但要真正理解它,需要先理解它翻转了什么。

二、为什么:容器编排是状态管理问题

脚本的天花板

手写脚本是第一反应。启动脚本、监控脚本、重启脚本——每家公司都写过这套东西。但脚本有一个根本性的局限:它描述的是步骤序列,不是期望状态

# 典型的运维脚本
docker run -d --name app myapp:v1
if ! docker inspect app; then
  docker restart app
fi

步骤执行完了,系统不一定在你期望的状态。容器在跑,但应用 crash loop 了怎么办?节点宕机了,谁来在另一台机器上把容器重新拉起来?脚本只能覆盖你预见到的场景,而分布式系统的失败模式是无穷的。

脚本的天花板不是”写得不够好”,是命令式思维在规模下的必然崩溃。

核心矛盾:期望状态与现实状态的持续偏差

大规模运行容器的核心矛盾是:你想要的状态和系统实际处于的状态,会持续产生偏差

节点会宕机,容器会 OOM,镜像会拉取失败,网络会抖动。每一次偏差,如果靠人工介入纠正,运维成本随着服务规模线性增长——最终你需要的运维工程师数量会和服务数量一样多。

解法不是”更快地发现偏差然后人工修复”,而是让系统自己持续地把偏差收敛回去

声明式翻转:谁承担复杂性

Kubernetes 的回答是一个认知翻转:不要告诉系统做什么,告诉系统世界应该是什么样子。

# 命令式思维(伪代码脚本):
启动 3 个 myapp:v1 容器
如果某个容器挂了,重启它
把流量均匀分配到这 3 个容器
 
# 声明式思维(Kubernetes YAML):
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 3          # 我要 3 个副本
  selector:
    matchLabels:
      app: myapp
  template:
    spec:
      containers:
      - name: myapp
        image: myapp:v1

你提交 YAML,描述的是期望状态(desired state)。Kubernetes 观察现实状态(observed state),发现偏差就采取行动。pod 崩了,重建;节点下线,重新调度;副本数不够,补足——自愈不是魔法,是这个收敛循环在运行

复杂性没有消失,而是从”运维工程师脑子里的步骤序列”转移到了”控制器里的收敛逻辑”——这个转移让复杂性变得可复用、可测试、可扩展。

最终一致为什么比事务性更有韧性

kubectl apply 成功返回,不代表 pod 在跑,只代表 API Server 接受了你的声明。这看起来像是缺陷,实际上是大规模系统的必然选择。

事务性(“要么全部成功,要么全部回滚”)要求全局协调——在几千个节点的集群里,任何一个全局协调操作都会成为性能瓶颈和单点故障。最终一致(“系统会收敛到期望状态,但不保证立刻到达”)放弃了即时性,换来了韧性:局部失败不影响全局,系统可以在任意子集正常运行的情况下继续推进收敛。

代价是你必须接受一个新的心智模型:提交声明之后,你观察的不是”成功还是失败”,而是”收敛到了哪里”

三、怎么设计:控制面与数据面

Kubernetes 的架构核心是一个分离:控制面负责决策,数据面负责执行。两者通过 API 解耦,可以独立演进、独立伸缩,控制面宕了不影响已运行的工作负载。

控制面——决策的大脑

API Server:集群的唯一入口。所有组件(kubectl、Controller Manager、Scheduler、kubelet)都只和 API Server 通信,不直接互相通信。API Server 负责认证、授权、准入控制,然后把状态持久化到 etcd。它是无状态的,可以水平扩展。

etcd:集群的记忆,单一真相之源。所有的期望状态(你提交的 YAML)和观察到的状态都存在 etcd 里。etcd 用 Raft 协议保证强一致性——整个集群里只有一个”真相”,不存在”这台机器看到的和那台机器看到的不一样”。etcd 宕了,集群就失明:无法接受新的变更,但已运行的 pod 不受影响(它们运行在数据面,不依赖控制面)。

Controller Manager:控制循环的集合体。它运行着数十个控制器(Deployment 控制器、ReplicaSet 控制器、Node 控制器……),每个控制器持续 watch 自己关心的资源,发现现实状态和期望状态不一致就采取行动。控制器是无状态的——它读取的是 etcd 里的状态,不在本地保存任何东西。

Scheduler:做一件事:把还没有分配节点的 pod 分配到合适的节点上。它观察未调度的 pod,根据 pod 的资源需求(requests)、亲和性规则、污点/容忍,计算出最合适的节点,然后把绑定关系写回 API Server。Scheduler 只负责”分配到哪”,不负责”怎么跑起来”。

数据面——执行的手脚

kubelet:每个节点上的代理,Kubernetes 在节点上的”手”。它 watch API Server,一旦发现有 pod 被调度到本节点,就负责把这个 pod 的容器真正跑起来(通过 CRI 调用容器运行时)。它还持续上报节点状态和 pod 状态,让控制面知道数据面的实际情况。

kube-proxy:实现 Service 抽象的网络组件。它在每个节点上维护网络规则(传统上是 iptables 规则,现代集群越来越多用 Cilium 的 eBPF 实现),让发往 Service ClusterIP 的流量能被正确路由到后端 pod。

容器运行时:通过 CRI(Container Runtime Interface)接入,实际负责创建和管理容器。常见的有 containerd 和 CRI-O。kubelet 不关心具体用哪个运行时,只要符合 CRI 接口即可——这是 01-Container-隔离是手段,交付是目的 里 OCI 标准化的延伸。

reconciliation loop: watch → diff → act

reconciliation loop 是 Kubernetes 里最重要的设计范式,所有控制器都在跑同一个结构:

loop:
    current = 观察现实状态(从 API Server watch)
    desired = 读取期望状态(从 etcd via API Server)
    if current != desired:
        act()   # 采取行动缩小差距
    sleep(一小会儿)

这个循环的关键性质:

  • 幂等:同样的输入产出同样的结果,多跑几次没有副作用
  • 自愈:任何外力造成的偏差(节点宕机、手动修改、网络抖动),下一次循环都会被发现并纠正
  • 可扩展:你可以写自己的控制器,监听自己定义的资源类型——这是 Operator 模式的基础

从 kubectl apply 到 pod 运行:请求的完整旅程

sequenceDiagram
    autonumber
    participant Dev as 开发者 (kubectl)
    participant API as API Server
    participant etcd as etcd
    participant CM as Controller Manager
    participant Sched as Scheduler
    participant KL as kubelet (节点)

    Dev->>API: kubectl apply -f deployment.yaml
    API->>API: 认证 → 授权 → 准入控制
    API->>etcd: 写入 Deployment 对象
    CM->>API: Watch 到新 Deployment
    CM->>etcd: 创建 ReplicaSet
    CM->>etcd: 创建 3 个 Pod(nodeName 为空)
    Sched->>API: Watch 到未调度 Pod
    Sched->>etcd: 绑定节点,写入 nodeName
    KL->>API: Watch 到分配给本节点的 Pod
    KL->>KL: 通过 CRI 启动容器
    KL->>API: 更新 Pod 状态 → Running

整条链路里,组件之间没有直接通信——全部通过 API Server 读写 etcd 完成协调。这是 Kubernetes 能水平扩展且故障隔离的关键。

四、上手:在集群里把第一个应用跑起来

准备集群:kind / k3s / minikube 怎么选

本地开发和学习阶段,不需要真正的多节点集群:

工具适合场景特点
kindCI 环境、本地测试用 Docker 容器模拟节点,启动快,支持多节点拓扑
k3s边缘 / 低资源环境、也适合本地轻量化 K8s,内存占用极低,适合 Linux 环境
minikube本地开发、需要插件生态功能最全,支持最多插件,但资源消耗较大

推荐从 kind 开始——启动最快,和真实 K8s 行为最接近,适合跟着本文做实验:

# 安装 kind(macOS)
brew install kind kubectl
 
# 创建一个单节点集群
kind create cluster --name my-cluster
 
# 验证集群正常
kubectl cluster-info
kubectl get nodes

第一个 Deployment:写 YAML,apply,查状态

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: nginx:1.25-alpine    # 用 nginx 做示例
        ports:
        - containerPort: 80
# 提交声明
kubectl apply -f deployment.yaml
 
# 观察 Deployment 状态(READY 显示几个副本就绪)
kubectl get deployment myapp
# NAME    READY   UP-TO-DATE   AVAILABLE
# myapp   3/3     3            3
 
# 查看 Pod 列表
kubectl get pods -l app=myapp
# NAME                     READY   STATUS    RESTARTS
# myapp-7d6b9c8f4-2xk9p   1/1     Running   0
# myapp-7d6b9c8f4-5nvml   1/1     Running   0
# myapp-7d6b9c8f4-r8qwt   1/1     Running   0

基础四件套:get / describe / logs / exec

这四个命令覆盖了日常操作的 80%:

# get:看资源的当前状态(快速概览)
kubectl get pods                          # 列出所有 Pod
kubectl get pods -o wide                  # 加上节点 IP、所在节点
kubectl get all                           # 列出所有资源类型
 
# describe:看资源的详细信息和事件(排查问题的第一步)
kubectl describe pod myapp-7d6b9c8f4-2xk9p
# 输出包含:容器状态、资源限制、挂载的卷、最近的 Events
# Events 部分是排查问题最有价值的信息
 
# logs:看容器日志
kubectl logs myapp-7d6b9c8f4-2xk9p        # 当前日志
kubectl logs -f myapp-7d6b9c8f4-2xk9p     # 实时追踪
kubectl logs myapp-7d6b9c8f4-2xk9p -p     # 查看上一个容器的日志(容器重启后)
 
# exec:进入容器执行命令
kubectl exec -it myapp-7d6b9c8f4-2xk9p -- /bin/sh
kubectl exec myapp-7d6b9c8f4-2xk9p -- env   # 查看环境变量

暴露服务:ClusterIP / NodePort / LoadBalancer 各自的场景

Pod 有 IP,但 Pod 的 IP 是不稳定的——Pod 重启后 IP 会变。Service 提供稳定的访问入口,把流量路由到匹配 label 的 Pod。

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
spec:
  selector:
    app: myapp          # 匹配带这个 label 的所有 Pod
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP       # 默认类型

三种 Service 类型的使用场景:

类型访问范围典型场景
ClusterIP只能集群内部访问服务间调用(后端调数据库、服务 A 调服务 B)
NodePort通过节点 IP + 端口从外部访问开发测试、没有云负载均衡器的环境
LoadBalancer云厂商自动创建外部负载均衡器生产环境对外暴露服务
kubectl apply -f service.yaml
kubectl get svc myapp-svc
 
# 本地测试访问(kind 环境)
kubectl port-forward svc/myapp-svc 8080:80
curl http://localhost:8080

验证自愈:delete pod,看控制器重建

# 记下当前 Pod 名称
kubectl get pods -l app=myapp
 
# 删掉一个 Pod
kubectl delete pod myapp-7d6b9c8f4-2xk9p
 
# 立刻再看——新的 Pod 已经在创建中
kubectl get pods -l app=myapp
# NAME                     READY   STATUS              RESTARTS
# myapp-7d6b9c8f4-5nvml   1/1     Running             0
# myapp-7d6b9c8f4-r8qwt   1/1     Running             0
# myapp-7d6b9c8f4-xk2mp   0/1     ContainerCreating   0   ← 新 Pod

这就是 reconciliation loop 最直观的体现:你删了一个 Pod,ReplicaSet 控制器在下一次循环里发现”现在只有 2 个副本,期望是 3 个”,立刻创建新 Pod 来补足。自愈不需要你写任何逻辑,是声明的自然结果。

五、走向生产:让应用跑得稳

配置与密钥:ConfigMap vs Secret

应用的配置(数据库地址、日志级别)和密钥(数据库密码、API Token)不能硬编码在镜像里——镜像要在不同环境复用,密钥更不能进镜像(进了 Git 就泄露了)。

ConfigMap 存非敏感配置:

apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "info"
  DATABASE_HOST: "postgres-svc:5432"

Secret 存敏感数据(base64 编码,不是加密——真正的安全需要配合 Vault 或云 KMS):

apiVersion: v1
kind: Secret
metadata:
  name: myapp-secret
type: Opaque
data:
  DATABASE_PASSWORD: cGFzc3dvcmQxMjM=   # base64("password123")

注入方式有两种,各有适用场景:

# 方式一:环境变量注入(适合少量配置,启动时读取)
env:
- name: APP_ENV
  valueFrom:
    configMapKeyRef:
      name: myapp-config
      key: APP_ENV
- name: DATABASE_PASSWORD
  valueFrom:
    secretKeyRef:
      name: myapp-secret
      key: DATABASE_PASSWORD
 
# 方式二:文件挂载(适合配置文件,支持热更新 ConfigMap)
volumeMounts:
- name: config-vol
  mountPath: /app/config
volumes:
- name: config-vol
  configMap:
    name: myapp-config

环境变量注入简单直接,但 ConfigMap 更新后需要重启 Pod 才能生效;文件挂载在 ConfigMap 更新后会自动同步到容器内的文件(但应用需要自己 watch 文件变化并热加载)。

健康检查:liveness / readiness / startup 的区别

三种探针解决三个不同的问题,混淆它们是生产事故的常见来源:

探针失败时的动作解决的问题
livenessProbe重启容器应用死锁、进程卡死——容器进程还活着但应用已经无响应
readinessProbe从 Service 的 Endpoints 里摘除这个 Pod应用还没准备好接流量——启动中、热身中、依赖不可用
startupProbe在成功前抑制 liveness 探针启动慢的应用——避免被 liveness 在启动期间误杀
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3       # 连续失败 3 次才重启,避免抖动误杀
 
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3       # 失败时从 Service 摘除,成功后重新加入
 
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30      # 启动期间允许失败 30 次(即最多等 300s)
  periodSeconds: 10

典型事故:把 readinessProbe 路径指向一个有副作用的接口,导致探针每次调用都触发业务逻辑。或者 livenessProbe 的 initialDelaySeconds 设得太短,应用还在启动就被探针判定为失败,触发无限重启循环(CrashLoopBackOff 的常见原因之一)。

资源管理:requests vs limits

resources:
  requests:
    memory: "128Mi"   # Scheduler 看这个来决定调度到哪个节点
    cpu: "250m"       # 250 毫核 = 0.25 个 CPU
  limits:
    memory: "512Mi"   # 内存超过这个值,OOM killer 直接杀容器(exit 137)
    cpu: "500m"       # CPU 超过这个值,内核限速(throttle),不杀容器

核心区别

  • requests调度依据——Scheduler 把 Pod 调度到”剩余资源 ≥ requests”的节点上。不设 requests,Pod 可能被调度到已经过载的节点。
  • limits运行时约束——内存 limit 是硬限制(超出即死),CPU limit 是软限制(超出被限速)。
  • 不设 limits,单个 Pod 可以吃光节点所有资源,影响同节点其他 Pod。

生产环境的原则:requests 和 limits 都必须设,limits 不要设得过小(会造成频繁 OOM 重启)也不要设得过大(资源浪费且影响调度)。

滚动更新与回滚

Kubernetes 默认的更新策略是滚动更新(RollingUpdate):新 Pod 逐步起来,旧 Pod 逐步销毁,全程服务不中断。

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1           # 更新过程中最多多出 1 个 Pod(超出 replicas)
      maxUnavailable: 0     # 更新过程中不允许有不可用的 Pod(零停机)
# 更新镜像版本
kubectl set image deployment/myapp myapp=myapp:v2
 
# 观察滚动更新过程
kubectl rollout status deployment/myapp
 
# 查看发布历史
kubectl rollout history deployment/myapp
 
# 回滚到上一个版本
kubectl rollout undo deployment/myapp
 
# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=2

回滚的本质是声明式的:rollout undo 把 Deployment 的 spec 改回之前的版本,控制器重新执行一次滚动更新流程,把集群收敛到旧版本的状态。

水平扩缩容

# 手动扩缩容
kubectl scale deployment myapp --replicas=5
 
# 查看当前副本数
kubectl get deployment myapp

HPA(Horizontal Pod Autoscaler) 根据指标自动扩缩:

apiVersion: autoscaling/v2
kind: HPA
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70    # CPU 使用率超过 70% 触发扩容

HPA 依赖 metrics-server 提供实时指标。触发逻辑:当所有 Pod 的 CPU 平均使用率超过 70%,HPA 计算需要多少副本才能把使用率降下来,然后更新 Deployment 的 replicas 字段——同样是声明式的,由控制器完成收敛。

六、故障剧场:亲手把它弄坏

实验一:kill pod,自愈现形

# 开一个终端持续观察
kubectl get pods -l app=myapp -w
 
# 另一个终端删掉一个 Pod
kubectl delete pod <pod-name>
 
# 观察第一个终端的输出:
# pod-xxx   1/1   Running   0   → Terminating → (消失)
# pod-yyy   0/1   Pending   0   → ContainerCreating → Running

从删除到新 Pod Running,通常在 10 秒以内。这个速度不是靠轮询,是靠 watch——控制器在 Pod 消失的瞬间就收到通知并采取行动。

实验二:停掉 controller-manager,控制面失明,数据面存活

# 在 kind 集群里,控制面组件以 static pod 运行
# 找到 controller-manager 的 Pod
kubectl get pods -n kube-system | grep controller-manager
 
# 删掉它(它是 static pod,kubelet 会重建——所以要快速观察)
kubectl delete pod -n kube-system kube-controller-manager-my-cluster-control-plane
 
# 在 controller-manager 重建之前,删一个应用 Pod
kubectl delete pod <app-pod-name>
 
# 观察:应用 Pod 不会被重建——控制器不在了,没人执行收敛循环
kubectl get pods -l app=myapp
# 只剩 2 个 Pod,replicas=3 的声明没有人去执行
 
# controller-manager 恢复后,缺失的 Pod 会立刻被创建出来

这个实验证明了控制面/数据面分离的真实含义:已经运行的 Pod 不依赖控制面——流量还在走,服务还活着;但新的调度和自愈需要控制面在线。

实验三:破坏 etcd quorum,单一真相消失

etcd 用 Raft 协议,需要超过半数节点(quorum)才能继续服务写请求。三节点集群允许 1 个节点失败,五节点集群允许 2 个节点失败。

在本地环境模拟 quorum 损失比较复杂,但理解现象比实操更重要:

  • etcd quorum 丢失后,API Server 无法写入新的状态变更——kubectl apply 会挂起或报错
  • 已运行的 Pod 继续运行(数据面不依赖 etcd)
  • 控制器无法更新状态——新的 Pod 创建、Service Endpoint 更新全部停止
  • kubectl get 可能仍然返回(读 etcd 快照),但数据可能是旧的

etcd 是集群的”记忆”——记忆丢了,集群失去了判断过去发生了什么的能力,但身体(数据面)还在继续运作。 这正是备份 etcd 在生产运维里是第一优先级的原因。

实验四:手动 kubectl edit,看控制器把你改回去

# 直接编辑 Deployment,把 replicas 改成 1
kubectl edit deployment myapp
# 在编辑器里把 replicas: 3 改成 replicas: 1,保存
 
# 立刻观察——Pod 数量减少到 1
 
# 现在再把它改成 5
kubectl edit deployment myapp
# replicas: 5,保存
 
# Pod 数量扩展到 5
kubectl get pods -l app=myapp

这个实验方向反过来做更有意思:如果你直接 kubectl edit 的是 ReplicaSet 而不是 Deployment,会发生什么?

# 找到 ReplicaSet
kubectl get replicaset
 
# 编辑 ReplicaSet 的 replicas(比如改成 1)
kubectl edit replicaset <rs-name>
 
# 你会发现 Pod 减少了,但几秒后又回到了 3 个
# 因为 Deployment 控制器在 watch ReplicaSet——
# 它发现 ReplicaSet 的 replicas 和 Deployment spec 不一致,把它改回去了

声明即法律,控制器是执法者——你手动改了 ReplicaSet,Deployment 控制器把它改回来。这是 Kubernetes 最直接的哲学体现:期望状态存在 Deployment 里,任何对中间层的修改都会被上层控制器纠正。

七、日常排障:K8s 坏了怎么查

Pod 起不来:状态速查

# 第一步:看 Pod 处于什么状态
kubectl get pods
kubectl describe pod <pod-name>    # Events 部分是排查起点

Pending:Pod 被创建了,但还没有被调度到节点上。

kubectl describe pod <pod-name> | grep -A 10 "Events"

常见原因:

  • Insufficient cpu/memory:没有节点有足够资源满足 requests
  • node(s) had untolerated taint:所有节点都有 Pod 不容忍的污点
  • 0/3 nodes are available:集群没有可用节点

ImagePullBackOff / ErrImagePull:镜像拉取失败。

kubectl describe pod <pod-name> | grep -A 5 "Failed"

常见原因:镜像名字/标签拼错;私有 Registry 未配置 imagePullSecret;网络无法访问 Registry。

# 配置私有 Registry 认证
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass

CrashLoopBackOff:容器反复崩溃重启。每次重启等待时间指数级增长(10s → 20s → 40s → 最大 5 分钟)。

# 看当前容器的日志
kubectl logs <pod-name>
 
# 看上一次崩溃的日志(最有用——崩溃时的最后输出)
kubectl logs <pod-name> --previous
 
# 看容器退出码
kubectl describe pod <pod-name> | grep "Exit Code"

常见原因:应用启动报错(看日志);CMD 命令不存在;livenessProbe 设置太激进导致健康检查启动期间失败;OOMKilled(内存 limit 太小)。

OOMKilled:容器因内存超限被内核杀死。

kubectl describe pod <pod-name> | grep -i "OOMKilled\|exit code\|last state"
# Last State: Terminated  Reason: OOMKilled  Exit Code: 137

解法:调高 resources.limits.memory,或排查内存泄漏。

服务不通:逐层排查

# 第一层:Pod 本身是通的吗?
kubectl exec <pod-name> -- curl -s http://localhost:8080/health
 
# 第二层:Service 的 Endpoints 里有 Pod 吗?
kubectl get endpoints myapp-svc
# 如果 ENDPOINTS 是 <none>,说明没有 Pod 匹配 Service 的 selector
 
# 检查 selector 是否和 Pod 的 label 一致
kubectl get svc myapp-svc -o yaml | grep selector -A 5
kubectl get pods --show-labels
 
# 第三层:集群内 DNS 是否正常?
kubectl exec <any-pod> -- nslookup myapp-svc
kubectl exec <any-pod> -- curl http://myapp-svc/health
 
# 第四层:如果有 Ingress,检查 Ingress 规则和 Controller 状态
kubectl describe ingress <ingress-name>
kubectl get pods -n ingress-nginx    # 检查 Ingress Controller 是否正常

常见原因:Service selector 和 Pod label 不匹配(最常见);Pod 的 readinessProbe 失败导致从 Endpoints 被摘除;容器监听的是 127.0.0.1 而不是 0.0.0.0

节点异常:NotReady 的处理

# 查看节点状态
kubectl get nodes
# NAME                      STATUS     ROLES
# my-cluster-control-plane  Ready      control-plane
# my-cluster-worker         NotReady   <none>        ← 异常
 
# 查看节点详情
kubectl describe node my-cluster-worker | grep -A 20 "Conditions"
# 关注 MemoryPressure / DiskPressure / PIDPressure / Ready 四个条件
 
# 常见压力信号
# MemoryPressure: True   → 节点内存不足,kubelet 开始驱逐 Pod
# DiskPressure: True     → 磁盘空间不足,清理镜像或日志

节点 NotReady 时,Kubernetes 会等待 node-monitor-grace-period(默认 40 秒),确认节点确实不可用后,把节点上的 Pod 标记为 Unknown,再等 pod-eviction-timeout(默认 5 分钟)后驱逐这些 Pod 到其他节点。

第一现场:kubectl get events

describe 命令里的 Events 只显示特定资源的事件。kubectl get events 显示整个 namespace 里的所有事件——出了问题先看这里:

# 按时间倒序查看最近的事件
kubectl get events --sort-by='.lastTimestamp'
 
# 只看 Warning 级别
kubectl get events --field-selector type=Warning
 
# 看特定 Pod 的事件
kubectl get events --field-selector involvedObject.name=<pod-name>

Events 是 Kubernetes 的”系统日志”——调度失败、镜像拉取失败、探针失败、OOM kill,都会在这里留下记录。

八、往哪走

Operator 模式:reconciliation loop 带给你自己的领域

Kubernetes 内置的控制器管理 Deployment、Service、ConfigMap 这些通用资源。但如果你要管理一个 PostgreSQL 集群的主从切换,或者一个 Kafka 的 topic 配置——这些业务特定的运维逻辑,内置控制器不懂。

Operator 模式的回答是:用 CRD(Custom Resource Definition)扩展 Kubernetes 的 API,定义你自己的资源类型(比如 PostgresCluster),再写一个自定义控制器 watch 这个资源,执行你的业务运维逻辑。reconciliation loop 这个范式不只是 Kubernetes 内部的实现细节,是一个可以被任何人复用的设计模式。

AI/GPU 工作负载与 DRA

Kubernetes 正在从”微服务编排器”固化为”分布式系统的内核”,而当前最大的压力来自 AI 工作负载。

GPU 和其他加速器的调度问题远比 CPU 复杂:一块 GPU 不能被任意切分;不同型号的 GPU 对模型推理有不同影响;训练任务需要多卡通信拓扑感知。DRA(Dynamic Resource Allocation)是 Kubernetes 对这个问题的系统性回答——让加速器像 CPU 一样被声明式地请求和分配,由驱动实现者定义具体的分配语义。这是 K8s 1.26 引入并在持续演进的特性,也是理解”K8s 如何接管 AI 基础设施”的入口。

这条线的终点:Kubernetes 是这个系列后续所有文章的地基——IaC 管理 K8s 集群本身,GitOps 把变更同步到 K8s,平台工程在 K8s 之上建 IDP,服务网格运行在 K8s 上,eBPF 优化 K8s 的数据平面。理解 K8s 的控制循环和声明式 API,就理解了这个体系其余部分的设计语言。

关联