跳到主要内容

Workload 工作负载

本篇是 Kubernetes 系列的第四篇,聊工作负载(Workload)。前面把架构、安装、API 都过了一遍,这篇开始真正「跑应用」:从最小单位 Pod 讲起,再到 Deployment、StatefulSet、DaemonSet、Job/CronJob 这些控制器,最后收在 HPA/VPA/PDB、生命周期、探针和滚动更新这些所有工作负载都绕不开的通用机制上。

Pod​

Pod 是 Kubernetes 里最小的调度和部署单位。K8s 不直接调度容器,而是调度 Pod——一个 Pod 里可以跑一个或多个容器。

为什么不直接用容器当最小单位​

因为一个「应用实例」往往不是一个容器能搞定的:主容器跑应用,旁边可能还要一个容器负责收集日志、一个容器做代理。这些容器生死与共、必须待在同一台机器上,还要共享网络和存储。如果调度单位是单个容器,K8s 就得自己处理「这几个容器必须落在同一节点」这类成组约束;把调度单位提升为 Pod、让容器成组出现,问题就简单多了——Pod 是原子单位,要么整组调度成功,要么整组不调度。

同一 Pod 内的容器共享什么​

  • 网络:同一 Pod 的所有容器共享同一个 network namespace——同一个 IP 地址、同一套端口空间,容器之间通过 localhost 直接通信(注意端口不能冲突)。
  • 存储:Pod 里定义的 volume 可以同时挂给多个容器,容器之间靠共享 volume 交换文件(比如应用把日志写到 emptyDir,日志收集容器从同一个 volume 读走)。
  • 容器之间不共享文件系统(镜像各自独立),也不共享进程表(除非显式开启 shareProcessNamespace)。

pause 容器:Pod 里看不见的「老大」​

每个 Pod 里都藏着一个 pause 容器(基础容器,镜像 registry.k8s.io/pause)。它是 Pod 里第一个启动的容器,职责很「卑微」但关键:启动后先占住 network namespace(以及 IPC 等共享资源),然后就一直睡眠;之后业务容器启动时加入 pause 容器的 namespace,从而实现「共享网络」。

可以理解为:pause 容器是 Pod 的「载体」,业务容器都是后来挤进它地盘的。所以在节点上用 crictl ps 查看时,会发现每个 Pod 都对应一个 pause 容器;pause 挂了,整个 Pod 的网络标识也就没了。

一个多容器 Pod 的例子:

apiVersion: v1
kind: Pod
metadata:
name: web-with-log-agent
spec:
containers:
- name: app
image: nginx:1.27
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: log-agent
image: busybox:1.36
args: [/bin/sh, -c, 'tail -f /logs/access.log']
volumeMounts:
- name: logs
mountPath: /logs
volumes:
- name: logs
emptyDir: {}
一个 Pod 一个容器是最常见的姿势

虽然 Pod 支持多容器,但绝大多数时候一个 Pod 只放一个主容器。只有确需「同生共死、共享网络或存储」的辅助进程才放进同一个 Pod;能拆成两个独立部署的服务就不要硬塞。

InitContainer​

Pod 还支持一类特殊容器——Init 容器(spec.initContainers),在主容器启动之前运行:

  • 顺序执行:多个 Init 容器按声明顺序一个接一个跑,前一个成功退出(退出码 0)后一个才会启动。
  • 阻塞主容器启动:全部 Init 容器成功完成后主容器才会启动;任何一个失败,kubelet 会按 Pod 的 restartPolicy 反复重跑它,直到成功为止(这期间 Pod 会一直卡在 Init:0/1 这类状态)。
  • Init 容器和主容器一样可以使用 volume、Secret、ConfigMap,因此常用来「打前站」。

两个真实场景:

场景一:等依赖服务就绪。应用要连 MySQL,但 K8s 不保证 MySQL 先就绪,可以用 Init 容器阻塞主进程直到依赖可用:

apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: wait-for-mysql
image: busybox:1.36
command:
- sh
- -c
- |
until nslookup mysql.default.svc.cluster.local; do
echo "waiting for mysql..."
sleep 2
done
containers:
- name: myapp
image: myapp:v1

场景二:初始化配置。启动前把配置拉下来放到共享 volume,主容器挂载使用(主容器镜像里就不用塞 git 之类的工具了):

apiVersion: v1
kind: Pod
metadata:
name: app-with-init-config
spec:
initContainers:
- name: fetch-config
image: alpine/git:2.45
command: ['sh', '-c', 'git clone https://example.com/config.git /config']
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: myapp:v1
volumeMounts:
- name: config
mountPath: /etc/myapp
volumes:
- name: config
emptyDir: {}
Init 容器能配探针吗

不能。Init 容器不支持 livenessProbe、readinessProbe 等探针——它们的生命周期就是「跑完即走」,K8s 直接用退出码判断成败,探针没有意义。

Sidecar​

Sidecar(边车) 指和主容器跑在同一个 Pod 里的辅助容器,给主容器「打下手」。典型场景:

  • 日志收集:主容器把日志写到共享 volume,Fluent Bit / Filebeat 边车读取并转发到日志中心,主容器完全不用关心日志后端。
  • 代理:Istio、Linkerd 等服务网格给每个 Pod 注入 Envoy 边车,接管全部进出流量,实现 mTLS、限流、遥测,业务零感知。
  • 配置/证书同步:边车监听配置中心,把新配置写进共享 volume 并通知主进程 reload。

1.29 起有「原生边车」:以前边车写在 containers 里有两个痛点——主容器和边车的就绪顺序不可控;Job 类 Pod 主容器跑完后边车不退出、Pod 永远不结束。Kubernetes 1.29 起(beta,默认开启)支持把边车写进 initContainers 并加 restartPolicy: Always:它在主容器之前启动、启动成功后持续运行(而不是跑完退出)、可以配探针卡住后续容器的启动顺序,主容器全部退出后 kubelet 还会主动把它停掉:

apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
initContainers:
- name: log-agent # 原生边车:写在 initContainers 里
image: fluent/fluent-bit:3.1
restartPolicy: Always # 关键:让它一直跑,而不是跑完就退出
volumeMounts:
- name: logs
mountPath: /logs
containers:
- name: app
image: myapp:v1
volumeMounts:
- name: logs
mountPath: /var/log/myapp
volumes:
- name: logs
emptyDir: {}

ReplicaSet​

ReplicaSet(RS) 的职责一句话:保证任何时刻都有指定数量的 Pod 副本在运行。少了就补(Pod 挂了、节点宕机),多了就删。

ReplicaSet 本身很简单,核心就三个要素:replicas(要几个副本)、selector(按 label 匹配管哪些 Pod)、template(Pod 模板)。

基本不要直接创建 ReplicaSet

RS 只负责维持副本数,不负责更新:改了 Pod 模板里的镜像,RS 不会对存量 Pod 做任何事,只能手工删旧 Pod 让它重建。所以实践中从不直接写 RS,而是写 Deployment,由 Deployment 创建和管理 RS:每次更新 Deployment 会新建一个 RS 并逐步扩容,同时把旧 RS 逐步缩到 0(旧 RS 不删,留着回滚用)。kubectl get rs 里看到的 myapp-7d9f5b8c4d 这种带哈希后缀的名字,就是 Deployment 自动建的。

Deployment​

Deployment 是部署无状态应用的首选,也是最常用的工作负载资源。它通过管理 ReplicaSet,提供副本维持、滚动更新、回滚、扩缩等一整套能力。

关键字段​

字段作用
replicas期望副本数
selector选择器,必须和 template.metadata.labels 匹配(创建后不建议改)
strategy.type更新策略:RollingUpdate(默认,滚动替换)或 Recreate(先全删再全建,有短暂停机)
strategy.rollingUpdate.maxSurge更新中最多可以超出的副本数(详见文末 Rolling Update 一节)
strategy.rollingUpdate.maxUnavailable更新中最多允许不可用的副本数
revisionHistoryLimit保留多少个旧 ReplicaSet 用于回滚,默认 10;设为 0 则无法回滚
minReadySeconds新 Pod 就绪后再观察多久才继续滚动,默认 0
progressDeadlineSeconds滚动卡住的超时时间,默认 600 秒,超时标记为失败

完整示例​

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
labels:
app: nginx
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels:
app: nginx
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi

更新与回滚​

改镜像触发滚动更新(也可以直接改 YAML 再 kubectl apply):

kubectl set image deployment/nginx-deploy nginx=nginx:1.28
kubectl rollout status deployment/nginx-deploy
Waiting for deployment "nginx-deploy" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "nginx-deploy" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "nginx-deploy" successfully rolled out

查看历史版本、回滚到上一版(或指定版本):

kubectl rollout history deployment/nginx-deploy
REVISION  CHANGE-CAUSE
1 <none>
2 <none>
3 <none>
kubectl rollout undo deployment/nginx-deploy                  # 回滚到上一版
kubectl rollout undo deployment/nginx-deploy --to-revision=2 # 回滚到指定版本
deployment.apps/nginx-deploy rolled back
回滚的本质

回滚不是「反向滚动」,而是把某个旧 ReplicaSet 重新扩回 replicas、把当前 RS 缩到 0。这就是为什么 revisionHistoryLimit 不能设成 0——旧 RS 删光了就没东西可回滚了。

StatefulSet​

StatefulSet 用于有状态应用——数据库、消息队列、etcd 这类「每个实例都有身份、有数据」的负载。和 Deployment 的「所有副本完全等价、随意替换」不同,StatefulSet 给每个 Pod 一个稳定不变的身份。

它提供的三样东西​

  1. 稳定的网络标识:Pod 名字固定为 StatefulSet名-序号(如 mysql-0、mysql-1),重建、漂移后名字不变。配合 Headless Service(clusterIP: None 的 Service),每个 Pod 还有固定的 DNS 记录,格式为 Pod名.Service名.命名空间.svc.cluster.local,例如 mysql-0.mysql.default.svc.cluster.local——集群内可以按名字直接找到某一个实例。
  2. 稳定的存储:用 volumeClaimTemplates 声明 PVC 模板,StatefulSet 给每个 Pod 各建一块独立的 PVC(data-mysql-0、data-mysql-1……)。Pod 重建后仍挂回原来那块盘,数据不丢。
  3. 有序的部署和伸缩:默认策略 OrderedReady(podManagementPolicy 字段)——按序号从小到大依次创建(前一个 Running 且 Ready 才建下一个),缩容、删除时按序号从大到小逆序进行。这契合主从数据库「先起主、再起从」这类启动顺序要求。如果实例间没有顺序依赖,可以设成 Parallel,全部并行创建和删除。

示例​

apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None # Headless Service:不给 VIP,DNS 直接解析到各 Pod
selector:
app: mysql
ports:
- port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # 必须指向上面那个 Headless Service
replicas: 3
podManagementPolicy: OrderedReady
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.4
ports:
- containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates: # 每个 Pod 独立一块 10Gi 的盘
- metadata:
name: data
spec:
accessModes: ['ReadWriteOnce']
resources:
requests:
storage: 10Gi
和 Deployment 怎么选

判断标准很简单:副本之间有没有区别。完全等价、随便换——Deployment;每个实例有名字、有顺序、有各自的数据——StatefulSet。另外注意 StatefulSet 的 PVC 在缩容甚至删除 StatefulSet 时都不会自动删除,数据安全要自己兜底。

DaemonSet​

DaemonSet 保证每个(符合条件的)节点上都跑且只跑一个 Pod 副本:新节点加入集群自动在上面建 Pod,节点移除 Pod 随之被回收。它不管「要几个副本」——副本数由节点数决定。

典型场景全是节点级的基础设施:

  • 日志收集 agent:Fluentd、Filebeat,收集本节点所有容器的日志;
  • 监控 agent:Prometheus node-exporter,采集本节点指标;
  • 网络/存储插件:Calico、Cilium、kube-proxy、CSI 节点驱动等。

容忍污点方面:DaemonSet 控制器会自动给 Pod 附加一组内置 toleration(node.kubernetes.io/not-ready、unreachable、disk-pressure 等节点状态类污点),保证节点异常时这些 agent 不会被轻易驱逐;而控制平面节点的 node-role.kubernetes.io/control-plane 污点需要显式加容忍,很多系统组件干脆 operator: Exists 容忍一切污点:

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
tolerations:
- operator: Exists # 容忍所有污点,控制平面节点上也会跑
containers:
- name: node-exporter
image: prom/node-exporter:v1.8
ports:
- containerPort: 9100

DaemonSet 也支持用 nodeSelector、nodeAffinity 把 Pod 限定到部分节点(比如只跑在带 GPU 的节点上),更新策略同样支持 RollingUpdate。

Job​

前面几种控制器都假设应用「一直跑」,Job 则是跑一次性任务:批处理、数据迁移、离线计算——跑完就结束,Pod 完成后不再重启。

关键字段​

字段作用默认值
completions需要成功完成的 Pod 次数1
parallelism最多同时并行运行几个 Pod1
backoffLimit失败重试次数上限,超过则整个 Job 标记失败6
activeDeadlineSecondsJob 整体最长存活时间,超时强制终止所有 Pod无
ttlSecondsAfterFinished完成后多久自动删除 Job 及其 Pod无(不清理)

completions 和 parallelism 组合出几种模式:单任务(都设为 1)、固定次数的并行任务(如 completions: 10, parallelism: 3,像线程池一样分三批跑完 10 个)、以及配合消息队列的 work queue 模式(不设 completions,Pod 自己从队列取任务直到队列空)。

示例​

apiVersion: batch/v1
kind: Job
metadata:
name: pi-calc
spec:
completions: 4
parallelism: 2
backoffLimit: 3
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never # Job 的 Pod 只允许 Never 或 OnFailure
containers:
- name: pi
image: perl:5.40
command: ['perl', '-Mbignum=bpi', '-wle', 'print bpi(2000)']
Job 的 Pod 模板里 restartPolicy 的讲究

Deployment 里 restartPolicy 只能是 Always,Job 里则只能是 Never 或 OnFailure:失败时要么整个 Pod 重建(Never),要么只重启容器(OnFailure,保留日志现场方便排查)。这是「跑完即走」和「永远在线」两类语义的分界线。

CronJob​

CronJob = 定时执行的 Job,到点自动创建一个 Job,思路和 Linux 的 crontab 一致。

schedule 语法速查​

和 crontab 完全相同的五段式,空格分隔:

字段取值范围含义
第 1 段0-59分钟
第 2 段0-23小时
第 3 段1-31日
第 4 段1-12月
第 5 段0-6星期几(0 表示周日)

支持 *(每个)、,(列举)、-(区间)、/(步长)写法,例如 */10 * * * * 表示每 10 分钟一次,0 3 * * 1-5 表示工作日凌晨 3 点。K8s 1.27 起还可以用 timeZone 字段显式指定时区(如 Asia/Shanghai),不写则按 kube-controller-manager 所在时区解释。

其他关键字段​

字段作用
concurrencyPolicy上一次还没跑完就到点了怎么办:Allow(默认,照样并发跑)/ Forbid(跳过这次)/ Replace(取消旧的、跑新的)
startingDeadlineSeconds错过调度时间超过这个秒数就不再补跑,记为失败
successfulJobsHistoryLimit保留几个成功 Job 的历史,默认 3
failedJobsHistoryLimit保留几个失败 Job 的历史,默认 1
suspend设为 true 暂停调度,不影响已在跑的 Job

示例​

apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: '0 3 * * *' # 每天凌晨 3 点
timeZone: Asia/Shanghai
concurrencyPolicy: Forbid # 上次备份还没跑完就跳过这次
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: mysql:8.4
command: ['sh', '-c', 'mysqldump -h mysql -u root app > /backup/app.sql']
并发策略要想清楚

对「跑两次会有副作用」的任务(发券、计费、清数据),concurrencyPolicy 至少要用 Forbid,必要时任务内部还要加分布式锁兜底——K8s 的并发控制是尽力而为,调度延迟、控制器重启都可能造成边界情况。

HorizontalPodAutoscaler​

HPA 按指标自动调整副本数(scaleTargetRef 指向 Deployment、StatefulSet 等可伸缩资源),是应对流量波动的主力。

指标从哪来​

  • 资源指标(CPU/内存):来自 metrics-server,需要提前装好(很多发行版自带)。HPA 算的是「实际用量 ÷ request」的利用率百分比,所以没配 resources.requests 的 Pod 无法按 CPU/内存扩缩。
  • 自定义/外部指标(custom/external metrics):比如每个 Pod 的 QPS、消息队列堆积量。需要额外的适配器(如 Prometheus Adapter,把 Prometheus 指标注册成 custom.metrics.k8s.io API),HPA 才能拿业务指标做扩缩。

autoscaling/v2 示例​

v2 版本可以在一个资源里同时配多组指标(每组指标都会算出一个目标副本数,取最大值):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # CPU 利用率(相对 request)超 60% 就扩容
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: 500 # 每个 Pod 平均 QPS 超 500 也扩容

behavior:控制扩缩容速率​

默认的扩缩节奏比较「猛」(扩容激进、缩容带 5 分钟稳定窗口)。behavior 字段可以分方向精细化控制速率,防止指标抖动导致副本数来回跳:

spec:
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容不等稳定窗口,尽快扩
policies:
- type: Pods
value: 4
periodSeconds: 60 # 每分钟最多新增 4 个 Pod
scaleDown:
stabilizationWindowSeconds: 300 # 缩容前指标要连续 5 分钟偏低
policies:
- type: Percent
value: 10
periodSeconds: 60 # 每分钟最多缩掉 10% 的 Pod

经验法则:扩容要快(扛流量)、缩容要慢(防抖动)。

VerticalPodAutoscaler​

VPA 调整的是单个 Pod 的资源量(CPU/内存的 request),和 HPA 动副本数是正交的两个维度。它不在 K8s 核心代码里,需要单独安装 VPA 组件(recommender、updater、admission-controller 三部分)。

updateMode 四种模式​

模式行为
Off只计算推荐值、写在 VPA 对象的 status 里,不改 Pod(适合先观察推荐值合不合理)
Initial只在 Pod 创建时注入推荐值,之后不再调整
Recreate推荐值变化时驱逐重建 Pod 让新值生效(会造成单副本应用中断,需配 PDB)
Auto目前等价于 Recreate;将来会切换到原地调整(InPlace,不重启容器直接改 request)
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web
updatePolicy:
updateMode: Initial
不要和按 CPU/内存扩缩的 HPA 同时用

HPA 按「用量 ÷ request」的利用率扩缩,VPA 恰好会去改 request——两个控制器同时盯同一个指标会互相干扰:VPA 一改 request,利用率基数就变了,HPA 可能做出相反方向的决策。要组合使用,就让 HPA 走自定义指标(如 QPS),VPA 只管 CPU/内存的 request。

PodDisruptionBudget​

PDB 保护应用免受「自愿中断」(voluntary disruption)的影响。自愿中断指管理员主动触发的操作:kubectl drain 排空节点、节点升级、集群缩容等;Pod 崩溃、节点宕机、资源不足被驱逐这类「非自愿中断」PDB 管不了。

PDB 只有两个核心字段,二选一:

字段含义
minAvailable中断过程中至少要有多少个(或百分之几)Pod 可用
maxUnavailable中断过程中最多允许多少个(或百分之几)Pod 不可用
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2 # 或写成 maxUnavailable: 1
selector:
matchLabels:
app: web

和 drain 的关系​

kubectl drain 驱逐 Pod 时走的不是普通删除,而是 Eviction API,会先检查 PDB:如果驱逐这个 Pod 会让可用副本跌破 minAvailable,驱逐请求就被拒绝,drain 会阻塞等待并不断重试,直到条件满足(其他节点补起了副本、或有人放宽了 PDB)。这就是为什么有时 drain 会卡住半天——先查 PDB 是不是配得太苛刻,或者副本数本身就不够。

常见搭配

3 副本的 Deployment 配 minAvailable: 2(或 maxUnavailable: 1)是很常见的组合:升级节点时每次最多驱逐 1 个,集群始终有 2 个副本扛流量。注意 minAvailable: 100% 等于「谁也不许动」,drain 会永远卡住,基本不要这么配。

生命周期​

Pod 从创建到删除有一整套钩子与终止流程,发布不停机全靠对这段流程的理解。

postStart 与 preStop 钩子​

容器级别可以配两个 hook(lifecycle 字段):

  • postStart:容器创建后立即执行(不保证早于容器的 ENTRYPOINT,两者是并发的)。常用来做主进程启动后的注册、预热动作;hook 执行失败容器会被杀掉。
  • preStop:容器收到终止信号之前执行,同步阻塞终止流程——hook 跑完才发 SIGTERM。常用来通知外部组件摘流量,或者先睡几秒等转发规则收敛。

hook 的处理方式有三种:exec(执行命令)、httpGet(发 HTTP 请求)、sleep(纯等待,1.29 起)。

spec:
containers:
- name: web
image: myapp:v1
lifecycle:
postStart:
exec:
command: ['sh', '-c', 'echo started >> /var/log/lifecycle.log']
preStop:
exec:
command: ['sh', '-c', 'sleep 5; nginx -s quit'] # 先等 5 秒再优雅退出

终止流程:SIGTERM → 宽限期 → SIGKILL​

Pod 被删除(滚动更新、缩容、驱逐都会触发)时的完整时间线:

1. Pod 标记为 Terminating,宽限期开始倒计时
2. 两件并行发生的事:
a. Pod 从 Service 的 Endpoints/EndpointSlice 中摘除(kube-proxy 异步更新转发规则)
b. kubelet 执行 preStop hook(若有),然后向容器主进程(PID 1)发送 SIGTERM
3. 等待 terminationGracePeriodSeconds 秒(默认 30,可在 Pod spec 配置)
4. 宽限期到点进程还没退出 → 发送 SIGKILL 强制杀死,Pod 从 API 中删除

应用收到 SIGTERM 后应该停止接收新请求、处理完在途请求再退出——这就是「优雅下线」。宽限期只约束从 SIGTERM 到 SIGKILL 的时长,kubectl delete pod --grace-period=N 可以临时覆盖。

优雅下线最大的坑:流量还在来

注意上面第 2 步:endpoint 摘除和 SIGTERM 是并行发生的,而 kube-proxy 更新 iptables/IPVS 规则、云厂商 LB 更新后端都需要时间(几百毫秒到几秒)。也就是说 Pod 收到 SIGTERM 开始退出时,仍可能有新流量被转发进来,直接退出就会丢请求。

常用解法:preStop 里先 sleep 几秒(如上面示例),等转发规则收敛后再让主进程退出;同时应用本身要在收到 SIGTERM 后进入「拒收新请求、处理完旧请求」的 draining 模式,而不是立刻 kill 自己。

探针(Liveness/Readiness/Startup)​

kubelet 通过三种探针检查容器健康。三者职责完全不同,混用是新手最常踩的坑:

探针回答的问题失败后的动作典型用途
Liveness(存活)进程还活着吗?重启容器(kill 后按 restartPolicy 拉起)检测死锁、假死:进程在,但已无法工作
Readiness(就绪)现在能接流量吗?从 Service endpoints 摘除,不重启容器启动预热、依赖未就绪、主动摘流量
Startup(启动)启动完成了吗?重启容器;探测成功前禁用另外两种探针保护慢启动的老应用(启动要几分钟)

关键区分:Liveness 失败 = 重启,Readiness 失败 = 摘流量不重启。如果把本该配 Readiness 的检查写到 Liveness 上(比如依赖的下游挂了导致探针失败),会造成容器被连环重启的雪崩。Startup 探针则解决「应用启动要 3 分钟,Liveness 30 秒就把它杀了」的问题:启动期间只由 Startup 负责探测,成功后才交给 Liveness 和 Readiness。

探测方式有四种:exec(容器内执行命令)、httpGet(HTTP GET 指定路径,返回 200-399 视为成功)、tcpSocket(能建立 TCP 连接即可)、grpc(1.27 起稳定,走 gRPC 健康检查协议)。

spec:
containers:
- name: web
image: myapp:v1
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 30 # 最多给 5s x 30 = 150 秒启动时间
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5

常用参数:initialDelaySeconds(容器启动后等多久才开始探,配了 Startup 探针后基本用不上)、periodSeconds(探测间隔,默认 10)、timeoutSeconds(单次探测超时,默认 1)、successThreshold(连续成功几次算通过,默认 1)、failureThreshold(连续失败几次算失败,默认 3)。

健康检查接口的设计

/healthz(Liveness)只检查进程自身活没活,不要在里面查数据库、查下游——下游抖动会害你被重启;/ready(Readiness)才去检查依赖是否就绪。两者分开,是探针正确使用的核心。

Rolling Update​

滚动更新是 Deployment(以及 StatefulSet、DaemonSet)的默认更新策略:不一次性全换,而是逐批用新 Pod 替换旧 Pod,整个过程中始终有足够副本对外提供服务,实现不停机发布。

maxSurge 与 maxUnavailable​

两个参数控制滚动的节奏,都可以写绝对数或百分比(相对 replicas 计算):

  • maxSurge:更新过程中,总 Pod 数最多可以超出期望副本多少(多创建的新 Pod),默认 25%。
  • maxUnavailable:更新过程中,最多允许多少个 Pod 不可用(还没就绪的旧 Pod 和新 Pod 都算),默认 25%。

百分比取整规则要记一下:maxSurge 向上取整,maxUnavailable 向下取整。

计算举例​

replicas: 10,两个参数都用默认值 25%:

  • maxSurge = ⌈10 × 25%⌉ = ⌈2.5⌉ = 3 → 更新中最多同时存在 10 + 3 = 13 个 Pod;
  • maxUnavailable = ⌊10 × 25%⌋ = ⌊2.5⌋ = 2 → 任意时刻至少保证 10 - 2 = 8 个可用 Pod。

实际滚动节奏大概是:先新建最多 3 个新 Pod → 等新 Pod 就绪 → 删掉等量旧 Pod → 再补新 Pod……直到旧 Pod 全部换完,全程可用副本不低于 8。

常见组合怎么选​

组合效果适用
默认 25% / 25%平滑、资源开销适中大多数场景
maxSurge: 1, maxUnavailable: 0先增后删:先起一个新 Pod,就绪后才删一个旧的,全程可用副本数等于期望值对可用性敏感、且集群有富余资源
maxSurge: 0, maxUnavailable: 1先删后增:不超发、省资源,但过程中可用副本少 1 个资源紧张,或应用不支持多版本共存
strategy.type: Recreate不滚动:旧的全删光再起新的,有停机窗口单副本且不能多实例并存的应用(如本地盘独占)

注意 maxSurge 和 maxUnavailable 不能同时为 0——那样滚动就没法进行了(控制器会直接拒绝)。另外滚动更新的前提是新旧版本可以同时对外服务;如果新版带了不兼容的 schema 变更,靠滚动更新本身兜不住,需要配合兼容设计或蓝绿、金丝雀发布。