# k8s 工作负载管理


## 导读

Pod 是 Kubernetes 中最小的调度单元，但直接管理零散的 Pod 既无法保证副本数量，也难以应对滚动更新与故障恢复。工作负载控制器在 Pod 之上抽象出一层管理逻辑，把"期望状态"与"实际状态"的调和交给控制平面完成。掌握 Deployment、StatefulSet、DaemonSet、Job 四种核心控制器的适用边界与协作机制，是在生产集群中稳定运行业务的前提。

## 核心概念

Deployment 是最常用的无状态工作负载控制器，通过管理 ReplicaSet 间接管理 Pod。它声明期望的副本数与 Pod 模板，控制器持续比对实际状态，发现副本数偏差或 Pod 异常退出时自动创建新 Pod 补足。Deployment 的核心价值在于滚动更新与回滚：每次修改 Pod 模板会生成新的 ReplicaSet，旧 ReplicaSet 保留但副本数归零，从而保留版本历史。新版本出现问题时，可通过 `kubectl rollout undo` 快速回到上一个稳定版本。

StatefulSet 面向有状态应用，解决 Deployment 无法处理的三类需求：稳定的网络标识、有序的部署与扩缩容、持久存储与 Pod 的绑定。StatefulSet 创建的 Pod 拥有可预测的名称（如 web-0、web-1），配合 Headless Service 可获得稳定的 DNS 记录。部署时 Pod 按 0、1、2 的顺序逐个创建，销毁时按逆序回收，每个 Pod 绑定独立的 PVC，即使 Pod 重新调度，存储依然跟随。这些特性使 StatefulSet 成为运行 MySQL、Redis Cluster、ZooKeeper 等分布式存储与协调服务的首选。

DaemonSet 保证在每个（或符合条件的）节点上运行一个 Pod 副本。节点加入集群时控制器自动创建 Pod，节点移除时 Pod 自动回收。典型场景集中在节点级基础设施：日志采集（Fluentd、Filebeat）、网络插件（Calico、Cilium）、监控 Agent（Node Exporter）。这类组件需要靠近节点资源运行，用 Deployment 难以覆盖所有节点，DaemonSet 天然适配。

Job 与 CronJob 面向批处理与定时任务。Job 创建一个或多个 Pod，保证任务成功完成后才视为结束，失败按策略重试。CronJob 在 Job 之上叠加 cron 格式的调度计划，按周期触发 Job，适合数据库备份、报表生成等周期性任务。两者都不追求长期运行，而以"完成"为目标。

Deployment 的滚动更新是一个典型的状态机推进过程。控制器收到模板变更后，先创建新的 ReplicaSet，再按 maxSurge 与 maxUnavailable 参数交替扩容新 Pod 与缩容旧 Pod，直到旧副本归零、新副本达到目标数。整个过程对调用者透明，但理解其内部交互有助于排查卡在中间状态的更新。

```mermaid
sequenceDiagram
    participant U as 用户
    participant DC as Deployment Controller
    participant RSN as 新 ReplicaSet
    participant RSO as 旧 ReplicaSet
    participant P as Pod

    U->>DC: kubectl apply 更新镜像
    DC->>RSN: 创建新 ReplicaSet (replicas=0)
    DC->>RSN: 扩容新 RS (0→1)
    RSN->>P: 创建新 Pod (v2)
    P-->>DC: Ready
    DC->>RSO: 缩容旧 RS (3→2)
    DC->>RSN: 扩容新 RS (1→2)
    RSN->>P: 创建新 Pod (v2)
    P-->>DC: Ready
    DC->>RSO: 缩容旧 RS (2→1)
    DC->>RSN: 扩容新 RS (2→3)
    RSN->>P: 创建新 Pod (v2)
    P-->>DC: Ready
    DC->>RSO: 缩容旧 RS (1→0)
    DC-->>U: 滚动更新完成
```

图 1：Deployment 滚动更新的交互时序。控制器在扩容新 ReplicaSet 与缩容旧 ReplicaSet 之间交替推进，每一步等待新 Pod 就绪后才继续缩容旧副本，从而在更新过程中维持可用副本数下限。若新 Pod 始终无法就绪，更新会停滞在中间状态，此时需要结合 `kubectl rollout status` 与事件排查根因。

## 图解原理

四种控制器各有定位，选型错误往往是生产事故的源头。判断顺序应从应用是否需要状态切入：无状态服务优先选 Deployment，它简单可靠且支持弹性扩缩容；有状态服务需要稳定标识或持久存储，则必须用 StatefulSet。如果需求是每个节点都要运行一个副本，无论有状态与否，DaemonSet 是唯一合适的选择；而一次性或周期性任务，则交给 Job 与 CronJob。

```mermaid
flowchart TD
    A[管理工作负载] --> B{无状态服务?}
    B -->|是| C[推荐 Deployment]
    B -->|否| D{需要稳定网络标识<br/>或持久存储绑定?}
    D -->|是| E[推荐 StatefulSet]
    D -->|否| F{每个节点运行一个副本?}
    F -->|是| G[推荐 DaemonSet]
    F -->|否| H{一次性或周期性任务?}
    H -->|是| I[推荐 Job / CronJob]
    H -->|否| J[考虑裸 Pod 或自定义控制器]

    style C fill:#d4edda
    style E fill:#d4edda
    style G fill:#d4edda
    style I fill:#d4edda
```

图 2：工作负载选型决策树。按"无状态 → 有状态 → 每节点运行 → 批处理任务"的顺序逐层判断，每一层命中即终止，避免混淆控制器的职责边界。实际项目中可能存在交叉需求，例如 DaemonSet 运行的 Agent 也可能需要持久配置，但只要核心诉求是覆盖所有节点，就应优先满足这一约束。

StatefulSet 虽然功能强大，但运维成本高于 Deployment：扩缩容速度受顺序约束、滚动更新串行推进、存储回收需要手动干预。在能用 Deployment 解决的场景下，不要为了看起来更严谨而选用 StatefulSet。同理，Job 的并行度与重试策略需要根据任务特性精细配置，默认值并不总是合适。

## 动手实操

### Deployment 基本操作

创建一个 Nginx Deployment，副本数为 3：

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
```

应用并查看状态：

```bash
kubectl apply -f nginx-deploy.yaml
kubectl get deploy,rs,pod -l app=nginx
```

触发滚动更新，将镜像升级到 1.26：

```bash
kubectl set image deployment/nginx-deploy nginx=nginx:1.26
kubectl rollout status deployment/nginx-deploy
```

查看历史版本并回滚到指定修订：

```bash
kubectl rollout history deployment/nginx-deploy
kubectl rollout undo deployment/nginx-deploy --to-revision=1
```

### StatefulSet 部署

StatefulSet 必须配合 Headless Service 提供稳定网络标识。以下示例运行三副本 Redis，每个 Pod 绑定独立 PVC：

```yaml
apiVersion: v1
kind: Service
metadata:
  name: redis-headless
spec:
  clusterIP: None
  selector:
    app: redis
  ports:
  - port: 6379
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis-headless
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7
        ports:
        - containerPort: 6379
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
```

部署后观察 Pod 的有序创建与命名规则：

```bash
kubectl apply -f redis-statefulset.yaml
kubectl get pod -l app=redis -w
# 输出按 redis-0、redis-1、redis-2 顺序依次就绪
kubectl run dns-test --image=busybox --rm -it -- nslookup redis-0.redis-headless
```

### DaemonSet 部署

以下 DaemonSet 在每个节点运行一个 Fluentd 日志采集器，挂载节点日志目录：

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      containers:
      - name: fluentd
        image: fluent/fluentd:v1.16
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
```

查看每个节点的 Pod 分布情况：

```bash
kubectl apply -f fluentd-daemonset.yaml
kubectl get pod -n kube-system -l app=fluentd -o wide
```

### Job 与 CronJob

Job 示例，执行一次数据库迁移并允许失败重试：

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 4
  template:
    spec:
      restartPolicy: OnFailure
      containers:
      - name: migrate
        image: myapp/migrate:latest
        command: ["./migrate", "up"]
```

CronJob 示例，每天凌晨 2 点执行数据库备份：

```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: backup
            image: myapp/backup:latest
            command: ["./backup.sh"]
```

提交并观察任务执行结果：

```bash
kubectl apply -f db-migrate-job.yaml
kubectl get job
kubectl logs job/db-migrate

kubectl apply -f db-backup-cronjob.yaml
kubectl get cronjob
```

## 常见问题与避坑

### 滚动更新卡住

新 Pod 长时间未就绪会阻塞整个更新流程。根因通常是 readinessProbe 配置过严或镜像拉取失败，用 `kubectl describe pod` 查看事件可以定位。**不要直接删除卡住的 ReplicaSet**，应通过 `kubectl rollout undo` 回退到上一个版本。必要时调大 `progressDeadlineSeconds` 让控制器及时判定更新失败。

### StatefulSet 存储残留

StatefulSet 删除后 PVC 默认保留，反复部署测试会累积大量孤儿卷。生产环境这是保护机制，测试环境可配置 `persistentVolumeClaimRetentionPolicy` 在删除时自动回收。**缩容有状态服务前需确认数据同步完成**，否则逆序销毁 Pod 可能导致数据不一致。例如 MySQL 主从架构中，缩容前应先验证从库复制延迟归零。

### Job 不退出

任务逻辑未正确返回退出码或陷入死循环时，Job 永远不会标记为完成。**务必为 Job 设置执行时长上限**，通过 `activeDeadlineSeconds` 限制单次运行时间，同时配置 `backoffLimit` 防止无限重试。CronJob 还需关注 `concurrencyPolicy`，默认 Allow 会叠加执行多个任务实例，对不支持并发的业务应改为 Forbid。

### DaemonSet 资源争抢

DaemonSet Pod 与业务 Pod 共享节点资源，若不设置资源请求与限制，日志或监控 Agent 在高负载时可能挤占业务容器。建议为 DaemonSet Pod 配置较低优先级的 PriorityClass，或使用 Guaranteed QoS 配合合理 limit。通过 `nodeSelector` 或 `tolerations` 控制覆盖范围，避免在不该运行的节点上启动。

## 小结与进阶

工作负载控制器的本质是把期望状态声明出来，由控制平面持续调谐实际状态，选对控制器是稳定运行的第一步。下一篇将进入服务暴露与网络通信，讨论 Service、Ingress 与 DNS 如何把分散的 Pod 组织成可访问的服务。

