资讯动态

K8s有状态应用与运维实战:StatefulSet、DaemonSet、Job及资源配额详解

发布时间:2026/10/20 23:54:36 来源:尧图企业网站定制
前两篇聊完集群部署和 Pod、Deployment、Service 这些基础工作负载之后这篇 k8s操作三要进入日常交付里最容易被反复折腾的内容有状态应用怎么部署、节点级代理怎么管理、一次性任务和定时任务怎么调参以及多团队共享集群时怎么用配额守住底线。这四个主题放在一起不是编辑流水账而是因为它们在实际运维现场往往同时出现业务库要用 StatefulSet日志采集要用 DaemonSet报表和迁移要用 CronJob/Job而整个命名空间前面还要卡一道 Quota避免有人手滑把资源打爆。适合的读者是已经能独立部署集群、对 Deployment/Service 有一定操作基础的人如果对那部分还不熟建议先回去翻上一篇文章再回来。1. 为什么“有状态”会成为部署分水岭StatefulSet关键机制1.1 为什么 Deployment 管不住有状态应用在 Kubernetes 里Deployment 的设计前提是“所有副本都是同义词”模板一填扩容缩容随便拆哪个 Pod 挂了直接换一个新的顶上IP、名字都不重要。这套模型对无状态 Web 服务非常友好可一旦换成数据库、缓存、消息队列问题就来了。第一Pod 重建后 IP 变了客户端没法把旧连接接到新 Pod 上第二多个副本如果共用同一个卷数据库会被锁头压死如果各挂各的卷副本重建后怎么知道自己该挂哪一块第三中间件大多有启动顺序依赖比如主节点要先就绪从节点才能去拉主库同步位点Deployment 是并发滚动升级的不会管谁先谁后。StatefulSet 就是为解决“身份 独立存储 顺序”这三件事而生的控制器。它给每个 Pod 分配一个从 0 开始递增的序号名字固定为“控制器名-序号”例如 sim-db-0、sim-db-1。不管这个 Pod 被删掉还是被重新调度只要 StatefulSet 还在新 Pod 依然叫这个名字。光有名字还不够存储也不能乱StatefulSet 一般配合 volumeClaimTemplates按每个副本独立创建 PVCPVC 的名字同样带序号所以 sim-db-0 重建后还能挂回原来那块卷数据不丢。维度DeploymentStatefulSetPod 名称随机前缀固定序号名Pod 身份不关心稳定且唯一存储一般不挂或共享卷每副本独立 PVC启动顺序并发按序号有序典型场景Web/API/批处理数据库/中间件需要说清一件事StatefulSet 并不神奇它不会让网络性能更好也不会自动复制数据更不会帮你做主从切换。数据备份、读写分离、高可用这些能力都来自应用本身StatefulSet 只是把“稳定的标识”和“稳定的存储”这两个基础条件给到了。很多人在中间件容器化时踩坑就是因为把 StatefulSet 当成万能的以为部署上去应用就自动集群化结果一堆副本在报错。1.2 没有 headless Service名字没法变成连接地址光有固定名字不代表能被业务访问。对无状态应用一个普通 ClusterIP Service 就够了但对 StatefulSet几乎总要配一个 headless Service也就是clusterIP: None。为什么因为 StatefulSet 里的每个 Pod 都需要一个独立的 DNS 记录形如sim-db-0.sim-db-svc.default.svc.cluster.local sim-db-1.sim-db-svc.default.svc.cluster.local这样应用就能直接通过完整域名连到某个确定的实例。普通 Service 把流量负载均衡到 Pod 上而 headless Service 不提供虚拟 IP它只是把每个 Pod 的地址直接解析出来由调用方自己决定连哪一个。对应配置一般是apiVersion: v1 kind: Service metadata: name: sim-db-svc spec: clusterIP: None selector: app: sim-db ports: - port: 3306注意headless Service 的 selector 必须和 StatefulSet 的 Pod 标签一致如果标签对不上Pod 的 DNS 记录就不会生成虽然 Pod 能跑但别人根本找不到它。这个坑我见过好几次症状是应用启动时 DNS 解析失败查了半天才发现在改标签时把 Service 的 selector 漏了。还有一个容易忽略的点如果 StatefulSet 的serviceName写错或引用的 Service 不是 headless集群内 DNS 不会按“Pod名.Service名”的格式生成记录。很多中间件客户端配置的“主库地址”就是靠这种固定域名一旦域名解析不到应用就会反复重试连接表现起来像是网络故障其实是基础配置没对上。1.3 volumeClaimTemplates 和滚动升级的注意点StatefulSet 的持久化存储不在 Pod 模板的 volumes 里硬编码而是用 volumeClaimTemplates 来声明“存储模板”。每个副本都会根据这个模板生成一份独立的 PVC名字规则是“模板名-控制器名-序号”。apiVersion: apps/v1 kind: StatefulSet metadata: name: sim-db spec: serviceName: sim-db-svc replicas: 2 selector: matchLabels: app: sim-db template: metadata: labels: app: sim-db spec: containers: - name: sim-db image: sim-db:3.2 ports: - containerPort: 3306 volumeMounts: - name: data mountPath: /var/lib/sim-db volumeClaimTemplates: - metadata: name: data spec: accessModes: [ ReadWriteOnce ] resources: requests: storage: 10Gi部署后会生成>apiVersion: apps/v1 kind: DaemonSet metadata: name: node-log-agent spec: selector: matchLabels: app: node-log-agent updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 template: metadata: labels: app: node-log-agent spec: hostNetwork: true tolerations: - operator: Exists containers: - name: agent image: node-log-agent:1.6 resources: requests: cpu: 100m memory: 128Mi说明hostNetwork: true会让容器直接使用节点网络栈很多日志采集器依赖它来区分节点和容器。tolerations里operator: Exists表示容忍全部污点一般只给必须覆盖每个节点的采集类组件用业务型的 DaemonSet 别这么写。resources.requests一定要设否则资源紧张时调度器容易把这种“看不见”的代理排到最后。DaemonSet 的更新策略有两个选择。默认 RollingUpdate 可以控制 maxUnavailable比如一次只让一台节点上的老版本退出尽量减少采集链路中断。OnDelete 则意味着只有你手动删除某个 Pod控制器才会按新模板重建适合网络插件这类需要人工确认的敏感组件。我倾向于给采集器配置 RollingUpdate配合maxUnavailable: 1这样线上变更的影响面可控网络类组件则先用 OnDelete在测试节点上验证没问题再手动批量推进。有个容易忽视的点DaemonSet 的 Pod 和普通 Pod 一样占资源也会被驱逐、被资源配额计算。别因为它是“系统组件”就不设 requests/limits更别把它的日志目录直接放内存盘不然节点内存一紧张最先死的往往是它。真实环境里我见过采集器把 hostPath 日志目录写到默认盘结果/var/log被撑满节点组件都跟着受影响所以最好给这类 Pod 单独留一块磁盘或做日志轮转。3. 一次性任务和定时任务Job 与 CronJob 的细节3.1 Job 的重试、超时与并行Job 的定位是“运行一次直到完成”的负载。典型场景数据库迁移、报表生成、批量导出、影像数据清洗。这些任务跑完了就退出不能被当成常驻进程。但奇怪的是很多人写 Job 时习惯照抄 Deployment 的模板结果留了不少隐患。几个细节不能漏restartPolicy 必须显式设为 Never 或 OnFailure。如果漏写Kubernetes 会按默认值配置Job 逻辑没法判断任务到底算成功还是失败。backoffLimit 控制 Job 控制器创建新 Pod 的重试次数。例如设为 3表示任务失败后控制器还会创建新的 Pod 再试几次直到成功或达到上限。需要注意容器重启次数和 Job 重建 Pod 的次数是叠加的所以排查时不能只看restartCount一个字段。activeDeadlineSeconds 是任务总执行时间上限。超过这个时间Job 会被判定为失败并终止。很多卡死的任务靠这个字段能从“无限挂起”变成“可感知失败”。parallelism 是同时跑的 Pod 数completions 是至少成功完成多少个才整体完成。默认都是 1大批量任务可以适当提高。示例apiVersion: batch/v1 kind: Job metadata: name: db-migrate spec: backoffLimit: 3 activeDeadlineSeconds: 3600 template: spec: restartPolicy: Never containers: - name: migrate image: migrate-tool:2.1 env: - name: DB_DSN value: user:passtcp(sim-db-svc:3306)/app为什么推荐restartPolicy: Never因为这样每次失败都会产生一个新 Pod你可以在 describe Pod 时看到每一次失败的完整状态和日志而不是同一个容器不断原地重启状态混在一起。如果是 OnFailure容器会在同一个 Pod 里重启排查记录没有隔离开。特别是数据库迁移这种对“是否重复执行”敏感的任务明确定位“第几次失败”很重要。3.2 CronJob 的几个易错点CronJob 等于“到点创建 Job”的调度器。它并不保证秒级精准只要集群状态正常一般也就几秒到十几秒的延迟。如果要严格的“整点必须跑到”最好在应用层也做幂等和补偿别指望平台级精确。几个坑要单独讲第一时区。CronJob 的 schedule 字段虽然看着像标准 cron但不少默认环境使用 UTC。如果你写0 2 * * *又以为是凌晨两点实际可能会是早上八点。新版 K8s 支持通过spec.timeZone直接指定时区老版本环境建议在 schedule 里写CRON_TZAsia/Shanghai 0 2 * * *或者干脆统一约定集群时间。第二并发策略。concurrencyPolicy 有三个选项Allow 允许上一次没跑完就开新一轮Forbid 禁止并发错过就等下一轮Replace 干掉旧的直接换新的。日常运维我偏爱 Forbid尤其是数据库迁移、批量写入这类有脏数据风险的任务并发执行可能造成重复写入。第三历史记录。CronJob 默认会保留 3 个成功记录和 1 个失败记录时间一长历史 Job 对象会积累。如果不加限制controller 会反复创建 Job旧对象还占着命名空间配额集群里看起来一堆资源被占用。建议显式设置 successfulJobsHistoryLimit 和 failedJobsHistoryLimit。apiVersion: batch/v1 kind: CronJob metadata: name: daily-report spec: schedule: 0 2 * * * timeZone: Asia/Shanghai concurrencyPolicy: Forbid successfulJobsHistoryLimit: 5 failedJobsHistoryLimit: 2 jobTemplate: spec: backoffLimit: 2 template: spec: restartPolicy: Never containers: - name: report image: report:4.0 command: [python, gen_report.py]注意timeZone字段要求集群版本足够新如果你的版本不支持别硬上用 CRON_TZ 或在 schedule 里换算更稳。另外CronJob 真正执行的并不是模板本身而是每次生成一个 JobJob 又生成 Pod所以“查看日志”要先找到最近一次 Job 的 Pod 名再 kubectl logs 对应 Pod。直接kubectl logs deployment/xxx这套逻辑在 CronJob 上不适用。4. 资源配额与 LimitRange防止一个命名空间拖垮集群4.1 配额用途与统计口径集群资源不是无限的。多个业务团队共用一个集群时最大的风险不是某个 Pod 写满了 bug而是有人不小心把“副本数调到 80”或者“内存 limit 设成 64Gi”整台节点瞬间被占满其他应用跟着被驱逐。资源配额就是给命名空间划一个资源使用上限超过就不让创建。配额统计口径有 request 和 limit 之分。调度基于 request运行时限制基于 limit。比如请求 1Gi 内存、限制 4Gi调度器按 1Gi 给 Pod 找位置运行时最多允许用到 4Gi。配额如果同时配了 requests 和 limits那么 Pod 的对应字段都要设值否则创建会被拦截。注意这个细节配额报错时经常提示“must specify requests”不是真的要求你写死只是你的 YAML 里没写 resource 段落。再补充一点配额是按命名空间统计的一个 Deployment 扩容到 10 个副本配额里就是 10 份 resources 叠加而不是一个 Pod 的量。如果明明看着集群空闲却创建失败多半是这个命名空间配额已经算满了。一个常见但容易被忽视的情况Pod 里只写了 limits 没写 requests 时Kubernetes 默认会把 requests 当成和 limits 一样大。也就是说你设了limits.memory: 4Gi调度时它也会占用 4Gi 的 request。这会导致配额和节点资源被迅速吃光。尽量避免这种“隐式请求”所有工作负载都显式写清 request 和 limit。4.2 LimitRange 如何配合配额LimitRange 是命名空间内的资源约束模板作用有两个一是给每个容器补默认的 request/limit二是限制单个容器可以申请的最大最小范围。没有它之前如果 YAML 里不写 resourcesPod 就能以“无资源声明”的方式启动调度器把这部分负载当成零开销实际运行却可能占满节点。配额和 LimitRange 配合使用才能既控总量又控单量。apiVersion: v1 kind: LimitRange metadata: name: default-limit spec: limits: - max: cpu: 4 memory: 4Gi min: cpu: 50m memory: 64Mi default: cpu: 500m memory: 512Mi defaultRequest: cpu: 100m memory: 128Mi type: Container在这个配置下如果容器没写 request会被填成 defaultRequest如果没写 limit会被填成 default如果申请量超过 max 或低于 min直接拒绝。于是配额、LimitRange 和部署模板会共同参与“能否创建”的决策。实际工作中我见过很多类似的情况配额明明够但 LimitRange 把默认 request 设得很大单个 Pod 就把额度吃掉一大块扩容只能扩很少的副本。调整时要三个 YAML 一起看不能只看 Quota。4.3 超额创建的排查路径遇到“创建 Pod 失败并提示 exceeded quota”我的排查顺序是先看配额明细。命令kubectl get resourcequota -n ns -o yaml会显示每个资源的使用量和上限。一般一眼就能看到是 CPU 还是内存额度用完。再看命名空间里的现存 Pod。有可能很多旧版本 Pod 处于 Terminating 状态但还没有完全清理退出中的 Pod 也会占用额度。此时要么等要么确认安全后清理残留。这里要特别提醒强制删除有风险如果 PVC 还在被使用删 Pod 可以但不要顺手把 PVC 也删了。最后看 LimitRange 介入后的效果。把上面的默认 request/limit 改小一点或者临时调高配额都能让发布继续下去。但要分清是“临时救火”还是“长期不合理”最好在事后重新统计各业务的资源画像再调整配额值。另外配额和 LimitRange 本身是作用于 API 准入的和节点资源的实时压力没有关系。就算集群节点很多很闲只要配额设置严了Pod 一样创建不了反过来如果配额宽松Pod 也照样可能调度失败。两者要分开看别混淆。5. 日常巡检三板斧事件、日志、指标5.1 事件第一手的故障线索Pod 卡在 Pending起不来又找不到原因时第一件事应该是看事件。kubectl get events -n ns --sort-by.lastTimestamp或者直接kubectl describe pod pod看最后几行 Event。事件里常见的几个高频问题FailedScheduling调度器因为资源不足、节点亲和、污点等原因拒绝FailedMount卷挂载失败存储类和访问模式可能不对ImagePullBackOff拉镜像失败检查镜像名、标签、仓库凭证BackOff容器反复启动失败要结合日志和探测参数来判断。要注意事件默认保留窗口很短一般只有半小时到几小时。如果间隔太久才去查可能什么都看不到这就是为什么必须同时配日志采集和监控。日常巡检我习惯按命名空间扫一遍 Warning 级别事件重点看那些一直在刷而没有自动恢复的而不是偶发一条就紧张。5.2 日志看容器内部到底在干什么事件告诉我们外部发生了什么日志告诉我们容器内部发生了什么。基础命令就三个kubectl logs pod看单容器日志多容器加-c container想看容器重启前的日志用--previous。CrashLoopBackOff 问题时--previous尤其重要因为当前容器可能刚重启日志只有启动那几行真正导致崩溃的原因都留在了上个实例。日志文件可能很大优先用--tail限制行数。线上环境不建议直接裸跑不带参数容易把终端卡住。kubectl logs适合定位单点问题不适合做日志分析要查某个时间段的错误聚合需要把容器标准输出接入采集系统。我在实际定位问题时还会把事件时间、容器重启时间、日志报错时间三个时间点对齐基本能还原出完整故障链条。5.3 指标判断是不是资源不足事件和日志能看到“创建失败”和“运行报错”但要判断节点是否压力过大得看指标。kubectl top node、kubectl top pod -n ns是运维最常用的两个视图。Pod 的 CPU 和内存使用量一目了然节点维度可以看到总量和剩余量。如果某个 Pod 反复被驱逐或 OOM优先看它的 requests 和 limits 设置。比如内存 limit 只给了 512Mi实际运行值逐步涨到 600Mi那就会被 OOMKilled然后重启然后又涨到 600Mi循环往复。这时候不是调 limit 就能解决要分析应用是不是存在内存泄漏或者把堆参数、连接池上限调整到位。kubectl top是实时快照也有一定延迟连续观察一段时间更有意义。我的习惯是先事件判断“为什么不能创建”再日志判断“容器里在报什么错”再指标判断“是不是资源卡住了”。三者联动大部分日常问题都能定位到七七八八剩下的极少情况才需要进一步看网络抓包、存储性能这类深层次内容。CronJob 任务失败时用kubectl get cronjob、kubectl get jobs、kubectl get pods这三级对象的名称和时间戳串起来就能定位到具体某一次执行然后重复 事件→日志→指标 的顺序问题就不难暴露。最后说点个人经验。StatefulSet、DaemonSet、Job/CronJob 和 ResourceQuota 这四样东西单独看都能跑通真正难的是它们在一个集群里互相影响。比如配额设了CronJob 历史 Job 没清理满了之后新的定时任务直接失败DaemonSet 的采集器没设 requests节点资源紧张时反而被驱逐日志一断又影响排查。做成体系之后我每次给业务方开通新命名空间都会顺手把 ResourceQuota、LimitRange 和可观测组件一起配好后续省下来的排查时间远比配置时那几分钟多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑