1. 从一次线上事故说起:为什么单靠监控还不够
去年秋天我们上了一个裂变活动,凌晨两点流量突然翻了四倍。我守在电脑前,眼睁睁看着监控大盘上一片飘红,但进程就是没死——CPU 91%,内存涨到 3.7G,接口超时率 32%,而 Kubernetes 里的 Pod 状态依然显示 Running。那种“明明没死,但其实已经废了”的状态,比直接宕机更恶心。因为监控系统不会报警“进程僵死”,它只会告诉你“请求慢、超时多”,等你手动点开容器日志的时候,用户已经骂了三轮。
那晚之后我花了差不多两周时间,把服务的可用性重新捋了一遍,最后沉淀下来的核心思路就是三个东西的配合:探针负责“判定健康”,资源配额负责“限制失控”,弹性伸缩负责“动态补充容量”。我把这套组合叫作“永不宕机三板斧”。它不是银弹,但如果你把这三件事做到位,线上服务能扛住的突发流量至少提升一个量级,而且很多故障会从“人工救火”变成“自动恢复”。
这篇文章我会按实际落地顺序,把每一板斧的原理、配置、常见坑和三者如何协同讲清楚。适合正在用 Kubernetes、或者准备把服务容器化的团队,也适合那些还在用传统虚拟机、但想引入健康检查和弹性能力的人参考。内容不要求你有深厚的容器基础,但我会默认你大概知道 Pod、Deployment 这些概念。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计思路:三板斧如何分工协作
2.1 三个工具各管一段,别把它们混为一谈
探针、资源配额、弹性伸缩,这三者经常被分开成三个独立章节写在文档里,但真正让系统稳定的是它们之间的衔接。很多人只配了探针,发现服务还是会被打死;只配了资源配额,发现资源够用的时候也会无故雪崩;只配了弹性伸缩,发现扩容的 Pod 一启动就 OOM。原因就在于这三个东西是强耦合的。
探针管的是“活没活、能不能接流量”。存活探针判断进程是不是僵死了,僵死就重启;就绪探针判断这个实例能不能把流量分给它,不能就摘掉;启动探针解决慢启动服务的误杀问题。资源配额管的是“一个 Pod 能占用多少 CPU/内存、一个命名空间总共能用多少”,它是在资源维度上兜底,防止单实例把整个节点吃干榨净。弹性伸缩管的是“当压力变大时,实例数量能不能跟着变”,HPA 一类的机制会周期性观察指标,动态调整副本数。
我把它们的关系总结成一句话:探针决定“活着的质量”,配额决定“生存的上限”,伸缩决定“队伍的规模”。只有质量不行时,重启或摘除流量;只有上限失控时,配额强制切断;只有规模不够时,扩容补充。三者互相配合,少了任何一个,其他两个都会出现盲区。
2.2 为什么是这三样组合,而不是单纯加机器
你可能会想,把所有服务都设置成无状态,然后疯狂扩容不就完了?事实没那么简单。扩容的前提是新的 Pod 能正常启动并且能开始接流量,但很多服务在流量飙升时是“启动即崩溃”,要么资源申请不够被 OOM,要么启动后因为内存不足被存活探针反复杀掉。资源配额可以提前把每个副本的最大资源限制死,让新的 Pod 不会因为抢占资源而把节点搞垮;探针可以保证新副本在真正健康之前不接入流量,避免把故障扩散给用户。
这三样的协同,本质上是把系统从“被动响应”变成“主动闭环”。我举个生活化的类比:探针像大楼门口的保安,负责检查进出的人是不是活的、健康状态怎么样;资源配额是大楼的承重上限,不管你进多少人,楼不能塌;弹性伸缩是楼里的电梯调度,人多的时候自动多开几部电梯,人少的时候少开几部减少空转。保安、承重、调度,三件事各管各的,但少了任何一样,大楼都会出问题。
2.3 适用场景与架构前提
这套组合最适用的场景是容器化部署,尤其是 Kubernetes。因为 Kubernetes 本身提供了 livenessProbe、readinessProbe、startupProbe、ResourceQuota、LimitRange、HPA、VPA、ClusterAutoscaler 这一整套原语,不需要你额外造轮子。即使你不是 K8s 用户,只要你的平台支持健康检查、资源限制和自动扩容,比如 Spring Cloud 结合 Nacos + Sentinel + 云负载均衡,同样可以套用这套思路,只不过“探针”变成“健康检查接口”,“资源配额”变成“容器 or 进程的内存限制”,“弹性伸缩”变成“云主机或容器实例的自动伸缩组”。
但有一点要提前讲清楚:这套组合不解决代码本身的性能问题。如果接口单次查询要扫全表慢查询拖死数据库,那探针和伸缩只能延缓崩溃,不能阻止崩溃。所以在引入三板斧之前,最好先做好基础的可观测性,至少把 CPU、内存、GC、QPS、P95 耗时这些指标采集起来,否则后面排查问题会非常痛苦。
3. 第一板斧:探针——让系统自己报告“我还没死”
3.1 三类探针:存活、就绪、启动,千万别用错
Kubernetes 里最常用的健康检查机制就是探针,它在 Pod 生命周期里扮演三种角色。
存活探针(livenessProbe)用来回答“这个容器还活着吗”。如果探测失败,Kubelet 会按配置杀掉容器并重启。典型场景是 Java 服务线程池被打满,进程还在,但已经无法处理任何请求,此时存活探针失败可以触发重启,让服务重新恢复。
就绪探针(readinessProbe)用来回答“这个容器能接收流量吗”。探测失败时,容器不会被杀死,但对应的 Endpoint 会从 Service 的负载均衡列表中移除。典型场景是服务启动后需要加载缓存或连接数据库,在就绪之前不应该让流量进来。就绪探针是发布、扩容过程中最重要的保护措施。
启动探针(startupProbe)用来回答“这个应用启动完成了吗”。它专门给启动慢的应用准备的,因为存活探针在容器启动后就开始探测,如果应用启动时间超过存活探针的失败阈值,就会被误杀。配置了启动探针之后,在启动探针成功之前,存活探针和就绪探针都不会生效。启动探针通常只执行一次,成功之后就会停止工作。
我的建议是:每个服务至少配置就绪探针和存活探针。如果你服务启动超过 10 秒,就一定要加启动探针。这三者你要当成不同的逻辑去配置,不要图省事用同一个接口。
3.2 探针的配置参数怎么调:这些经验值直接抄
先看一个完整配置示例:
yaml复制livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
successThreshold: 1
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
successThreshold: 1
startupProbe:
httpGet:
path: /actuator/health/startup
port: 8080
initialDelaySeconds: 0
failureThreshold: 30
periodSeconds: 5
参数含义我就不一一念文档了,只说核心调参逻辑。
- initialDelaySeconds:容器启动后到第一次探测的等待时间。我习惯设置为“应用预计完成初始化时间再加 5 秒”。如果你不确定,先设 15 秒,通过看日志里“应用 ready”的时间再调整。
- periodSeconds:探测频率。就绪探针建议设 5 秒,因为发布和扩容场景需要快速感知实例就绪;存活探针建议设 10 秒;启动探针可以设 5 秒,配合较大的 failureThreshold。
- timeoutSeconds:单次探测超时。HTTP 探针默认 1 秒,比较激进,我建议 2 秒,给 GC 停顿留点余地。
- failureThreshold:连续失败多少次才算失败。存活探针建议 3 次,避免偶发 GC 或网络抖动导致容器被杀。就绪探针建议 2 次,因为摘流量快一点没坏处。
- successThreshold:连续成功几次才算成功。一般 1 次就够了,设大了反而会延迟就绪时间。
可以拿这几个参数做一张表贴到团队 wiki 里,方便新人直接参考。
按我经验,最容易被坑的是把存活探针和就绪探针指向同一个接口。比如你的健康检查接口本身会连数据库,一旦数据库抖动,两个探针同时失败,结果就是容器被反复重启,流量全断。正确做法是:存活探针应该指向一个“只检测进程与核心线程池状态”的轻量接口;就绪探针才可以包含依赖组件的状态,比如数据库连接、Redis 连接、消息队列可用性等。Spring Boot 的 actuator 其实已经帮你拆好了 /health/liveness 和 /health/readiness,直接用就行。
3.3 探测方式怎么选:HTTP、TCP、Exec 的对比
探针支持三种方式:httpGet、tcpSocket、exec。
httpGet 最常用,它真的会发一个 HTTP 请求到容器内指定端口和路径,只要返回 200~399 就认为成功。这种方式最贴近真实请求链路,能检测 Web 容器的可用性。缺点是如果健康检查接口逻辑太重(比如每次查询数据库),会放大后端压力。
tcpSocket 只检查端口是否能建立 TCP 连接,相当于“端口通没通”。它很轻量,但无法区分“端口通但应用卡死”的情况。比如 Java 进程 accept 了连接但业务线程全部阻塞,TCP 探测依然成功。所以我一般只在不需要精细判断的基础组件(比如 Redis、MySQL 自建镜像)上用它。
exec 是在容器内执行一条命令,根据退出码判断。它最灵活,但要求容器内必须有可用的命令环境,而且 exec 每次都会创建进程,开销相对高。我见过有人用它执行 pgrep java 来判断 Java 进程是否存在,这其实不如 HTTP 探针精确,因为进程存在不等于服务可用。
结论:Web 服务优先用 HTTP,数据库等基础组件可以用 TCP,实在没有 HTTP 端口的情况下再用 exec。
3.4 实战:为 Spring Boot 服务配置探针
假设我们有一个标准的 Spring Boot 2.x 服务,已经引入了 actuator。那么配置非常简单:
yaml复制spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
probes:
enabled: true
group:
readiness:
include: db, redis, mq
liveness:
include: ping
这样 /actuator/health/liveness 只会检查应用自身状态(相当于 ping),而 /actuator/health/readiness 会检查 DB、Redis、MQ 等依赖组件的连接情况。然后在 Deployment 里配置探针即可。
有一个细节:容器内不能有多个进程,否则存活探针会变成无意义检查。建议一个容器只跑一个 Java 进程、一个 Nginx 进程,保持容器的“单进程主流程”。
4. 第二板斧:资源配额——把“内存泄漏”关进笼子
4.1 requests 和 limits:一个是“预定”,一个是“上限”
很多初学 Kubernetes 的人分不清 requests 和 limits,我先用一句话解释:requests 是你告诉调度器“我至少要这么多资源”;limits 是你告诉内核“我最多只能用这么多资源”。
调度器根据 requests 来决定把 Pod 放在哪个节点上。如果一个节点剩余可分配资源小于 Pod 请求的 requests,这个 Pod 就不会被调度到该节点。如果所有节点都放不下,Pod 会一直 Pending,不会启动。所以 requests 的设置直接决定了扩容“能不能成功”。
limits 是限制运行时资源使用的硬上限。对于 CPU,超过 limit 时容器会被限流(throttle),而不是被杀;对于内存,超过 limit 时可能会触发 OOMKilled,容器被杀掉重启。内存没有像 CPU 那样的“限流”机制,因为内存不是可分片让渡的资源,超了只能杀。
我见过一个典型事故:Java 服务 requests.memory 设置 512Mi,limits.memory 设置 512Mi,结果 JVM 堆申请了 1G,容器直接被 OOMKilled。原因就是没给 JVM 设置合适的堆大小,导致容器内存超限。所以配置 Java 容器资源时,一定要同时调整 JVM 的 -Xmx 参数,让 JVM 最大堆加上 Metaspace、线程栈等开销总和,不要超过 limits。
4.2 ResourceQuota 和 LimitRange:命名空间级的双保险
单靠每个 Deployment 里的 resources 配置,还是挡不住有人漏配。ResourceQuota 可以限制一个命名空间内的资源总量,比如下面这样:
yaml复制apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "10"
requests.memory: 16Gi
limits.cpu: "20"
limits.memory: 32Gi
pods: "50"
services: "20"
configmaps: "20"
一旦这个配额生效,在 dev 命名空间里创建任何 Pod、Service、ConfigMap 时,Kubelet 或 API Server 都会检查总和是否超过配额;超过就拒绝创建。这能有效避免某个业务线疯狂创建资源把整个集群拖垮。
LimitRange 则像一个“模板”,给命名空间内每个 Pod 或容器设置默认的 requests、limits 和可设置范围。比如:
yaml复制apiVersion: v1
kind: LimitRange
metadata:
name: default-limit-range
namespace: dev
spec:
limits:
- max:
cpu: "4"
memory: 8Gi
min:
cpu: "50m"
memory: 64Mi
default:
cpu: "200m"
memory: 512Mi
defaultRequest:
cpu: "100m"
memory: 256Mi
type: Container
这样即使开发者创建 Deployment 时没有写 resources,系统也会自动填入默认值。如果写了但超出 max/min 范围,创建也会被拒绝。这相当于给“人工失误”上了两道保险,强烈建议每个命名空间都配置。
4.3 CPU 和内存的单位换算,别让 100m 吓到
Kubernetes 里 CPU 的单位是“核”,但通常用 millicores(m)表示。1000m=1 核。100m 就代表 0.1 核。内存单位则用 Mi、Gi,分别对应 1024 的换算。注意,Mi 是 10241024 字节,而 M 是 10001000 字节,容易混淆。配置里建议统一用 Mi/Gi,避免歧义。
一个常见的坑是:requests.cpu 设置 250m,limit.cpu 也设置 250m,但实际服务是单线程密集计算型,结果 CPU 被限流,看起来 CPU 使用率只有 25%,但请求延迟非常高。原因就是 250m 只允许它用四分之一核的 CPU 时间。所以 requests 不要拍脑袋,最好用压测数据估算:先用基准环境跑一轮,看峰值 CPU 用多少,再留 30% 的余量。
内存方面也是一样。对于 Java 应用,建议 JVM 堆大小设为容器内存 limit 的 60%-70%,额外留着堆外内存和线程栈的空间,别把容器内存挤满。
4.4 一个完整的资源配额配置示例
结合前面内容,给大家一个可以复制的组合示例:
yaml复制apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "20"
requests.memory: 32Gi
limits.cpu: "40"
limits.memory: 64Gi
persistentvolumeclaims: "10"
pods: "100"
---
apiVersion: v1
kind: LimitRange
metadata:
name: production-limit-range
namespace: production
spec:
limits:
- type: Container
min:
cpu: 50m
memory: 128Mi
max:
cpu: "8"
memory: 16Gi
default:
cpu: 500m
memory: 1Gi
defaultRequest:
cpu: 250m
memory: 512Mi
在这个配置下,生产命名空间里的 Pod 如果没有显式声明 resources,就会自动获得 CPU 500m/内存 1Gi 的 limit,以及 CPU 250m/内存 512Mi 的 request。如果开发者写了超规格的资源(比如内存 32Gi),创建会被拒绝。你可以根据业务规模调整数字,但这套“总量配额 + 默认模板”的搭配方式适合绝大多数团队。
4.5 limits 设置不当会怎样:OOMKilled 和 CPU Throttle
内存 limits 设置过低,最常见的现象是 Pod 状态变成 OOMKilled,然后容器反复重启。排查方式很简单:kubectl describe pod xxx,看最后的事件里有没有“Killing”和“OOMKilled”字样;再用 kubectl logs 看进程崩溃前的 GC 日志,判断是堆不够还是堆外内存涨了。
CPU limits 设置过低,现象更隐蔽。容器不会重启,但 CPU 会被强制节流,表现是 CPU 使用率看起来没有很高,接口延迟却持续上升。Linux CFS 调度器用 quota 机制做 CPU 限制,超限后进程只能等待下一个周期再运行,这就是 throttle。排查方式是在容器内执行 cat /sys/fs/cgroup/cpu/cpu.stat,看 nr_throttled 是否持续增长。如果增长明显,就说明 CPU limit 需要提高,或者用 HPA 扩容把负载分散开。
我个人的建议是:requests 应该接近服务的真实负载底线,limits 可以比 requests 高 30%-50% 左右。不要把 limits 设置成无限大,否则资源配额就失去了意义。
5. 第三板斧:弹性伸缩——用最低成本扛住流量尖峰
5.1 HPA 的扩容公式,搞懂它才能不慌
HPA(HorizontalPodAutoscaler)是 Kubernetes 内置的水平弹性伸缩控制器。它的核心逻辑是周期性地获取 Pod 的监控指标,然后与目标值比较,计算出期望的副本数。
计算公式是:期望副本数 = ceil(当前副本数 × (当前指标值 / 目标指标值))。
举个例子:当前有 4 个副本,每个 Pod 的 CPU 使用率是 80%,目标使用率设置为 50%,那么期望副本数 = ceil(4 × (80/50)) = ceil(6.4) = 7。HPA 就会把副本数扩展到 7 个。当指标降下来之后,它会逐渐缩容,但不会立刻缩,因为有过期时间和稳定窗口。
HPA 能工作的前提是:Pod 必须配置了 requests(尤其是 CPU requests),因为 HPA 计算的是“当前 CPU 实际使用量 / requests 值”,而不是限制 values。如果你没有配置 requests,HPA 无法获取指标,也就无法扩容。我以前就踩过这坑,Deployment 里忘了写 resources,HPA 状态一直显示 Unable to fetch metrics。
5.2 HPA 配置示例与参数解读
一个最基本的 HPA 配置如下:
yaml复制apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 60
参数说明:
- minReplicas:最小副本数,保证基础容量。
- maxReplicas:最大副本数,防止无限扩容把账号打穷。
- metrics:可以配置多组指标,HPA 会分别计算期望副本数,然后取其中的最大值。CPU 用平均值,内存也用平均值。需要额外说明的是,内存指标不太好用,因为内存不会像 CPU 那样自动释放,HPA 扩容后内存可能仍然居高不下,缩容又会导致部分副本内存压力更大。所以大部分场景建议只用 CPU 或者自定义 QPS 指标。
- behavior.scaleDown.stabilizationWindowSeconds:缩容稳定窗口,默认 300 秒,意思是缩容会延迟 5 分钟执行,避免因瞬时波动频繁缩容。
- behavior.scaleUp.policies:扩容策略,我习惯设置为一次最多增加 100% 的副本数。意思是如果当前有 4 个副本,最多一次扩到 8 个,避免扩容过快打爆下游依赖。
- behavior.scaleDown.policies:缩容策略,设置为 20%,一分钟最多缩掉 20%,避免突然减少太多副本导致容量不足。
关于 HPA 的另一个关键点是:它依赖 metrics-server 采集指标。Kubernetes 集群如果没有安装 metrics-server,HPA 完全无法工作。检查方法:kubectl top nodes 和 kubectl top pods,如果报 error,就是 metrics-server 没有装好。
5.3 VPA 和 Cluster Autoscaler:要不要一起上
HPA 负责水平扩容,也就是增加/减少 Pod 副本数。但对于一些不适合水平扩容的有状态服务,或者某个 Pod 实例因为业务特性无法拆分,水平扩容无效。这时候可以考虑 VPA(Vertical Pod Autoscaler),它会自动调整 Pod 的 requests 和 limits,让 Pod 获得更多的资源。VPA 的典型用法是给数据库、消息队列这类有状态服务调整规格。但 VPA 的升级模式通常需要重启 Pod,所以在生产上要谨慎,建议搭配 OPA(开放在线升级)机制来做。
Cluster Autoscaler 则是集群层级的伸缩,它会在节点资源不足时,自动为云厂商申请新的节点加入集群;在节点空闲时,自动释放节点。如果你用的是公有云托管 K8s,比如 EKS、ACK、TKE 这些,通常有现成的节点伸缩能力。建议把节点池的最小/最大实例数设置好,否则 HPA 想扩容但节点没有空间,Pod 会一直 Pending。
我的建议是:初期不要一上来就上全套。先把 HPA 跑熟,再把节点伸缩打开,最后根据实际情况评估要不要引入 VPA。工具太多,运维成本也是指数级上升的。
5.4 弹性伸缩的“抖动”问题,我见过太多人吃亏
HPA 最常见的坑是“抖动”。比如某服务 CPU 在 55% 和 65% 之间反复横跳,目标值是 60%,结果 HPA 一会儿扩容、一会儿缩容,Pod 频繁创建销毁。这会导致发布不稳定,也会增加后端数据库连接压力。
解决抖动有两个手段。一是设置合理的 stabilizationWindowSeconds,缩容窗口拉长到 300 秒以上,扩容窗口可以设置为 0,因为扩容及时是好事。二是设置 scaleUp 和 scaleDown 的速率限制,比如上面示例里每次最多扩 100%、缩 20%,就算指标波动,副本数变化也不会太剧烈。
还有一个细节:HPA 的指标聚合周期默认是 15 秒,但 metrics-server 采集周期通常是 15 秒到 30 秒,所以从指标变化到扩容生效,一般有 1 到 2 分钟的延迟。对于突发性非常强的流量,比如秒杀,HPA 可能来不及。这时候要靠“提前扩容”:用 Cron 定时把副本数弹上去,或者基于业务流量预估手动扩容。更高级的方案是基于队列长度、QPS 的自定义指标做 HPA,这需要暴露监控系统指标给 API Server。我建议在流量模型比较稳定之后再折腾这些,否则容易过度设计。
6. 协同作战:从流量冲击到自动恢复的完整链路
6.1 一次流量高峰,三板斧到底是怎么配合的
把三个部分串起来,我带你完整走一遍流量高峰时的系统反应。
假设凌晨 2 点流量开始涨。第一批流量打到 Service 上,Service 的 Endpoint 列表里只有 3 个 Pod,且因为就绪探针正常,三个 Pod 都在承受流量。此时每个 Pod 的 CPU 使用率迅速上升,容器 CPU limit 开始起作用——注意,CPU limit 不是限制使用率不超过 100%,而是限制它最多只能用多核中的一部分时间。当 CPU 超过 requests 的 60% 时,metrics-server 采集到了 CPU 指标,HPA 计算出需要扩容到 5 个副本,于是 Deployment 开始创建新的 Pod。
新 Pod 启动后,启动探针开始探测,应用加载 Spring 上下文、连接数据库和 Redis,可能需要 20 秒。此时就绪探针失败,Service 不会把流量分给新 Pod。等到启动探针成功、就绪探针也通过后,新 Pod 被加入 Endpoint,开始分担流量。如果流量继续涨,HPA 再扩容。
在这个过程中,如果有某个 Pod 因为代码 bug 导致内存泄漏,内存超过 limits,它会被 OOMKilled,然后在几秒内被重新创建。如果进程没死但线程池被打满,存活探针探测失败,触发容器重启。如果整个节点的内存已经不够分配新的 Pod,Node 上的资源配额会阻止调度,这时候 Cluster Autoscaler 会自动申请新节点。所有动作都不需要人手工干预,这就是“永不宕机”的底气。
6.2 探针、配额、伸缩之间的隐含联动关系
这三者之间有几个容易被忽视的联动点,我用实际经验提醒一下。
第一,探针失败和 HPA 扩容可能互相掩盖问题。假如某个副本因为局部故障导致 CPU 使用率飙升,HPA 会以为需要扩容,副本数增加后故障副本反而被探针杀死重启,指标又下降,于是 HPA 开始缩容。这个循环会造成“扩容—重启—缩容”的震荡。解决办法是,探针的健康检查接口不应该让 CPU 权重太高,同时 HPA 的稳定窗口要够大,别让单点故障影响整体副本数。
第二,资源配额的 limit 会影响探针的存活概率。如果 memory limit 设得过低,应用频繁 OOM,容器不断重启,这时候存活探针可能还没来得及探测,容器就被杀了。建议内存 limit 至少比 JVM 最大堆高 25%,给元数据、线程栈、直接内存留空间。
第三,HPA 扩容时,新 Pod 的资源 requests 会立刻加入命名空间配额。如果命名空间的 ResourceQuota 已经用满了,新 Pod 创建会被拒绝,HPA 会一直报 Hit the max pods limit or resource quota。所以你在设置 HPA maxReplicas 的同时,一定要计算好命名空间配额是否足够支持扩容后的资源总和。比如每个副本要 1Gi 内存,maxReplicas 是 20,那么至少预留 20Gi 的内存配额。
6.3 一个教科书级的组合配置(Deployment + HPA + ResourceQuota)
我这里给你一份可以“抄作业”的完整配置。假设我们有一个 nginx 服务,命名空间 production。
首先定义 Deployment:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
namespace: production
labels:
app: web-server
spec:
replicas: 3
selector:
matchLabels:
app: web-server
template:
metadata:
labels:
app: web-server
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1
memory: 512Mi
startupProbe:
httpGet:
path: /healthz
port: 80
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
然后定义 HPA:
yaml复制apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-server-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-server
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
behavior:
scaleDown:
stabilizationWindowSeconds: 300
scaleUp:
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 60
别忘了命名空间配额:
yaml复制apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "20"
requests.memory: 32Gi
limits.cpu: "40"
limits.memory: 48Gi
pods: "30"
这套配置放到一起,就是一个小型高可用系统的雏形。首先,nginx 的 Pod 启动时会先通过启动探针等待;就绪后进入 Service;CPU 使用率超过 50% 时 HPA 自动扩容;任何时刻 Pod 内存不能超过 512Mi,否则被 OOMKilled 重启;整个 production 命名空间最多 30 个 Pod,资源总和不能超过配额。三个机制层层嵌套。
6.4 如何验证这套机制真的有效:压测与故障演练
配置写完不压测,你心里永远没底。我建议每个月做一次小规模故障演练。
压测工具可以用 wrk、vegeta、locust 或者云上压测平台。先用固定 QPS 压 5 分钟,观察 HPA 是否自动扩容;再模拟故障,删除几个 Pod,看探针能不能快速拉起新 Pod;还可以用 tc 命令模拟网络延迟,看就绪探针是否会正确摘掉故障实例。注意压测不要直接压生产,最好在预发或灰度环境做,而且要提前和业务方对齐时间窗口。
具体演练步骤可以这样:先在测试环境部署一套同样配置的服务,打十分钟 500 QPS 的流量,观察 CPU 指标、HPA 副本数和 P99 耗时。如果没有扩到预期副本数,检查 metrics-server 是否正常、Pod 是否有 requests。然后模拟内存泄漏,用一个故意泄漏内存的版本,观察 Pod 是否被 OOMKilled 并自动重启。最后模拟资源配额打满,在一个命名空间里创建大量资源,确认 ResourceQuota 的拒绝行为是否符合预期。做完整套演练,你才敢跟老板说“这个系统不会宕机”。
7. 常见问题与排查技巧实录
7.1 实战中遇到的典型问题速查表
下面这个表格,是我把过去踩过的坑整理出来的,希望能帮你少走弯路。
| 现象 | 可能原因 | 排查命令/方法 | 解决方向 |
|---|---|---|---|
| Pod 一直 Pending | 节点资源不足 / ResourceQuota 打满 | kubectl describe pod | 查看节点可分配资源与命名空间配额,扩容节点或调低 requests |
| Pod 反复 CrashLoopBackOff | 存活探针失败 / OOMKilled | kubectl describe pod,kubectl logs | 调大内存 limit 或调整 JVM 堆;检查探针接口是否太依赖外部组件 |
| 服务没有流量但 Pod 正常 | 就绪探针失败,Endpoint 中无实例 | kubectl get endpoints | 查看探针健康检查接口返回码与日志,修复依赖组件 |
| HPA 不扩容 | 未配置 requests / metrics-server 未装 | kubectl top pods,kubectl get hpa -o yaml | 补上 resources.requests;安装 metrics-server |
| HPA 频繁扩缩容 | 稳定窗口太短 / 指标波动大 | kubectl get hpa -w,看历史曲线 | 增大 scaleDown.stabilizationWindowSeconds,加限速策略 |
| 命名空间创建 Pod 被拒 | ResourceQuota 超出硬限制 | kubectl get resourcequota -n ns | 清理无用资源或申请调整配额上限 |
| 服务启动慢但被误杀 | 没有启动探针 | kubectl describe pod 中 Last State 为 OOMKilled 或 Error | 配置 startupProbe,延迟存活探针生效 |
| 内存持续增长直到被杀 | 应用内存泄漏 / limit 偏小 | 看 GC 日志、heap dump | 修复泄漏;结合 HPA 扩容或提升单副本内存上限 |
7.2 排查工具和心法
排查三板斧问题,我常用的命令是这几个:
bash复制# 查看 Pod 状态和最近事件
kubectl describe pod <pod-name> -n <namespace>
# 查看 HPA 当前指标和事件
kubectl get hpa -n <namespace>
kubectl describe hpa <hpa-name> -n <namespace>
# 查看节点资源分配
kubectl describe node <node-name>
# 查看命名空间资源配额使用情况
kubectl get resourcequota -n <namespace> -o yaml
# 进入容器查看 cgroup 限制和 CPU 节流
kubectl exec -it <pod-name> -n <namespace> -- cat /sys/fs/cgroup/cpu/cpu.stat
# 查看 metrics-server 是否正常
kubectl get --raw /apis/metrics.k8s.io/v1beta1/pods | head
排查顺序我建议是:先看 Pod 状态和事件,定位是探针导致重启还是配额导致 Pending;再看 HPA 的 events 和 metrics,确认扩容逻辑是否生效;最后看命名空间资源使用,确认配额是否会成为瓶颈。不要一上来就翻日志,先用这些元数据缩小范围。
有一个心法要记住:一切自动恢复机制都可能成为“故障放大器”。比如节点网络抖动,导致全部 Pod 的就绪探针失败,Service 的 Endpoint 变成空,HPA 可能会尝试扩容更多 Pod,新 Pod 又因为同样的网络问题无法就绪,最终把集群资源耗尽。遇到这类大面积故障,先切维护模式、关闭 HPA 或临时调整探针阈值,恢复后再分析原因。
7.3 独家避坑技巧:三板斧上线前,一定要检查这五点
经过多次线上事故之后,我总结出几个“上线前必查项”,分享给你。
第一,检查所有 CPU 密集型的服务是否设置了合适的 requests。没有 requests 的 Pod 不仅 HPA 算不了指标,调度器也无法感知资源压力,容易把一个节点塞满。
第二,检查健康检查接口是否包含了过多的依赖。我见过团队把数据库连通性检查放到 livenessProbe 里,导致数据库一慢,所有实例同时重启,形成雪崩。正确做法是 liveness 只检查“进程活着”,readiness 才检查依赖可用性。
第三,检查 ResourceQuota 的 pods 数量是否够 HPA 扩容。很多人只限制了 CPU 和内存,不限制 Pod 数量,结果业务方把副本数调到 500,把集群打爆。反过来,如果你设了 pods: 30,但 HPA maxReplicas 是 50,那么当副本数达到 30 后扩容就会失败,所以这两个数字要保持逻辑一致。
第四,检查 limits 和 requests 的比例是否合理。对大多数 Web 服务,limits 可以是 requests 的 2~4 倍;但如果你的服务内存模型非常敏感,比如 Java 堆和容器内存必须精确匹配,那就得通过压测来校准,不能随便给个“倍率”。
第五,检查探针和优雅终止的配合。Kubernetes 停止 Pod 时,先发送 SIGTERM,然后等待 terminationGracePeriodSeconds,默认 30 秒。如果应用能处理 SIGTERM 并优雅退出,那么终止时才不会造成大量请求失败。建议探针的 failureThreshold/periodSeconds 计算出来的“判定失败时间”要明显短于 terminationGracePeriodSeconds,避免应用还在关停过程,探针却已经开始报失败。
8. 写在最后:三板斧不是终点,自动化才是
这套三板斧组合上线后,我们线上的故障工单量至少降了一半,很多以前需要凌晨爬起来处理的“假死”问题,现在系统自己就恢复了。但我个人的体会是,工具只能保证“当坏情况发生时,系统有兜底”,真正有价值的还是对业务负载和资源模型的深入理解。比如,你到底应该把 HPA 目标 CPU 设成 50% 还是 70%?这取决于你业务的突发节奏和下游依赖的承受能力。再比如,你的服务启动要 30 秒还是一分钟?这直接影响启动探针的 failureThreshold 配置。这些参数没有标准答案,只能靠一次次的压测和故障复盘去校准。
最后分享一个小技巧:在把三板斧这套东西推广到团队时,别只写文档发 URL,我建议你组织一次“容灾演练 Hackerthon”,让每个业务线自己配置探针、配额和 HPA,互相压测对方的服务。只有亲手把服务打死一次,又亲眼看到它自动活过来,大家才会真正信服这套机制,也才不会在遇到问题时随手把探针给关掉。这套机制说到底,是为了让你在深更半夜安心睡觉,而不是为了让你在告警群里表演手速。配置越多,越要守住“简单、可预测、可观测”这三条原则。
