AI时代的Kubernetes教程36招-第11招-StatefulSet、DaemonSet、Job和CronJob分别适合什么工作负载?

K8s 工作负载选型:Deployment、StatefulSet、DaemonSet、Job、CronJob 的语义边界

五种控制器不是强度阶梯,而是五份互不替代的语义契约

[读者] {在 K8s 上同时运营常驻服务、节点采集、批任务与数据库的工程师}
[痛点] {所有负载套同一份 Deployment 模板,靠改参数和外部脚本维持}
[现在读] {节点扩容、定时刷新、索引任务同时出事,参数已经调不动}
[读完] {能按语义给每类负载选控制器,并知道 StatefulSet 不是数据库答案}
[旧方案] {一套参数化 Deployment + 集群外 crontab 敲 kubectl}
    |
    v
[新需求] {节点级采集、跑完即止的批任务、定时刷新、自管有状态数据库}
    |
    v
[冲突] {Deployment 只承诺副本数和可互换,不承诺节点覆盖、终态、身份与时间}
    |
    v
[后果] {新节点无采集、索引任务反复重启、定时任务重叠、数据库身份错乱}

我是老李,带一个小团队维护一套知识库平台的部署配置:主服务是一个 RAG API,旁边挂着向量索引构建、模型预热脚本,还有一套自管的 PostgreSQL。集群从 3 个节点扩到 30 个,deploy/base/workloads/ 这个目录也从两份 YAML 长到了十几个。

我们最初的认知非常朴素:K8s 就是声明期望副本数。于是 API 是 Deployment,节点采集 agent 是 Deployment,跑一次的索引构建是 Deployment 加一个 command 覆盖入口,每天的刷新交给集群外的 crontab 敲 kubectl create,PostgreSQL 也是 Deployment 挂一块 PVC。

这套写法在小规模下活了一年多,代价是每周几次手工补操作,谁也没觉得那是架构问题,只觉得是运维习惯。

直到三件事在一周内同时发生:扩到 30 个节点后监控面板仍然只有 3 个采集点;索引 Pod 第二天凌晨以 CrashLoopBackOff 出现在告警里;PostgreSQL 在一次滚动更新后卡在卷挂载失败,业务读不到数据。

同一份 Deployment,为什么在四种场景里给出了四种完全不同的失败方式?

这些失败究竟是参数没调好,还是我们从一开始就选错了对象?

如果继续在 Deployment 上加字段、加 sidecar、加外部脚本,这套东西还能撑多久?

01、故事

平台最早只有三样东西:一个对外 API、一份索引构建脚本、一套 PostgreSQL。索引脚本当初是 kubectl exec 进 API Pod 里手跑的,跑完人肉确认,慢但可控。

变化从知识库内容量上来了开始。刷新频率从每周手工一次变成每小时一次,索引构建从几分钟变成几十分钟,再塞进 API 容器里会把在线请求一起拖慢,必须拆出来单独跑。同时合规要求把 PostgreSQL 从托管服务搬回自管集群,节点也从 3 台扩到了 30 台。

那天定下来的任务是:在同一套 base 目录里,让常驻服务、节点级采集、跑完即止的批任务、定时刷新、有状态数据库这五类负载各自找到能表达自己语义的控制器,并且集群扩容、任务重跑、数据库升级时都不需要人工补操作。

我们的做法是统一 Deployment。一份参数化模板,用 replicas 和 command 两个变量覆盖所有场景:API 用 replicas: 3;采集 agent 用 replicas 手动跟着节点数改;索引任务用 replicas: 1,脚本跑完后 sleep infinity 挂着不让它退出;刷新靠一台跳板机上的 crontab,每小时 kubectl create job;PostgreSQL 用 Deployment 加一块 20Gi 的 PVC。

第一次真正出事,不是在大流量下,而是在一次普通的节点扩容之后。凌晨自动扩容加了 12 个节点,早上监控大盘的采集点数量纹丝不动;同一天,索引 Pod 的 RESTARTS 计数涨到 40 多;晚上做 PostgreSQL 镜像升级,滚动更新把旧 Pod 干掉后,新 Pod 卡在卷挂载上起不来。

三件事的报错信息毫无关联,但那一天我意识到它们可能是同一个问题的三种表现。

一副模板走天下
三处裂缝暗中生
扩容之日现原形
方知控制器非开关

02、问题

旧方案的失效点不是性能,而是表达力:Deployment 恰好在四个维度上都缺少表达能力。它表达不了每个节点一个,表达不了跑完即止,表达不了按时间触发,也表达不了这个 Pod 就是那个角色。

业务影响先落在可观测性上:新扩容的 12 个节点没有采集 Pod,容量规划拿不到这几台机器的真实负载,只能在扩缩容时凭经验留余量。

接着落在数据一致性上:索引构建 Pod 被反复重启,同一份源数据被重复构建,下游只能靠人工比对版本号决定用哪一份索引;刷新任务跑在集群外,失败了没有重试,前一轮还没结束下一轮又启动了,两个实例同时写同一份索引。

技术表现是可复现的几类信号:kubectl get pods -o wide 里采集 Pod 始终固定在那三个老节点上;索引 Pod 的 Last State 是容器主进程正常退出后被重启;kubectl describe pod 的事件里出现 ReadWriteOnce 卷的挂载冲突;集群外的 crontab 根本不存在并发策略这个概念。

我把验收标准写成四条可以直接验证的条款:第一,新节点加入后 5 分钟内必须出现采集 Pod,节点删除后对应 Pod 自动回收;第二,索引任务必须有 Succeeded 终态,失败在限定次数内重试,成功后按 TTL 自动清理;第三,刷新任务同一时刻只允许一个实例运行,错过执行窗口要有明确的放弃条件;第四,数据库由具备备份与故障转移控制回路的组件管理,StatefulSet 只用来理解身份绑定,不作为生产数据库的交付形态。

副本等数量
节点看覆盖
完成要终态
时间归调度

03、原理

Kubernetes 官方工作负载文档(https://kubernetes.io/docs/concepts/workloads/controllers/ )把 Deployment、ReplicaSet、StatefulSet、DaemonSet、Job、CronJob 的适用场景分别列出,关键不在于谁更高级,而在于每个控制器收敛的目标对象不同。我把这五个目标对象归纳成五个维度。

数量维度由 Deployment 和它下面的 ReplicaSet 负责,收敛目标是集合大小。

节点维度由 DaemonSet 负责,收敛目标是节点覆盖:每个符合条件的节点上有且只有一个 Pod,节点加入自动补齐,节点删除自动回收,调度由控制器直接参与,而不是单纯交给调度器排队。

终态维度由 Job 负责,收敛目标是成功完成的 Pod 计数,靠 completions 和 parallelism 描述,失败重试由 backoffLimit 限制,保留时间由 ttlSecondsAfterFinished 控制。

时间维度由 CronJob 负责,收敛目标是按 cron 表达式生成 Job 对象,真正的执行确定性来自 concurrencyPolicy 和 startingDeadlineSeconds。

身份维度由 StatefulSet 负责,收敛目标是稳定网络标识、稳定存储绑定和有序操作,配合 Headless Service 与 volumeClaimTemplates 使用。

第一个反直觉判断:Deployment 的 replicas 是集合大小,不是角色分配。控制器从不承诺哪个 Pod 是主、哪个是备、哪个负责哪个分片。任何依赖这个 Pod 就是那个角色的设计,在 Deployment 上从第一天就是错的,只是在小规模时被掩盖。

第二个反直觉判断:Job 的完成不等于容器退出。它是控制器按成功 Pod 数计数的结果,如果 Pod 模板的 restartPolicy 是 Always,这个 Pod 永远不会进入 Succeeded,Job 也就永远等不到完成。

第三个反直觉判断:StatefulSet 不提供数据库能力。它只让名字和挂载点稳定下来,主从仲裁、故障转移、备份恢复、版本升级编排都不在它的职责范围内。

语义先于参数
契约先于规模
身份不可互换
终态不许伪装

04、架构

[输入] deploy/base/workloads/ 下的声明式清单
    |
    v
[模块] 五类控制器 + kube-scheduler + kubelet
    |
    v
[数据/状态] ReplicaSet 期望值 / 节点集合 / Job 完成计数 / CronJob 调度记录 / PVC 绑定
    |
    v
[处理] 各控制器循环比对期望与实际并持续收敛
    |
    v
[输出] Pod 的五种终局:常驻副本 / 每节点一个 / 已完成 / 定时生成 / 稳定身份

这套结构里,五类控制器共享同一套调度、网络和存储底座,差别只在于各自比对的目标对象。

边界在这里:控制器只负责把自己的目标对象收敛到期望状态,它不负责判断业务是否可重入、不负责数据库的语义正确性、不负责任务之间的依赖顺序。这些必须由清单和上层流程表达。

收益是扩容与重试从人工操作变成控制循环:节点增减由 DaemonSet 自动跟进,任务失败由 Job 按次数重试,定时触发由 CronJob 记录在 API 对象里而不是某台跳板机上。

代价是语义必须选对,选错之后控制器会非常勤快地朝错误方向收敛:把批任务放进 Deployment,它会认真地把每次正常退出当成故障重启;把数据库塞进 StatefulSet 而不给控制回路,它会稳定地给你三个互不相关的单机实例。

适用条件是任务可重入。Job 和 CronJob 都会重试,如果一次索引构建不能安全地重跑,先解决幂等,再谈控制器选型。

一图定边界
五态各归位
代价先讲清
收益才可信

05、实战一次

环境是本地 kind 或 minikube 集群,节点数可调,方便验证节点覆盖;kubectl 版本以 kubectl version --short 的实际输出为准;命名空间 kb-platform。

依赖只用内置 API 组:apps/v1 和 batch/v1,不安装任何 CRD。为了让示例可以独立跑通,容器镜像统一用 busybox 做最小验证,真实项目替换成业务镜像即可。

目录结构如下:

deploy/base/workloads/
├── api-deployment.yaml
├── node-exporter-daemonset.yaml
├── index-warmup-job.yaml
├── kb-refresh-cronjob.yaml
└── pg-statefulset.yaml

常驻服务用 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kb-api
  namespace: kb-platform
spec:
  replicas: 2
  selector:
    matchLabels:
      app: kb-api
  template:
    metadata:
      labels:
        app: kb-api
    spec:
      containers:
        - name: api
          image: busybox:1.36
          command: ["/bin/sh", "-c", "while true; do sleep 3600; done"]
          ports:
            - containerPort: 8080

节点采集用 DaemonSet,并显式容忍控制面污点:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: kb-platform
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule
      containers:
        - name: exporter
          image: busybox:1.36
          command: ["/bin/sh", "-c", "while true; do sleep 3600; done"]
          resources:
            requests:
              cpu: 50m
              memory: 64Mi

索引预热用 Job,注意 restartPolicy: Never:

apiVersion: batch/v1
kind: Job
metadata:
  name: index-warmup
  namespace: kb-platform
spec:
  backoffLimit: 3
  ttlSecondsAfterFinished: 3600
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: warmup
          image: busybox:1.36
          command: ["/bin/sh", "-c", "echo build-index && exit 0"]

定时知识刷新用 CronJob,用 Forbid 阻断重叠:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: kb-refresh
  namespace: kb-platform
spec:
  schedule: "0 * * * *"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: refresh
              image: busybox:1.36
              command: ["/bin/sh", "-c", "echo refresh-done"]

PostgreSQL 只做理解身份绑定用的单副本示例,生产不这么交付:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pg
  namespace: kb-platform
spec:
  serviceName: pg-headless
  replicas: 1
  selector:
    matchLabels:
      app: pg
  template:
    metadata:
      labels:
        app: pg
    spec:
      containers:
        - name: postgres
          image: postgres:16
          env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: pg-secret
                  key: password
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

启动与验证:

kubectl create namespace kb-platform
kubectl -n kb-platform apply -f deploy/base/workloads/api-deployment.yaml
kubectl -n kb-platform apply -f deploy/base/workloads/node-exporter-daemonset.yaml
kubectl -n kb-platform apply -f deploy/base/workloads/index-warmup-job.yaml
kubectl -n kb-platform apply -f deploy/base/workloads/kb-refresh-cronjob.yaml
kubectl -n kb-platform get deploy,ds,job,cronjob
kubectl -n kb-platform get pods -o wide

未在当前环境实测,以下为预期结果,属于示例输出:

NAME                          READY   UP-TO-DATE   AVAILABLE
deployment.apps/kb-api        2/2     2            2

NAME                           DESIRED   CURRENT   READY
daemonset.apps/node-exporter   30        30        30

NAME                    STATUS     COMPLETIONS   DURATION
job.batch/index-warmup  Complete   1/1           3s

NAME                    SCHEDULE      SUSPEND   ACTIVE
cronjob.batch/kb-refresh 0 * * * *    False     0

跑通之后的遗留问题有三个:DaemonSet 只覆盖了集群内节点,边缘与异构节点还需要 nodeSelector 或亲和性约束;索引 Job 仍是单 Pod 全量构建,模型预热和向量索引混在一个任务里;PostgreSQL 这个 StatefulSet 只有一个副本,没有备份,没有主从,滚动更新期间业务会中断。第三个问题就是下一章的主线。

先跑通最小
再暴露缺口
不写省略号
不留假数据

06、排查

第一条链,节点扩容后监控数据不涨。

现象:节点数从 3 变成 30,采集点仍然是 3。

怀疑:exporter 的 Deployment 副本数没跟着改。

检查:同时看两边,kubectl -n kb-platform get pods -o wide -l app=node-exporter 和 kubectl get nodes。

证据:三个 Pod 全部落在最早的三台节点上,其余节点的 NODE 列里没有任何采集 Pod,而 Deployment 的 READY 显示 3/3,控制器认为自己完全达标。

根因:Deployment 的 replicas 与节点数之间没有任何因果关系。调度器只保证存在三个副本,不保证分布在三个不同节点上,更不会因为节点新增而补一个。

错误尝试: 把 replicas 从 3 直接改成 30。为什么错:这是把节点数当成了副本数,集群再扩一次还得手工改一遍;更糟的是 30 个副本不保证落在 30 个不同节点上,完全可能出现某节点两个 Pod、某节点零个;而且 DaemonSet 那套调度语义,包括对节点污点的容忍处理和节点删除时的回收,都不是调大 replicas 能复制的。

修复:改写成 DaemonSet,需要覆盖控制面时显式加 tolerations。

第二条链,索引 Pod 反复重启。

现象:索引 Pod 一晚上 RESTARTS 涨到 40 多,日志里能看到同一份源数据被构建了多次。

怀疑:容器被 OOMKilled,或者健康探针配置过紧。

检查:kubectl -n kb-platform describe pod 看 Last State 与 Events,再确认 Deployment 模板里的 restartPolicy,同时看探针的 exec 内容。

证据:容器主进程正常退出,退出码为 0,随后探针失败,kubelet 重启容器。真正的矛盾在于容器语义被写成了永远运行,而脚本的语义是跑完就结束。

根因:Deployment 的 Pod 模板只允许 restartPolicy: Always,它没有任何完成状态。把一次性任务塞进 Deployment,控制器会把每一次正常退出解释成故障,然后按自己的理解重启它,直到把任务做成幂等为止。

错误尝试: 在脚本末尾加 while true; do sleep 3600; done,让容器永远不退出。为什么错:这是把完成伪装成永不退出,控制器永远不知道任务到底成功还是失败,completions、backoffLimit、ttlSecondsAfterFinished 全部用不上,最终只能靠人看日志判断。

修复:改写为 Job,restartPolicy: Never,错误交给 backoffLimit 和上层重试策略。

第三条链,PostgreSQL 滚动更新后卷挂载失败。

现象:更新镜像后旧 Pod 被删除,新 Pod 卡在 ContainerCreating,事件里出现卷挂载冲突。

怀疑:StorageClass 的卷不支持跨节点挂载,或者更新期间没有做串行控制。

检查:kubectl -n kb-platform describe pod pg-0 看 Events,kubectl get pvc 看绑定状态,再看 StorageClass 的 volumeBindingMode。

证据:ReadWriteOnce 卷在节点 A 上被旧 Pod 持有,新 Pod 被调度到节点 B,卷无法同时挂到两处,等待期间就卡在创建阶段。

根因:StatefulSet 保证了 Pod 名字和 PVC 的绑定关系,但它不判断这块卷此刻能不能被挂上,也不判断数据库此刻能不能提供服务,卷挂载是存储与调度的职责。

错误尝试: 把 StatefulSet 的 replicas 从 1 扩到 3,想用三个实例做主从。为什么错:StatefulSet 只提供稳定标识 pg-0、pg-1、pg-2 和各自的 PVC,它不做主从选举、不做复制、不做故障转移、不做备份。三个实例会各自初始化自己的数据目录,你得到的是三个互不相关的单机数据库,而不是一个数据库集群。

修复:短期用 volumeBindingMode: WaitForFirstConsumer 配合串行更新;长期把数据库交给具备备份、复制和切换控制回路的成熟 Operator 管理,StatefulSet 只留作理解身份绑定的教具。

现象先落地
证据压猜测
错误尝试留
根因才敢改

07、优化

优化严格对着第 06 章的三条根因做,一条根因对应一处修改。

第一处,根因是节点覆盖表达不了。修改:把采集负载改成 DaemonSet,并补齐 tolerations 与资源请求;如果集群里存在不需要采集的特殊节点,用 nodeSelector 或 nodeAffinity 排除,而不是靠改副本数。原因:DaemonSet 的收敛对象就是节点集合,节点增减由控制循环负责。新行为:新节点加入后自动出现一个采集 Pod,节点下线后自动回收。验证方式:扩一个节点,观察 kubectl -n kb-platform get pods -o wide -l app=node-exporter 的 NODE 列是否覆盖到新节点;再缩掉一个节点,观察对应 Pod 是否被回收。

第二处,根因是一次性任务缺少终态语义。修改:把索引和预热统一改写为 Job,保持 restartPolicy: Never,设置 backoffLimit,并加 ttlSecondsAfterFinished 让成功的 Job 自动清理。原因:Job 的收敛对象是按成功 Pod 数计算的完成计数,它需要 Pod 有真实的终态。新行为:任务成功进入 Complete,失败在限定次数内重试,超过次数按失败告终并由上层告警接手。验证方式:kubectl -n kb-platform get job 观察 COMPLETIONS 列,再故意让命令返回非零退出码,观察重试次数是否触及上限后停止。

第三处,根因是定时刷新落在集群外,没有重叠控制。修改:用 CronJob 承接刷新,concurrencyPolicy: Forbid 阻断重叠,startingDeadlineSeconds 限定错过窗口后的行为,successfulJobsHistoryLimit 与 failedJobsHistoryLimit 控制历史 Job 堆积。原因:时间维度的收敛属于控制器职责,写在 API 对象里才能被审计和被观测。新行为:同一时刻只有一个刷新实例,前一轮没结束时后一轮直接跳过并记录跳过。验证方式:kubectl -n kb-platform get cronjob kb-refresh -o yaml 确认策略字段,再用 kubectl get jobs 观察是否出现时间戳重叠的 Job。

第四处,根因是把有状态等于 StatefulSet。修改:生产数据库不再用自写的 StatefulSet 交付,改由成熟数据库 Operator 承担,选型与版本以对应项目当前官方文档为准。原因:StatefulSet 提供身份与挂载点稳定性,而数据库需要的是复制、切换、备份、恢复和升级编排,这是控制回路的差异,不是参数差异。新行为:数据库的日常操作由 Operator 的 CR 描述,StatefulSet 只出现在学习和验证场景。验证方式:核对 Operator 是否具备备份与恢复的具体能力条目,并按官方文档做一次恢复演练,需按当前官方文档核验具体命令与版本支持矩阵。

一处根因
一处修改
一次验证
一步收敛

08、演进

[同一输入] deploy/base/workloads/ 下的声明式清单
    |
    +--[V1] 统一 Deployment + 集群外 crontab + 自写 StatefulSet
    |       代价:节点覆盖靠手工同步、批任务无终态、定时无重叠控制、数据库无控制回路
    |
    +--[V2] 按语义拆分:Deployment / DaemonSet / Job / CronJob + 数据库 Operator
    |       代价:需要理解五套语义、需要维护 Operator、需要为 Job 与 CronJob 定历史与 TTL 策略
    |
[Trade-off]
得到:控制器语义与业务语义对齐,扩容、重试、定时都由控制循环负责
失去:一份模板走天下的表面简单,引入 Operator 这类有状态的运维组件
适用边界:无状态服务、节点级采集、可重入批任务、定时任务;数据库交给成熟 Operator

正确性上,V1 依赖人工同步节点数与副本数,任何一次扩容或缩容都要人介入,V2 把这件事交给节点维度的收敛逻辑。

稳定性上,V1 的索引任务被反复重启,刷新任务可能重叠执行,V2 分别用终态语义和并发策略约束住这两种情况。

复杂度上,V2 更高:从一份模板变成五类清单,还要维护 Operator;这部分复杂度换来的是每种负载都有明确的验收方式。

成本上,V2 增加了 Job 与 CronJob 的历史对象管理、Operator 的资源开销和运维学习成本,同时减少了人工补操作和故障恢复时间。

适用范围上,这套划分适用于把同一集群当作多种负载共用底座的项目;如果集群里只有无状态服务,V1 那份 Deployment 模板本身没有问题,加东西才是问题。

遗留问题有两个:Job 与 CronJob 的失败目前只有状态可看,缺少耗时与失败率的统一观测;数据库 Operator 的备份恢复演练还没有纳入固定周期。

同输入分流
代价各不同
取舍写清楚
边界说明白

09、洞见

9.1 控制器是语义契约,不是强度档位

反直觉判断:StatefulSet 并不比 Deployment 更强,它只是把身份写进了契约,同时放弃了自由调度和快速替换。选型不是往上爬楼梯,而是给负载挑一份最贴近它语义的契约。用 Deployment 承载可互换的副本、用 StatefulSet 承载需要稳定名字和稳定挂载点的实例,两者是平行选择。

9.2 有状态不等于 StatefulSet

反直觉判断:StatefulSet 不提供数据库能力,它只提供命名和挂载点的稳定性。把 PostgreSQL 跑成 StatefulSet,拿到的是一个名字稳定的单机数据库,不是高可用集群;备份、复制、切换、升级编排都是另一套控制回路的事,那套回路通常叫 Operator。建议是:理解 StatefulSet 用它,交付数据库不要用它。

9.3 完成与运行是两种不同的语义,必须先分清再谈参数

Job 与 Deployment 的差别不是 restartPolicy 的一个取值,而是收敛目标不同:一个数成功的 Pod,一个数存活的 Pod。判断标准很朴素:这个负载有没有结束这个概念?有就归 Job,没有才归 Deployment。选错这一层,后面所有关于资源、探针、重试的参数调整都是在错误的地基上调参。

9.4 时间维度不要外溢到集群外

集群外的 crontab 能触发任务,但它不参与 K8s 的期望状态收敛:没有并发策略、没有错过窗口的定义、没有执行历史对象、没有审计。CronJob 的价值不在于它能定时,而在于它把定时这件事变成集群内可查询、可观测、可回溯的 API 对象。

9.5 用最弱的语义承载需求

在能表达清楚的前提下,永远选语义更弱的那一个:能用 Deployment 就不要 StatefulSet,能一次跑完就不要常驻,能靠 Job 就不要引入外部调度系统。语义越弱,控制器替你做的假设越少,出问题时可解释性越强。

弱语义优先
强语义慎用
身份与能力
从来不同源

10、系统落地

原来有什么:一份参数化的 Deployment 模板,覆盖全部负载;一台跳板机上的 crontab;一个自写的 PostgreSQL StatefulSet 加一块 PVC。交付形态是复制模板加手工补操作。

本篇新增了什么:deploy/base/workloads/ 目录按语义分层,API 用 Deployment,节点采集用 DaemonSet,索引与预热用 Job,知识刷新用 CronJob,数据库改由成熟 Operator 承担;同时给 Job 和 CronJob 补齐了重试上限、TTL、并发策略和历史保留策略。

现在能做什么:集群扩缩容时采集自动跟进;批任务有明确的成功与失败终态,成功后自动清理;定时刷新不会重叠;数据库的备份与切换有对应的控制回路,不再依赖手工操作。

还缺什么:Job 与 CronJob 的统一观测,包括耗时、失败率、跳过次数;DaemonSet 采集 Pod 的资源配额与优先级;关键服务的 PodDisruptionBudget;数据库 Operator 的恢复演练排期。

下一步如何演进:给索引任务加分片,用 parallelism 和 completions 配合工作队列把单 Pod 全量构建拆开;给 CronJob 加 suspend 开关,用于维护窗口;把数据库的备份恢复演练写进季度计划,并保留一份从备份恢复的验证记录。

有目录有据
有策略有度
有缺口有单
有余量有备

11、小结

Q1 → Deployment 只保证副本数,节点覆盖是 DaemonSet 的节点维度语义,调大 replicas 无法替代
Q2 → 一次性任务需要终态,Job 的完成计数与 Deployment 的永久运行是两份不同契约
Q3 → StatefulSet 只给身份与挂载点稳定,数据库的高可用与备份必须由 Operator 这类控制回路承担
状态 → deploy/base/workloads/ 按语义分层,四类负载各自可验证,数据库交由成熟 Operator

开篇三个问题的答案其实指向同一件事:控制器的价值是收敛目标对象的差异,不是配置项的多少。

本篇的最终可交付能力是:给定一个负载,能先判断它属于数量、节点、终态、时间、身份中的哪一类语义,再决定用哪个控制器,并知道每种选择背后的代价与验证方式。

三问答在前
一图收在后
能力可交付
遗留不藏私

12、作业

12.1 理解题:为什么把 Deployment 的 replicas 调大,不能替代 DaemonSet 的节点覆盖?

参考答案:两者的收敛目标不同。Deployment 的目标是集合大小,调度器只保证存在指定数量的副本,不保证分布在多少节点上,也不会因为节点增减而自动调整;DaemonSet 的目标是节点覆盖,每个符合条件的节点上有一个 Pod,节点加入自动补齐,节点删除自动回收,并且能通过 tolerations 与 nodeSelector 表达对特殊节点的处理方式。把节点数当成副本数,等于把集群拓扑硬编码进清单。

12.2 实战题:为一次索引预热任务写最小 Job 清单,要求失败最多重试 2 次、成功后 1 小时内自动清理。

参考答案:

apiVersion: batch/v1
kind: Job
metadata:
  name: index-warmup
  namespace: kb-platform
spec:
  backoffLimit: 2
  ttlSecondsAfterFinished: 3600
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: warmup
          image: busybox:1.36
          command: ["/bin/sh", "-c", "echo warmup && exit 0"]

要点是 restartPolicy: Never 保证 Pod 有终态,backoffLimit 限制重试次数,ttlSecondsAfterFinished 控制成功后对象保留时间。如果任务不可重入,先改成幂等再交付。

12.3 排障题:CronJob 每小时执行一次,某次任务耗时 90 分钟,观察到同一时刻有两个实例在跑,怎么查、怎么改?

参考答案:先确认默认并发策略。不设置时 concurrencyPolicy 默认为 Allow,允许上一轮没结束时启动下一轮。查证方式是 kubectl -n kb-platform get cronjob kb-refresh -o yaml 看策略字段,再 kubectl get jobs 按创建时间看是否存在时间戳重叠的 Job 对象。修改方向是把策略改为 Forbid,严格禁止重叠;如果业务更希望用新任务替换旧任务,才选 Replace,并配合 startingDeadlineSeconds 明确错过窗口后的行为。改完还要确认任务本身是否可重入,避免跳过的一轮造成状态缺口。

12.4 架构判断题:团队打算把 PostgreSQL 从托管服务迁到自管集群,理由是已经有 StatefulSet 了,这个判断成立吗?

参考答案:不成立。StatefulSet 提供的是稳定网络标识、稳定 PVC 绑定和有序操作,它不提供主从选举、复制、故障转移、备份恢复、时间点恢复、连接路由和版本升级编排。迁移到自管等于团队要承接数据库运维职责,正确做法是优先选择成熟的数据库 Operator,把备份、切换、升级交给它的控制回路,并且按各自官方文档核验能力与版本支持,而不是把 StatefulSet 当作数据库方案。StatefulSet 更适合放在学习与验证场景里,用来理解身份绑定的机制。

题题有答案
步步有验证
不靠背结论
靠证据说话

13、思考

回到本篇的核心冲突:同一份 Deployment 模板在四种场景里给出了四种失败方式,问题不在参数,而在语义错配。工程上真正稳定的做法,是先问这个负载的收敛目标是什么,再去挑控制器。

这套判断可以复用到别的技术选型上:当一个组件同时被用来解决多种形态的问题时,先检查它在每种形态下承诺了什么、又放弃了什么。承诺越多、越模糊,故障时的解释成本就越高。

最后留一个可执行的检查清单:这个负载有没有结束的概念?它是否要求每个节点各有一个?它是否需要稳定的名字或挂载点?它是否按时间触发?这四个问题问完,控制器基本就定了;剩下唯一需要额外克制的是,别把有状态三个字直接等同于 StatefulSet。

一念辨语义
一证定根因
一改知取舍
一役见真章

posted @ 2026-10-12 10:22  李福春  阅读(3)  评论(0)    收藏  举报