目录

Kubernetes-04 工作负载详解

Kubernetes 工作负载详解

系列第四篇。工作负载(Workload)是在 K8s 上运行的应用程序。K8s 提供多种内置工作负载资源,适用于不同的应用场景。


目录

  1. 工作负载概览
  2. Deployment
  3. ReplicaSet
  4. StatefulSet
  5. DaemonSet
  6. Job
  7. CronJob
  8. HorizontalPodAutoscaler(HPA)
  9. 工作负载选型指南
  10. 小结

1. 工作负载概览

工作负载类型
├── Deployment        → 无状态应用(Web 服务、API 服务)★ 最常用
├── StatefulSet       → 有状态应用(数据库、消息队列)
├── DaemonSet         → 每个节点都要跑一个(日志收集、监控 Agent)
├── Job               → 一次性任务(数据处理、批量导入)
└── CronJob           → 定时任务(定时报表、定时清理)

2. Deployment

Deployment 是最常用的工作负载类型,管理无状态应用

2.1 核心特性

  • 声明式更新:修改 spec 后自动滚动升级
  • 副本管理:保持指定数量的 Pod 副本
  • 滚动升级:零停机更新
  • 回滚:一键回滚到之前版本
  • 扩缩容:手动或自动调整副本数

2.2 完整 Deployment 示例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-api
  namespace: production
  labels:
    app: go-api
  annotations:
    kubernetes.io/change-cause: "升级到 v1.2.0,修复内存泄漏"
spec:
  replicas: 3                  # 副本数

  selector:
    matchLabels:
      app: go-api              # 必须与 template.metadata.labels 匹配

  # 升级策略
  strategy:
    type: RollingUpdate        # RollingUpdate(默认)| Recreate
    rollingUpdate:
      maxSurge: 1              # 升级过程中最多多出几个 Pod(可以是数字或百分比)
      maxUnavailable: 0        # 升级过程中最多有几个 Pod 不可用(0 表示始终保持 replicas 个可用)

  # Pod 稳定性(等待这么久才开始对下一个 Pod 升级)
  minReadySeconds: 10

  # 保留历史版本数(用于回滚)
  revisionHistoryLimit: 5

  template:                    # Pod 模板
    metadata:
      labels:
        app: go-api
        version: "1.2.0"
    spec:
      # 优雅终止超时(SIGTERM 后等待这么久再强制 SIGKILL)
      terminationGracePeriodSeconds: 30

      containers:
      - name: go-api
        image: myregistry.io/go-api:v1.2.0
        ports:
        - containerPort: 8080

        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"

        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10

      # Pod 拓扑分散约束(打散到不同 Zone/Node)
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: go-api

2.3 Deployment 操作

# 创建
kubectl apply -f deployment.yaml

# 查看
kubectl get deployment go-api
kubectl get deploy go-api -o wide

# 查看滚动更新状态
kubectl rollout status deployment/go-api

# 查看历史版本
kubectl rollout history deployment/go-api
kubectl rollout history deployment/go-api --revision=2

# 手动触发更新(修改镜像)
kubectl set image deployment/go-api go-api=myregistry.io/go-api:v1.3.0

# 回滚到上一版本
kubectl rollout undo deployment/go-api

# 回滚到指定版本
kubectl rollout undo deployment/go-api --to-revision=2

# 暂停升级(用于多次修改后统一升级)
kubectl rollout pause deployment/go-api
# 恢复
kubectl rollout resume deployment/go-api

# 手动扩缩容
kubectl scale deployment go-api --replicas=5

# 删除
kubectl delete deployment go-api

2.4 滚动升级详解

初始状态:3 个 v1 Pod
[v1] [v1] [v1]

maxSurge=1, maxUnavailable=0 的升级过程:

Step 1: 创建 1 个新 v2 Pod(超出 replicas,但不超过 maxSurge)
[v1] [v1] [v1] [v2(pending)]

Step 2: v2 Pod 就绪后,终止 1 个 v1 Pod
[v1] [v1] [v2]

Step 3: 再创建 1 个 v2
[v1] [v1] [v2] [v2(pending)]

Step 4: v2 就绪,终止 v1
[v1] [v2] [v2]

Step 5: 同上...
[v2] [v2] [v2]  ← 升级完成

2.5 Go 应用的优雅终止

K8s 删除 Pod 时的流程:

1. Pod 进入 Terminating 状态
2. 从 Service Endpoints 中移除(新请求不再路由到这个 Pod)
3. 发送 SIGTERM 信号给容器的 PID 1
4. 等待 terminationGracePeriodSeconds(默认 30s)
5. 如果还没退出,发送 SIGKILL 强制杀死

Go 应用处理优雅关闭:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    mux.HandleFunc("/healthz", healthz)

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // 启动服务器
    go func() {
        log.Println("Server starting on :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("HTTP server error: %v", err)
        }
    }()

    // 等待信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
    sig := <-quit
    log.Printf("Received signal: %v, shutting down...", sig)

    // 优雅关闭(等待进行中的请求完成)
    ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Printf("HTTP server shutdown error: %v", err)
    }

    log.Println("Server stopped gracefully")
}

3. ReplicaSet

ReplicaSet 确保指定数量的 Pod 副本始终运行。

你通常不需要直接使用 ReplicaSet,Deployment 会自动管理它。

# 查看 Deployment 创建的 ReplicaSet
kubectl get replicaset
# NAME                   DESIRED   CURRENT   READY   AGE
# go-api-7d9b4c9b8d      3         3         3       10m   ← 当前版本
# go-api-6c8b9d7f6a      0         0         0       20m   ← 上一版本(用于回滚)

4. StatefulSet

StatefulSet 管理有状态应用,与 Deployment 的核心区别:

特性 Deployment StatefulSet
Pod 名称 随机哈希(go-api-7d9b4c-xk2lp) 稳定有序(mysql-0, mysql-1)
网络标识 Pod IP 变化 稳定 DNS(mysql-0.mysql)
存储 多 Pod 共享或独立(需手动处理) 每个 Pod 独立 PVC(自动创建)
启动顺序 并行(无序) 顺序(0→1→2)
删除顺序 无序 逆序(2→1→0)
升级 并行滚动 逆序顺序升级

4.1 StatefulSet 示例(Redis 集群)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
  namespace: default
spec:
  serviceName: "redis"       # 必须指定,用于创建稳定 DNS
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7.2-alpine
        ports:
        - containerPort: 6379
          name: redis
        command:
        - redis-server
        - /etc/redis/redis.conf
        volumeMounts:
        - name: data
          mountPath: /data
        - name: config
          mountPath: /etc/redis
        resources:
          requests:
            cpu: "100m"
            memory: "256Mi"
          limits:
            cpu: "500m"
            memory: "1Gi"

      volumes:
      - name: config
        configMap:
          name: redis-config

  # 每个 Pod 自动创建独立的 PVC
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "fast-ssd"
      resources:
        requests:
          storage: 10Gi

对应的 Headless Service(无 ClusterIP):

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  clusterIP: None          # 关键:Headless Service
  selector:
    app: redis
  ports:
  - port: 6379
    name: redis

稳定 DNS 记录:

redis-0.redis.default.svc.cluster.local  → redis-0 的 IP
redis-1.redis.default.svc.cluster.local  → redis-1 的 IP
redis-2.redis.default.svc.cluster.local  → redis-2 的 IP

4.2 StatefulSet 操作

# 查看
kubectl get statefulset
kubectl get pods -l app=redis  # redis-0, redis-1, redis-2

# 扩容(按顺序创建新 Pod)
kubectl scale statefulset redis --replicas=5

# 升级(逆序:redis-2 先升级)
kubectl set image statefulset/redis redis=redis:7.4-alpine

# 查看升级状态
kubectl rollout status statefulset/redis

4.3 什么时候用 StatefulSet

  • 数据库:MySQL、PostgreSQL(主从复制需要稳定网络标识)
  • 分布式存储:Elasticsearch、Cassandra
  • 消息队列:Kafka(每个 Broker 有固定 ID)
  • 缓存集群:Redis Cluster
  • ZooKeeper、Consul 等需要固定节点身份的服务

注意: 真实生产环境中,数据库通常不建议跑在 K8s 中(或使用专门的 Operator,如 MySQL Operator、PG Operator),直接用 StatefulSet 运维复杂度较高。


5. DaemonSet

DaemonSet 确保每个(或指定)节点上都运行一个 Pod 副本。当节点加入集群时自动在其上创建 Pod,节点移除时对应 Pod 也被删除。

5.1 使用场景

  • 日志收集:Fluentd、Fluent Bit(每个节点的日志都需要采集)
  • 监控 Agent:Prometheus Node Exporter、Datadog Agent
  • 网络插件:Calico、Flannel(CNI 插件)
  • 存储插件:CSI Node Driver
  • 安全扫描:Falco

5.2 DaemonSet 示例(Node Exporter)

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: node-exporter
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1     # 每次更新最多有 1 个节点的 Pod 不可用

  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      hostNetwork: true      # 使用宿主机网络
      hostPID: true          # 共享宿主机 PID namespace

      # 允许调度到 control-plane 节点
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule

      containers:
      - name: node-exporter
        image: prom/node-exporter:latest
        ports:
        - containerPort: 9100
          hostPort: 9100     # 使用宿主机端口
        args:
        - --path.procfs=/host/proc
        - --path.sysfs=/host/sys
        - --path.rootfs=/host/root
        volumeMounts:
        - name: proc
          mountPath: /host/proc
          readOnly: true
        - name: sys
          mountPath: /host/sys
          readOnly: true
        - name: root
          mountPath: /host/root
          readOnly: true
        securityContext:
          privileged: true    # 需要读取宿主机信息

      volumes:
      - name: proc
        hostPath:
          path: /proc
      - name: sys
        hostPath:
          path: /sys
      - name: root
        hostPath:
          path: /

5.3 只在指定节点运行

spec:
  template:
    spec:
      # 只在有 gpu=true 标签的节点上运行
      nodeSelector:
        gpu: "true"

      # 或使用 nodeAffinity
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node-type
                operator: In
                values: [gpu-node]

6. Job

Job 管理一次性任务,确保指定数量的 Pod 成功完成。

6.1 使用场景

  • 数据处理/ETL
  • 数据库迁移
  • 批量导入
  • 生成报告
  • 发送批量邮件

6.2 Job 类型

6.2.1 单次 Job(默认)

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  # 成功完成次数(默认 1)
  completions: 1
  # 并行运行数(默认 1)
  parallelism: 1
  # 失败重试次数
  backoffLimit: 3
  # Job 完成后多久自动删除(秒)
  ttlSecondsAfterFinished: 3600

  template:
    spec:
      restartPolicy: OnFailure   # Job 必须是 OnFailure 或 Never

      containers:
      - name: migration
        image: myapp:v1.2.0
        command: ["/app/migrate", "--up", "--database", "$(DATABASE_URL)"]
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
        resources:
          requests:
            cpu: "200m"
            memory: "256Mi"

6.2.2 并行 Job(固定完成次数)

spec:
  completions: 10    # 需要成功完成 10 次
  parallelism: 3     # 同时运行 3 个 Pod

执行过程:

Round 1: [Pod1] [Pod2] [Pod3]  → 3 个完成
Round 2: [Pod4] [Pod5] [Pod6]  → 3 个完成
Round 3: [Pod7] [Pod8] [Pod9]  → 3 个完成
Round 4: [Pod10]               → 1 个完成
结果:10 次完成

6.2.3 Work Queue 模式(不固定完成次数)

spec:
  parallelism: 5     # 并行 5 个
  # 不设置 completions,靠 Pod 自己判断队列是否为空

6.3 Job 操作

kubectl get jobs
kubectl describe job db-migration
kubectl logs job/db-migration

# 查看 Job 创建的 Pod
kubectl get pods -l job-name=db-migration

7. CronJob

CronJob 按照 cron 表达式定时创建 Job。

7.1 Cron 表达式

┌─────── 分钟 (0-59)
│ ┌───── 小时 (0-23)
│ │ ┌─── 日 (1-31)
│ │ │ ┌─ 月 (1-12)
│ │ │ │ ┌ 星期 (0-7, 0和7都是周日)
│ │ │ │ │
* * * * *

示例:
0 2 * * *     → 每天凌晨 2:00
0 */6 * * *   → 每 6 小时
0 9 * * 1-5   → 工作日上午 9:00
*/5 * * * *   → 每 5 分钟
0 0 1 * *     → 每月 1 日 0:00

7.2 CronJob 示例

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
  namespace: production
spec:
  # 每天凌晨 2:00 生成报告
  schedule: "0 2 * * *"

  # 时区(K8s 1.27+)
  timeZone: "Asia/Shanghai"

  # 并发策略
  concurrencyPolicy: Forbid      # Allow(默认)| Forbid(跳过)| Replace(替换)

  # 任务开始时间超过多少秒算 missed
  startingDeadlineSeconds: 100

  # 保留成功/失败的 Job 历史数量
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1

  jobTemplate:
    spec:
      ttlSecondsAfterFinished: 86400   # Job 完成后 24h 删除
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: report
            image: myapp:v1.2.0
            command: ["/app/report", "--date", "yesterday", "--output", "s3://my-bucket/reports/"]
            env:
            - name: AWS_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: aws-secret
                  key: access-key-id
            - name: AWS_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: aws-secret
                  key: secret-access-key
            resources:
              requests:
                cpu: "500m"
                memory: "1Gi"
              limits:
                cpu: "2"
                memory: "4Gi"

7.3 CronJob 操作

# 查看 CronJob
kubectl get cronjob

# 手动触发一次(测试用)
kubectl create job --from=cronjob/daily-report manual-test-001

# 暂停 CronJob(不再创建新 Job)
kubectl patch cronjob daily-report -p '{"spec":{"suspend":true}}'

# 恢复
kubectl patch cronjob daily-report -p '{"spec":{"suspend":false}}'

# 查看历史 Job
kubectl get jobs -l app=daily-report

7.4 concurrencyPolicy 详解

Allow(默认):允许并发执行,上一次没跑完时开始新的
  适用:任务之间互不影响,且幂等

Forbid:禁止并发,上一次没跑完时跳过这次
  适用:任务不可并发(如独占数据库操作)

Replace:上一次没跑完时,终止它并开始新的
  适用:只需要最新一次结果

8. HorizontalPodAutoscaler(HPA)

HPA 根据指标自动调整 Deployment/StatefulSet 的副本数。

8.1 基于 CPU 的 HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: go-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-api

  minReplicas: 2      # 最少副本数
  maxReplicas: 20     # 最多副本数

  metrics:
  # CPU 利用率超过 70% 时扩容
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

  # 内存使用超过 80% 时扩容
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60   # 扩容稳定窗口(60s 内连续超阈值才扩容)
      policies:
      - type: Pods
        value: 4                        # 每次最多增加 4 个 Pod
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容稳定窗口(5min 内持续低于阈值才缩容)
      policies:
      - type: Percent
        value: 25                       # 每次最多缩容 25%
        periodSeconds: 60

8.2 基于自定义指标的 HPA

需要配合 Prometheus Adapter 或 KEDA:

metrics:
# 基于 Prometheus 自定义指标
- type: Pods
  pods:
    metric:
      name: http_requests_per_second
    target:
      type: AverageValue
      averageValue: "1000"  # 每个 Pod 平均处理 1000 RPS

# 基于外部指标(如消息队列长度)
- type: External
  external:
    metric:
      name: kafka_consumer_lag
      selector:
        matchLabels:
          topic: order-events
    target:
      type: AverageValue
      averageValue: "100"   # 消费延迟超过 100 才扩容

8.3 HPA 操作

# 查看 HPA 状态
kubectl get hpa
kubectl describe hpa go-api-hpa

# 输出:
# NAME         REFERENCE            TARGETS    MINPODS   MAXPODS   REPLICAS
# go-api-hpa   Deployment/go-api    45%/70%    2         20        3

前提: HPA 需要 Metrics Server 提供 CPU/内存指标

# 安装 Metrics Server(minikube)
minikube addons enable metrics-server

# 验证
kubectl top pods
kubectl top nodes

9. 工作负载选型指南

你的应用是什么类型?
│
├─ Web 服务/API/微服务(无状态)
│  └─ Deployment ✓
│
├─ 需要稳定网络标识和存储(数据库/MQ/缓存集群)
│  └─ StatefulSet ✓
│
├─ 需要在每个节点上运行(监控/日志/网络)
│  └─ DaemonSet ✓
│
├─ 一次性批处理任务
│  └─ Job ✓
│
├─ 定时执行的任务
│  └─ CronJob ✓
│
└─ 需要自动扩缩容?
   └─ 在 Deployment/StatefulSet 基础上加 HPA ✓

10. 小结

各工作负载对比

特性 Deployment StatefulSet DaemonSet Job CronJob
Pod 名称 随机 有序固定 随机 随机 随机
存储 共享 独立 PVC 独立 临时 临时
有序操作
自动恢复 是(失败重试)
适用场景 Web/API DB/MQ 基础设施 批处理 定时任务

关键命令速查

# 查看所有工作负载
kubectl get deploy,sts,ds,job,cronjob -A

# 查看 Deployment 事件
kubectl describe deploy <name>

# 滚动升级状态
kubectl rollout status deploy/<name>

# 回滚
kubectl rollout undo deploy/<name>

# 扩缩容
kubectl scale deploy <name> --replicas=5

# 查看 HPA
kubectl get hpa
kubectl top pods