课程目录(第 11 章 / 共 33 章)
课程/让应用可访问、可配置

探针与资源管理:让集群理解你的应用

11 章 / 共 33·18 分钟·入门探针资源管理kubectl

用 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 成功之前,livenessProbereadinessProbe 都不会执行。所以有了 startupProbe,就不用再给 liveness 配一个很大的 initialDelaySeconds 去赌启动时间。共通的字段有 periodSeconds(探测间隔)、failureThreshold(连续失败几次算失败)、successThreshold(连续成功几次算恢复,liveness 只能是 1)。

判定流程和失败后果串起来是这样:

text
容器启动 ──► startupProbe:启动完成了吗?
                 │ ✓ 成功                     ✗ 连续失败 ──► 重启容器

             readinessProbe:现在能接流量吗? ── ✗ 失败 ──► 标记 NotReady,
                 │ ✓ 成功                                  从 EndpointSlice 摘掉
                 ▼                                        (容器不重启,只是没流量)
             livenessProbe:还活着吗? ──────── ✗ 失败 ──► 重启容器(RESTARTS 增长,
                 │ ✓ 成功                                  可能进入 CrashLoopBackOff)

             Service 把流量发给这个 Ready 的 Pod

三种探测方式

方式写法成功条件
httpGet指定 pathport,可加 httpHeaders返回状态码 200 到 399
tcpSocket指定 portTCP 连接能建立成功
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 只能是 1liveness 写大于 1 会被 API Server 拒绝

startupProbeperiodSeconds × failureThreshold 就是允许的最长启动时间,按应用实测启动时间留 2 到 3 倍余量即可。

动手:给 web 加上三种探针

把 nginx 部署成 web,加上完整的三种探针和一个 Service。

yaml
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: http
bash
kubectl apply -f web-probes.yaml
kubectl get pods -n demo -l app=web
kubectl get endpointslices -n demo

kubectl get pods 里两个 Pod 都显示 READY 1/1STATUS Running(分子是就绪容器数,分母是容器总数);kubectl get endpointslicesweb 这一行的 ENDPOINTS 列会列出两个 Pod IP。只有 Ready 的 Pod 才会出现在这里,这是 Service 能做负载均衡的基础。

验证:readiness 摘流量,liveness 重启容器

做一次「故意写错探针」的实验,这是理解三种探针差别最快的方式。先准备一份探针写错的清单:web-bad 只有一个副本,readiness 指向 nginx 并不存在的 /healthz

yaml
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 路径写错,看流量怎么被摘掉

  1. 先确认基线:kubectl get pods -n demo -l app=web 两个 Pod 都是 READY 1/1kubectl get endpointslices -n demoweb 那一行的 ENDPOINTS 列有两个 Pod IP。
  1. 应用上面那份写错的清单,等十几秒让探针连续失败:
bash
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-bad

Pod 是 Running,但 READY 列是 0/1web-bad 的 EndpointSlice 的 ENDPOINTS 列是空的,说明 Service 不会把流量发给它;事件里写着 Readiness probe failed: HTTP probe failed with statuscode: 404注意 `RESTARTS` 还是 0——readiness 失败只摘流量,不重启容器。

  1. 改回正确路径,确认流量回来:
bash
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 demo

ENDPOINTS 列出现一个 Pod IP,说明它被加回流量列表。

  1. 再看 liveness 失败的效果:把 readinessProbe 换成 livenessProbe(路径同样是错的),失败的后果就从「摘流量」变成「重启容器」:
bash
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 列开始增长,最终进入 CrashLoopBackOffkubectl describe pod 里能看到 Liveness probe failedContainer failed liveness probe, will be restarted。验证完用 kubectl delete -f web-bad.yaml 清理。

探针怎么选才不会互相打架

只读接口用 httpGet;纯 TCP 服务用 tcpSocketreadinessProbe 可以检查下游依赖,livenessProbe 千万不要检查下游依赖——数据库抖一下就会把所有副本一起重启,反而放大故障。启动慢就用 startupProbe,别用巨大的 initialDelaySeconds 去赌。

requests 与 limits:让调度器和内核都心里有数

探针管的是「健康」,资源字段管的是「够不够用」。它们是一对:

字段作用谁在读它
requests声明「至少给我这么多」。调度器据此选节点,CPU 紧张时也按这个比例分配时间片kube-scheduler、kubelet
limits声明「最多只能用这么多」。内存超限直接 OOMKilled,CPU 超限只会被限流内核 cgroup

调度器怎么用 requests 挑节点,见第 14 章 调度。单位要记清楚:CPU 的 1 表示 1 个核,500m 是半个核,100m 是 0.1 个核(m 是毫核);内存的 MiGi 是 1024 进制,MG 是 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最先被驱逐
bash
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 默认没装,可以这样补上:

bash
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-server

kind 的 kubelet 用的是自签证书,所以需要 --kubelet-insecure-tls,否则 metrics-server 抓不到数据。等半分钟左右再执行:

bash
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 podkubectl -n kube-system logs deploy/metrics-server 继续查。

metrics-server 的证书、校验与 HPA 的指标来源

上一节用一条 kubectl apply 就装上了 metrics-server,但它在生产里最容易坏在两个 TLS 环节上。先看它到底插在哪条链路里:

text
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 → kubeletmetrics-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 挂进去,让它正常校验:

bash
# 仅本地测试:跳过 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,要把聚合层验一遍:

bash
kubectl get apiservice v1beta1.metrics.k8s.io                 # AVAILABLE 必须为 True
kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes | head -c 300
kubectl top nodes

apiservice 那一行显示 True--raw 能返回 JSON,才说明「API Server → metrics-server → kubelet」整条链路通了。只看到 Pod Running 但 AVAILABLE=False,问题一定在上面那两处 TLS 环节之一。

HPA 的指标来源也是它。autoscaling/v2 的 HPA 在 type: Resource 时读的就是 metrics.k8s.io 给出的容器实际用量,再除以 requests 算出利用率——目标工作负载必须写 requests,否则百分比算不出来:

bash
kubectl get hpa -n demo
kubectl describe hpa web -n demo | tail -20

TARGETS 列显示 <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 是 RunningREADY 0/1,Service 访问不到readiness 探测失败kubectl describe pod 里的 Readiness probe failed: HTTP probe failed with statuscode: 404修正 pathport;确认依赖是否真的就绪
RESTARTS 不断增长,最终 CrashLoopBackOffliveness 探测失败触发重启kubectl describe podLiveness probe failedLast State调大 failureThresholdinitialDelaySeconds,慢启动应用改用 startupProbe
Last State: TerminatedReason: OOMKilledExit Code: 137内存超过 limits.memorykubectl describe podkubectl top pod 看实测内存调大内存 limit,或排查应用是否泄漏
没有报错,但延迟变高、抖动CPU 超过 limit 被限流(throttling)kubectl top pod 看 CPU 是否长期贴着 limit提高或去掉 CPU limit,别拍脑袋设小值
Pod 一直 Pendingrequests 超过任何节点的可分配资源kubectl describe pod 事件里的 Insufficient cpu / Insufficient memory调小 requests,或给集群加节点
kubectl top 报 x509 / Metrics API not availablemetrics-server 抓不到 kubelet 的自签证书,或 APIService 没就绪kubectl get apiservice v1beta1.metrics.k8s.iologs deploy/metrics-server本地测试加 --kubelet-insecure-tls,生产改为挂载集群 CA
HPA 的 TARGETS 显示 <unknown>/50%没设 requests,或 metrics-server 没有数据kubectl top pod 有没有数字、Pod 有没有 resources.requests补上 requests;先修好 metrics-server

四个高频问题

  • 内存 limit 太低导致 OOMKilledkubectl get pod 看到 CrashLoopBackOffkubectl describe podLast State 里写着 Reason: OOMKilledExit 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 失败才会重启容器。
  • 探测方式有 httpGettcpSocketexec 三种,判断标准要轻量、无副作用。
  • requests 决定调度与 CPU 分配,limits 是硬上限:内存超了 OOMKilled,CPU 超了被限流。
  • QoS 按 requests/limits 的组合分为 Guaranteed、Burstable、BestEffort,节点资源紧张时按这个顺序被驱逐。

练习

  1. 把 web 的 limits.memory 改成 8Mi 再 apply,观察 Pod 状态变化,并用 kubectl describe pod 找出原因,然后改回来。
  2. 给 redis 写一个 tcpSocket 的 readinessProbe(端口 6379),验证它在 kubectl get endpointslices 里的表现。
  3. demo 命名空间建一个 LimitRange,把默认 requests 设为 cpu: 50mmemory: 64Mi,然后创建一个没写 resources 的 Pod,看它的 QoS 类别变成什么。

应用的健康和资源都有了保障,但一个集群往往要给多个团队用:怎么隔离、怎么限制、谁有权限看哪个命名空间?下一章第 12 章 命名空间与 RBAC回答这些问题。

相关章节:第 7 章 Service(EndpointSlice 与流量分发)、第 14 章 调度(requests 如何影响调度)、第 15 章 排障手册CrashLoopBackOff 的排查流程)。