课程目录(第 11 章 / 共 33 章)
探针与资源管理:让集群理解你的应用
用 liveness、readiness、startup 三种探针告诉集群「我的应用现在能不能接流量」,再用 requests/limits 让调度器和内核都知道该给它多少资源。
学完这一章,你将能够
- ✓说得出三种探针分别在什么场景下用,失败后集群会做什么
- ✓会写 httpGet、tcpSocket、exec 三种探测方式,并配置合理的阈值
- ✓能为容器设置 requests 与 limits,判断自己属于哪种 QoS 类别
探针解决什么问题
前面几章我们一直在做同一件事:让 Pod 跑起来。但「跑起来」和「能用」是两回事:进程还在,不代表配置加载完了;端口还在监听,不代表数据库连得上;内存悄悄涨到上限,容器会被内核直接杀掉,而 Deployment 只会默默换一个新的。
如果集群对应用的状态一无所知,它只能靠猜:服务刚启动就把流量打进来,用户看到 502;某个副本卡死了(进程活着但不再响应),Service 还一直把请求发给它;死锁的进程永远不退出,Pod 就那么挂着,没人发现。
探针(Probe)就是应用主动告诉集群「我现在怎么样」的接口。 你定义探测方式和判断标准,kubelet 按周期去问,然后根据结果做三件事:把 Pod 从 Service 的流量列表里摘掉、重启容器、或者干脆先别管它。
三种探针的分工
| 探针 | 回答的问题 | 失败后果 | 典型场景 |
|---|---|---|---|
startupProbe | 容器启动完成了吗? | 按 failureThreshold 重试,超过就重启容器 | 启动要几十秒的 Java 应用、要预热缓存的进程 |
readinessProbe | 现在能接流量吗? | 标记为 NotReady,从 Service 的 EndpointSlice 里摘掉,但不重启 | 依赖的下游还没就绪、正在重载配置 |
livenessProbe | 还活着吗? | 重启容器 | 死锁、假死、无法自行恢复的进程 |
三个探针可以同时存在,执行顺序是:startupProbe 成功之前,livenessProbe 和 readinessProbe 都不会执行。所以有了 startupProbe,就不用再给 liveness 配一个很大的 initialDelaySeconds 去赌启动时间。共通的字段有 periodSeconds(探测间隔)、failureThreshold(连续失败几次算失败)、successThreshold(连续成功几次算恢复,liveness 只能是 1)。
判定流程和失败后果串起来是这样:
容器启动 ──► startupProbe:启动完成了吗?
│ ✓ 成功 ✗ 连续失败 ──► 重启容器
▼
readinessProbe:现在能接流量吗? ── ✗ 失败 ──► 标记 NotReady,
│ ✓ 成功 从 EndpointSlice 摘掉
▼ (容器不重启,只是没流量)
livenessProbe:还活着吗? ──────── ✗ 失败 ──► 重启容器(RESTARTS 增长,
│ ✓ 成功 可能进入 CrashLoopBackOff)
▼
Service 把流量发给这个 Ready 的 Pod三种探测方式
| 方式 | 写法 | 成功条件 |
|---|---|---|
httpGet | 指定 path 与 port,可加 httpHeaders | 返回状态码 200 到 399 |
tcpSocket | 指定 port | TCP 连接能建立成功 |
exec | 指定 command 数组 | 命令退出码为 0 |
httpGet 最常用,也最贴近真实流量;tcpSocket 适合没有 HTTP 接口的服务(比如 redis、MySQL);exec 适合用脚本做复杂判断,但命令是在容器里执行的,镜像里没有那个命令,探针就会一直失败。
探针的判断标准要尽量轻量、无副作用。它会被高频调用,写一个每次都查全表的接口,等于给自己加了一个慢查询。
探针参数怎么定
阈值不是拍脑袋写的,它们决定了「多久发现故障」和「会不会误判」之间的平衡:
| 参数 | 作用 | 经验值 | 说明 |
|---|---|---|---|
initialDelaySeconds | 容器启动后等多久开始探测 | 有 startupProbe 时留 0 到 5 秒 | 设太大掩盖问题,设太小会误杀慢启动应用 |
periodSeconds | 探测间隔 | readiness 5 到 10 秒,liveness 10 到 30 秒 | 越短越灵敏,也越消耗资源 |
timeoutSeconds | 单次探测超时 | 1 到 3 秒,建议小于 periodSeconds | 依赖慢的下游很容易超时失败;超时设得比间隔还长,探测会排队 |
failureThreshold | 连续失败几次算失败 | readiness 3,liveness 3,startup 用「实测启动时间 ÷ periodSeconds」再乘 2 到 3 | 太小会误重启,太大发现故障慢 |
successThreshold | 连续成功几次算恢复 | readiness 1 到 2,liveness 只能是 1 | liveness 写大于 1 会被 API Server 拒绝 |
startupProbe 的 periodSeconds × failureThreshold 就是允许的最长启动时间,按应用实测启动时间留 2 到 3 倍余量即可。
动手:给 web 加上三种探针
把 nginx 部署成 web,加上完整的三种探针和一个 Service。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- name: http
containerPort: 80
startupProbe:
httpGet:
path: /
port: http
periodSeconds: 2
failureThreshold: 30
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /
port: http
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: demo
spec:
selector:
app: web
ports:
- name: http
port: 80
targetPort: httpkubectl apply -f web-probes.yaml
kubectl get pods -n demo -l app=web
kubectl get endpointslices -n demokubectl get pods 里两个 Pod 都显示 READY 1/1、STATUS Running(分子是就绪容器数,分母是容器总数);kubectl get endpointslices 中 web 这一行的 ENDPOINTS 列会列出两个 Pod IP。只有 Ready 的 Pod 才会出现在这里,这是 Service 能做负载均衡的基础。
验证:readiness 摘流量,liveness 重启容器
做一次「故意写错探针」的实验,这是理解三种探针差别最快的方式。先准备一份探针写错的清单:web-bad 只有一个副本,readiness 指向 nginx 并不存在的 /healthz。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-bad
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: web-bad
template:
metadata:
labels:
app: web-bad
spec:
containers:
- name: web
image: nginx:1.27
readinessProbe:
httpGet:
path: /healthz
port: 80
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: web-bad
namespace: demo
spec:
selector:
app: web-bad
ports:
- name: http
port: 80
targetPort: 80动手练习:把 readiness 路径写错,看流量怎么被摘掉
- 先确认基线:
kubectl get pods -n demo -l app=web两个 Pod 都是READY 1/1,kubectl get endpointslices -n demo里web那一行的ENDPOINTS列有两个 Pod IP。
- 应用上面那份写错的清单,等十几秒让探针连续失败:
kubectl apply -f web-bad.yaml
kubectl get pods -n demo -l app=web-bad
kubectl get endpointslices -n demo
kubectl describe pod -n demo -l app=web-badPod 是 Running,但 READY 列是 0/1;web-bad 的 EndpointSlice 的 ENDPOINTS 列是空的,说明 Service 不会把流量发给它;事件里写着 Readiness probe failed: HTTP probe failed with statuscode: 404。注意 `RESTARTS` 还是 0——readiness 失败只摘流量,不重启容器。
- 改回正确路径,确认流量回来:
kubectl patch deployment web-bad -n demo --type=json -p '[
{"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/httpGet/path", "value": "/"}
]'
kubectl rollout status deployment/web-bad -n demo
kubectl get endpointslices -n demoENDPOINTS 列出现一个 Pod IP,说明它被加回流量列表。
- 再看 liveness 失败的效果:把
readinessProbe换成livenessProbe(路径同样是错的),失败的后果就从「摘流量」变成「重启容器」:
kubectl patch deployment web-bad -n demo --type=json -p '[
{"op": "remove", "path": "/spec/template/spec/containers/0/readinessProbe"},
{"op": "add", "path": "/spec/template/spec/containers/0/livenessProbe",
"value": {"httpGet": {"path": "/healthz", "port": 80}, "periodSeconds": 5, "failureThreshold": 2}}
]'
kubectl get pods -n demo -l app=web-bad -w几十秒后新 Pod 的 RESTARTS 列开始增长,最终进入 CrashLoopBackOff;kubectl describe pod 里能看到 Liveness probe failed 和 Container failed liveness probe, will be restarted。验证完用 kubectl delete -f web-bad.yaml 清理。
探针怎么选才不会互相打架
只读接口用 httpGet;纯 TCP 服务用 tcpSocket;readinessProbe 可以检查下游依赖,livenessProbe 千万不要检查下游依赖——数据库抖一下就会把所有副本一起重启,反而放大故障。启动慢就用 startupProbe,别用巨大的 initialDelaySeconds 去赌。
requests 与 limits:让调度器和内核都心里有数
探针管的是「健康」,资源字段管的是「够不够用」。它们是一对:
| 字段 | 作用 | 谁在读它 |
|---|---|---|
requests | 声明「至少给我这么多」。调度器据此选节点,CPU 紧张时也按这个比例分配时间片 | kube-scheduler、kubelet |
limits | 声明「最多只能用这么多」。内存超限直接 OOMKilled,CPU 超限只会被限流 | 内核 cgroup |
调度器怎么用 requests 挑节点,见第 14 章 调度。单位要记清楚:CPU 的 1 表示 1 个核,500m 是半个核,100m 是 0.1 个核(m 是毫核);内存的 Mi、Gi 是 1024 进制,M、G 是 1000 进制,Kubernetes 里习惯用 Mi。
实践上有个简单的起点:requests 填「平时用量」,limits 填「峰值留一点余量」。内存的 limits 不要低于 JVM、Node.js 这类运行时自己声明的堆上限,否则容器会在你以为还早的时候就 OOMKilled。
QoS 类别、LimitRange 与 ResourceQuota
Kubernetes 会按 requests/limits 的组合给 Pod 打上 QoS 标签,节点资源紧张时按这个顺序驱逐:
| QoS 类别 | 判定条件 | 被驱逐的优先级 |
|---|---|---|
Guaranteed | 每个容器都设了 CPU 和内存的 requests 与 limits,且两者相等 | 最后被驱逐 |
Burstable | 至少一个容器设了 requests 或 limits,但不满足 Guaranteed | 中间 |
BestEffort | 所有容器都没设任何 requests/limits | 最先被驱逐 |
kubectl get pod -n demo -l app=web -o jsonpath='{.items[0].status.qosClass}{"\n"}'上面 web 的配置里 requests 和 limits 不相等,所以结果是 Burstable。团队协作时还有两个常用对象:LimitRange 给命名空间里的容器设默认值和上下限,新人忘了写 requests 也能拿到默认值;ResourceQuota 限制整个命名空间的总量,比如「最多 4 核 CPU、8Gi 内存、20 个 Pod」,超过就创建失败。
用 kubectl top 看真实用量
requests 是你猜的,kubectl top 才是实测。它依赖集群里的 metrics-server,kind 默认没装,可以这样补上:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.7.2/components.yaml
kubectl patch deployment metrics-server -n kube-system --type=json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl -n kube-system rollout status deployment metrics-serverkind 的 kubelet 用的是自签证书,所以需要 --kubelet-insecure-tls,否则 metrics-server 抓不到数据。等半分钟左右再执行:
kubectl top pod -n demo
kubectl top node输出是 NAME / CPU(cores) / MEMORY(bytes) 三列。如果提示 error: Metrics API not available,说明 metrics-server 还没就绪,用 kubectl -n kube-system get pod 和 kubectl -n kube-system logs deploy/metrics-server 继续查。
metrics-server 的证书、校验与 HPA 的指标来源
上一节用一条 kubectl apply 就装上了 metrics-server,但它在生产里最容易坏在两个 TLS 环节上。先看它到底插在哪条链路里:
kubectl top / HPA 控制器
│ 读 metrics.k8s.io/v1beta1
▼
API Server ──(APIService 聚合代理)──▶ metrics-server
│ HTTPS 10250
▼
kubelet /metrics/resource(每个节点一个)metrics-server 不是一个普通的 Deployment,它通过 Aggregation Layer 把自己注册成 API Server 的一个扩展接口(APIService v1beta1.metrics.k8s.io)。kubectl top 和 HPA 都只是去读这个接口,从不直接连 metrics-server。链路上有两处证书校验,排错时先分清是哪一个:
| 环节 | 谁校验谁 | 典型报错 |
|---|---|---|
| API Server → metrics-server | 用 APIService 的 CA bundle 校验 metrics-server 的服务证书 | x509: certificate signed by unknown authority |
| metrics-server → kubelet | metrics-server 校验 kubelet 的服务证书 | x509: cannot validate certificate for 节点IP because it doesn't contain any IP SANs |
第二种在 kind、k3s 这类本地集群里几乎必然出现:kubelet 用自签证书,证书里也没有节点 IP 的 SAN。--kubelet-insecure-tls 的作用就是跳过这次校验,代价是放弃了对 kubelet 的身份验证——理论上任何人都能冒充 kubelet 给 metrics-server 喂假数据,而 HPA 会照着假数据扩缩容。所以它的定位是「本地开发/测试集群的临时开关」,生产上应该把集群 CA 挂进去,让它正常校验:
# 仅本地测试:跳过 kubelet 证书校验
helm install metrics-server metrics-server/metrics-server --namespace kube-system \
--set 'args={--kubelet-insecure-tls}'
# 生产:把集群 CA 挂进容器(需另外配 volume),并指定 CA 路径
helm install metrics-server metrics-server/metrics-server --namespace kube-system \
--set 'args={--kubelet-certificate-authority=/etc/ssl/certs/kube-ca.crt,--kubelet-preferred-address-types=InternalIP}'装完不要只看 Pod 是不是 Running,要把聚合层验一遍:
kubectl get apiservice v1beta1.metrics.k8s.io # AVAILABLE 必须为 True
kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes | head -c 300
kubectl top nodesapiservice 那一行显示 True、--raw 能返回 JSON,才说明「API Server → metrics-server → kubelet」整条链路通了。只看到 Pod Running 但 AVAILABLE=False,问题一定在上面那两处 TLS 环节之一。
HPA 的指标来源也是它。autoscaling/v2 的 HPA 在 type: Resource 时读的就是 metrics.k8s.io 给出的容器实际用量,再除以 requests 算出利用率——目标工作负载必须写 requests,否则百分比算不出来:
kubectl get hpa -n demo
kubectl describe hpa web -n demo | tail -20TARGETS 列显示 <unknown>/50% 时按顺序查两件事:kubectl top pod -n demo 有没有数字(没有就是 metrics-server 的问题),Pod 有没有写 resources.requests(没写就是配置的问题)。还要记住 metrics-server 的定位:默认每 15 秒采集一次(--metric-resolution),只保留最近几分钟的瞬时值,不能查历史、不能做告警。要回溯「三天前开始变慢」这类问题,得靠 可观测性 里的 Prometheus 或 VictoriaMetrics。
参考:reference/k8s-in-action/addons/metrics-server/README.md常见坑与排错
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
Pod 是 Running 但 READY 0/1,Service 访问不到 | readiness 探测失败 | kubectl describe pod 里的 Readiness probe failed: HTTP probe failed with statuscode: 404 | 修正 path 与 port;确认依赖是否真的就绪 |
RESTARTS 不断增长,最终 CrashLoopBackOff | liveness 探测失败触发重启 | kubectl describe pod 的 Liveness probe failed 与 Last State | 调大 failureThreshold 或 initialDelaySeconds,慢启动应用改用 startupProbe |
Last State: Terminated、Reason: OOMKilled、Exit Code: 137 | 内存超过 limits.memory | kubectl describe pod;kubectl top pod 看实测内存 | 调大内存 limit,或排查应用是否泄漏 |
| 没有报错,但延迟变高、抖动 | CPU 超过 limit 被限流(throttling) | kubectl top pod 看 CPU 是否长期贴着 limit | 提高或去掉 CPU limit,别拍脑袋设小值 |
Pod 一直 Pending | requests 超过任何节点的可分配资源 | kubectl describe pod 事件里的 Insufficient cpu / Insufficient memory | 调小 requests,或给集群加节点 |
kubectl top 报 x509 / Metrics API not available | metrics-server 抓不到 kubelet 的自签证书,或 APIService 没就绪 | kubectl get apiservice v1beta1.metrics.k8s.io、logs deploy/metrics-server | 本地测试加 --kubelet-insecure-tls,生产改为挂载集群 CA |
HPA 的 TARGETS 显示 <unknown>/50% | 没设 requests,或 metrics-server 没有数据 | kubectl top pod 有没有数字、Pod 有没有 resources.requests | 补上 requests;先修好 metrics-server |
四个高频问题
- 内存 limit 太低导致 OOMKilled:
kubectl get pod看到CrashLoopBackOff,kubectl describe pod的Last State里写着Reason: OOMKilled、Exit Code: 137。把 limit 调大,或者去查应用是不是真的泄漏了内存。 - CPU limit 造成 throttling:CPU 超限不会被杀死,只会被限流,表现是「没有报错但变慢、延迟抖动」。给 CPU 设 limit 前先看
kubectl top的实测值,别拍脑袋。 - 探针初值太激进:
initialDelaySeconds太小、failureThreshold太小,容器还没起来就被判定失败,于是反复重启,看起来像应用崩溃。慢启动应用优先用startupProbe兜住。 - startupProbe 反而更慢:
periodSeconds × failureThreshold就是最长的启动等待时间。设成2 × 30意味着真挂掉的容器要拖 60 秒才被发现;设得太小又会让慢启动应用一直重启。按实测启动时间留 2 到 3 倍余量。
自测题
自测:为什么 livenessProbe 里不应该去检查数据库连接?(点击展开答案)
因为 liveness 失败的动作是重启容器,而重启容器修不好数据库。如果 liveness 去 ping 数据库,数据库抖一下就导致所有副本一起被重启,故障不是被隔离而是被放大:重启中的 Pod 既不能服务,又给下游增加了重连压力。这类「依赖是否可用」的判断应该放在 readinessProbe 里——它只把 Pod 从流量列表摘掉,不重启容器,等依赖恢复后自动重新接入。一句话:liveness 只管进程自己还能不能干活,不管外部世界。
自测:为什么内存超过 limit 会被杀掉,CPU 超过 limit 只是变慢?(点击展开答案)
因为两者的资源性质不同。内存是不可压缩资源,一旦分配出去就没法从别的容器手里收回,超限的唯一办法是杀掉进程(OOMKilled,退出码 137);CPU 是可压缩资源,内核可以按时间片限流,让容器跑得慢一点,但不会让它崩溃。所以内存的 limit 要留足余量(尤其 JVM、Node.js 这类自己声明堆上限的运行时),而 CPU 的 limit 主要影响延迟。
自测:为什么加了 startupProbe 之后,liveness 的 `initialDelaySeconds` 可以调小?(点击展开答案)
因为在 startupProbe 成功之前,livenessProbe 根本不会开始探测。启动慢的这段时间由 startup 探针兜住,liveness 不需要再用一个很大的 initialDelaySeconds 去「赌」启动时间——那样做的话,如果应用真的启动失败了,集群要等很久才发现。用 startup 探针的好处是把「启动慢」和「运行中假死」两件事分开判断:启动阶段用宽松的阈值,运行阶段用严格的阈值。
小结
startupProbe管启动、readinessProbe管接不接流量、livenessProbe管要不要重启,三者职责不能混。- readiness 失败只会把 Pod 从 Service 的 EndpointSlice 摘掉,liveness 失败才会重启容器。
- 探测方式有
httpGet、tcpSocket、exec三种,判断标准要轻量、无副作用。 requests决定调度与 CPU 分配,limits是硬上限:内存超了 OOMKilled,CPU 超了被限流。- QoS 按 requests/limits 的组合分为 Guaranteed、Burstable、BestEffort,节点资源紧张时按这个顺序被驱逐。
练习
- 把 web 的
limits.memory改成8Mi再 apply,观察 Pod 状态变化,并用kubectl describe pod找出原因,然后改回来。 - 给 redis 写一个
tcpSocket的 readinessProbe(端口 6379),验证它在kubectl get endpointslices里的表现。 - 给
demo命名空间建一个LimitRange,把默认 requests 设为cpu: 50m、memory: 64Mi,然后创建一个没写 resources 的 Pod,看它的 QoS 类别变成什么。
应用的健康和资源都有了保障,但一个集群往往要给多个团队用:怎么隔离、怎么限制、谁有权限看哪个命名空间?下一章第 12 章 命名空间与 RBAC回答这些问题。
相关章节:第 7 章 Service(EndpointSlice 与流量分发)、第 14 章 调度(requests 如何影响调度)、第 15 章 排障手册(CrashLoopBackOff 的排查流程)。