4. Kubernetes工作负载:Deployment、StatefulSet、DaemonSet、Job、CronJob

工作负载,说白了就是你在Kubernetes上跑的应用。我刚开始接触K8s时,总觉得这五个概念长得差不多,都是跑Pod嘛。但实际用下来,每个都有自己独特的脾气。选错了,后面运维能让你哭出来。

今天咱们一个一个捋清楚。我会结合我踩过的坑,告诉你什么时候该用哪个。

4.1 Deployment:最常用的无状态部署

Deployment是我用得最多的一个。它管理的是无状态应用——说白了就是每个Pod都一样,谁死了换谁上,不用管身份。

它的核心能力有三个:

  • 声明式更新:你告诉它「我要3个副本」,它自己会搞定
  • 滚动更新:逐个替换Pod,保证服务不中断
  • 回滚:出问题了,一条命令回到上一个版本

来个最简单的例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
我的习惯:我一般会在Deployment里加上 strategy.rollingUpdate.maxSurgemaxUnavailable。默认值是25%,但生产环境我通常设成10%,避免流量波动太大。

Deployment适合什么场景?Web服务、API网关、微服务中的无状态模块。说白了,只要Pod不需要记住自己是谁,就用Deployment。

4.2 StatefulSet:有状态应用的救星

StatefulSet是我曾经最头疼的一个。为什么?因为它要解决一个根本问题:Pod是有身份的。

你想想看,数据库、消息队列、缓存集群——这些应用每个节点都有自己的角色。主节点挂了,新Pod必须知道自己是主;从节点挂了,新Pod得知道该从哪同步数据。

StatefulSet给了三个关键特性:

  • 稳定的网络标识:Pod名字固定,比如 mysql-0mysql-1
  • 稳定的持久化存储:每个Pod绑定自己的PVC,重启后数据还在
  • 有序的部署和伸缩:从0到N逐个启动,从N到0逐个删除

看个例子:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi
我曾经踩过的坑:有一次我把Elasticsearch部署成Deployment,结果Pod重启后IP变了,集群直接分裂。后来改成StatefulSet,用稳定的Pod名称做节点发现,问题才解决。记住:有状态应用,别用Deployment

4.3 DaemonSet:每个节点一个

DaemonSet的逻辑很简单:集群里每个节点上跑一个Pod。新节点加入,自动创建;节点删除,自动清理。

我一般用它来部署基础设施组件:

  • 日志采集(Fluentd、Filebeat)
  • 监控代理(Prometheus Node Exporter)
  • 网络插件(Calico、Flannel)
  • 安全代理

代码示例:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
spec:
  selector:
    matchLabels:
      name: fluentd
  template:
    metadata:
      labels:
        name: fluentd
    spec:
      containers:
      - name: fluentd
        image: fluent/fluentd:v1.14
        volumeMounts:
        - name: varlog
          mountPath: /var/log
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
小技巧:如果你只想在部分节点上运行DaemonSet,可以用 nodeSelectoraffinity。我有个项目只在GPU节点上跑AI推理的DaemonSet,就是这么干的。

4.4 Job:跑一次就结束

Job就是用来跑一次性任务的。Pod执行完就退出,Job会帮你确认任务是否成功完成。

我常用它来做:

  • 数据库迁移
  • 数据批处理
  • 定时报表生成
  • 初始化脚本

看个例子:

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: myapp/migrate:1.0
        command: ["python", "migrate.py"]
      restartPolicy: Never
  backoffLimit: 4

注意restartPolicy 必须设为 NeverOnFailure。设成 Always 的话,Pod会一直重启,Job永远结束不了。我见过有人因为这个把集群资源吃光的。

4.5 CronJob:定时任务

CronJob就是在Job外面包了一层定时器。语法和Linux的crontab一样,但多了Kubernetes的特性。

我一般用它来做:

  • 每天凌晨的数据备份
  • 每小时的健康检查
  • 每周的日志清理

代码示例:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: myapp/backup:1.0
            command: ["sh", "-c", "pg_dumpall > /backup/db.sql"]
          restartPolicy: OnFailure
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
我曾经踩过的坑:CronJob默认会保留最近3个成功的Job和1个失败的Job的历史记录。如果不设限制,时间长了会积累大量已完成的Pod,把etcd撑爆。我有个客户就是没设 successfulJobsHistoryLimit,半年后集群响应越来越慢,查了半天才发现是历史Job太多。

4.6 如何选择?一张表说清楚

工作负载类型 适用场景 Pod身份 存储 更新策略
Deployment 无状态Web服务、API 无身份 临时存储 滚动更新
StatefulSet 数据库、消息队列、缓存 有身份 持久化存储 有序更新
DaemonSet 日志、监控、网络插件 按节点分配 通常用宿主机路径 逐个更新
Job 一次性任务、批处理 临时 可选 不适用
CronJob 定时任务、定期备份 临时 可选 不适用

嗯,这五种工作负载基本覆盖了你在Kubernetes上跑应用的所有场景。我个人建议:刚开始用K8s时,先死磕Deployment和StatefulSet。这两个搞明白了,剩下的自然就通了。

下一章咱们聊聊服务发现和负载均衡——Service和Ingress。到时候我会讲讲我是怎么用Ingress做灰度发布的,那个坑也挺有意思。