很多运维老哥看到“永不停机”这几个字,第一反应都是“扯淡”。我做过几年一线运维,也带过团队,现在回头看,这个词确实有标题党嫌疑,但背后的思路是实打实的。没有任何系统能绝对保证不宕机,但通过合理的架构和平台能力,我们可以把宕机时间压缩到趋近于零,让业务在故障面前“感受不到疼”。今天想聊的,就是我在生产环境里反复验证过的一套组合拳:探针、资源配额和弹性伸缩。这三样东西拆开看都不新鲜,但把它们串成一个整体去协同作战,才是真正的关键,这也是我踩了不少坑之后换来的经验。
进入正文之前,先明确一下这套东西适用的场景。它最适合微服务架构、容器化部署(比如Kubernetes环境)、以及已经有一定流量波动或者预期流量会快速增长的在线业务。如果你的服务还是个单体应用,跑在一台虚拟机上,那探针和弹性伸缩也能做,但效果会打折扣,资源配额倒是随时都能用。这篇文章我会从设计思路、具体配置、实操验证到问题排查,完整过一遍,里面用到的配置片段都是我在真实环境中验证过的,你可以直接参考。
1. 永不宕机的核心思路:从“被动救火”到“主动防御”
在聊具体技术之前,得先把思路掰扯清楚。很多团队一开始对“高可用”的理解就是“多买几台机器”、“搞个负载均衡”,这属于典型的“被动救火”思维——流量大了加机器,机器挂了重新拉起来。这种思路不能说错,但天花板很低。原因很简单:你永远在等故障发生,然后再去响应。就算响应速度再快,用户已经感知到了一次闪断。
主动防御的核心在于,把故障前置处理掉。我们不去赌某个节点会不会挂,而是假设它一定会挂,并且在这个假设下,让整个系统依然可以对外提供服务。这就要求平台具备三个基本能力:发现问题、限制故障范围、自动恢复容量。这三件事,正好对应了探针、资源配额和弹性伸缩这三个工具。
打个比方,你就把这个集群当成一个酒店。探针是酒店的服务员,随时检查每个房间(Pod或容器)里的客人是不是安好、有没有什么需求,一旦发现客人晕倒或者房间漏水了,马上通知前台清理换房。资源配额是酒店的消防隔离门,每个区域(命名空间或应用)有多少座位、多少逃生通道,提前规划好,某个区域着火了不会烧到隔壁去。弹性伸缩则是酒店的临时加床机制,节假日入住率高了,马上让加床房上线;过了高峰期,再撤掉加床恢复原样。三者协同起来,酒店才能在人满为患的时候依然不拒客、不失控。
那么在实际落地时,这三者需要一个统一调度的大脑。在Kubernetes里,这个大脑就是kubelet、kube-scheduler和controller-manager这几个组件的配合。探针负责采集健康状态,Controller(比如Deployment)负责维持期望的副本数,HPA(Horizontal Pod Autoscaler)负责根据指标动态调整副本数,而资源配额则由ResourceQuota和LimitRange来管控。把这些组件调教好,你就拥有了一套不需要24小时盯着屏幕的“自动驾驶”运维系统。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 探针:业务的健康守门员
2.1 三种探针的使用场景与配置解析
探针(Probe)是Kubernetes里很基础也很有分量的能力。很多新手只知道“探针能检测容器是不是活着”,但到底哪三种探针、各管什么,常常分不清楚。我先把结论放在这里,然后逐个解释。
-
LivenessProbe(存活探针):回答“容器还活着吗”的问题。如果探测失败,kubelet会根据配置杀掉容器并重新创建。它解决的是容器内进程卡死、死锁、内存溢出导致无法处理请求但又没有退出这类问题。
-
ReadinessProbe(就绪探针):回答“容器能不能接收流量”的问题。如果探测失败,Kubernetes会把这Pod从Service的Endpoints里摘掉,流量不会再打过来,但Pod不会被杀掉。它解决的是应用正在启动、正在加载大量缓存、依赖的下游服务暂时不可用等情况。
-
StartupProbe(启动探针):回答“应用启动完成了吗”的问题。它专门解决启动特别慢的容器的问题。在StartupProbe成功之前,Liveness和Readiness都不会生效。对于Java应用(特别是启动时要连数据库、拉配置中心的应用),这个探针非常实用。
三个探针之间的关系,我用一个容易理解的方式总结一下。StartupProbe是“安检通道”,过了安检你才算正式进入候机厅;ReadinessProbe是“登机口状态”,告诉乘客能不能上飞机;LivenessProbe是“飞行途中乘客是否清醒”,不清醒就强制返航重飞。如果StartupProbe不配置,那么LivenessProbe会在容器启动后立刻开始计时,碰到启动耗时超过initialDelaySeconds + timeoutSeconds的场景,就会发生“应用还在启动,就被误杀”的恶性循环。
2.2 探针参数调优:别用默认值当生产配置
探针的参数看似简单,实际调优空间很大。直接抄默认值通常会埋坑,我把自己常用的参数贴出来,并解释每个参数的取舍。
yaml复制livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: Custom-Header
value: liveness-probe
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
successThreshold: 1
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
successThreshold: 1
failureThreshold: 3
startupProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
successThreshold: 1
failureThreshold: 30
这里有几个细节值得细品。第一,initialDelaySeconds不宜设置过大,但也不能过小。太大的话,Pod其实已经就绪了,但探针还没开始工作,白白浪费了扩容的时间;太小的话,应用进程还没完成端口监听,第一次探测就会失败。我见过团队把initialDelaySeconds设为60秒,问原因竟然是“怕应用启动慢,所以多等一会儿”。这个想法是好的,但更优雅的方案是把启动慢的问题交给StartupProbe去解决,Liveness的initialDelaySeconds就可以设得很小,这样一旦应用启动完成,快速进入健康检查状态。
第二,periodSeconds和failureThreshold要放在一起权衡。假设periodSeconds=10,failureThreshold=3,那么最快要20秒才能判定探针失败(第一次失败不算,连续3次失败后才触发)。如果是一个时延敏感的业务,这20秒的“反应时间”就太长了。我之前调过一版,把periodSeconds降到了2秒,failureThreshold保持3,虽然探头对CPU的消耗略有增加(这个量级其实很小),但故障Pod的摘除时间缩短到了4秒左右,用户几乎无感知。至于timeoutSeconds,我建议设成和periodSeconds相等甚至略小,并且确保你的健康检查接口在极端情况下也能在200ms内返回,否则会出现接口慢导致探针超时误判的问题。
第三,探针接口要轻量。千万别在健康检查的接口里去查数据库、查下游依赖,这会引来灾难。想象一下,数据库慢查询导致健康接口返回超过2秒,探针判定失败,服务被摘流量,但这时候数据库压力并没有减少,其他正常的Pod依然在查询,整体雪崩。健康检查接口应该只检查本进程内最基本的状态,比如端口是否能连上、关键线程池队列是否满、本地缓存是否初始化完成。下游依赖的状态,应该交给业务自己的监控系统去判断,而不是让K8s的探针来做。
2.3 探针方案选型:HTTP、TCP还是Exec
探针协议的选择也是个容易忽略的点。大部分场景用HTTP是最合理的,因为它能借助HTTP状态码表达更丰富的语义。比如返回200表示健康,返回503表示“我现在不想接收流量,但还没死”。这种方式比TCP协议简单粗暴的“端口通不通”要精细得多。TCP探针只适合那种没有HTTP接口的中间件容器,比如Redis、Kafka,或者说你的应用实在没法暴露HTTP端口。
Exec探针(执行命令)是最灵活但也最危险的。灵活在于你可以在容器内执行任意命令来判定健康状态,比如检查某个文件是否存在、某个进程是否在跑。危险在于,如果命令本身没有超时控制、没有考虑资源占用,它可能会拖死容器。我踩过一次坑:一个Sidecar容器用Exec探针去执行一个复杂的Python脚本来检查配置一致性,脚本里有一行正则匹配大文件,结果periodSeconds一到,脚本还没跑完,探针挂了,容器反复重启。所以如果要用Exec探针,务必加timeout,并且命令复杂度控制在毫秒级。
个人推荐一个折中方案:应用暴露/healthz接口,内部用异步方式聚合关键的本地状态(磁盘、内存、线程池),主线程只做状态读取和200/503返回,所有数据库检查通过goroutine异步更新状态,不在请求链路里同步执行。这样既保证探针快速响应,也能及时发现组件内部的问题。
3. 资源配额:把故障关进笼子里
3.1 为什么说资源配额是稳定性的基石
很多团队在追求“高可用”的时候,目光只盯着副本数、负载均衡和故障转移,却忽略了资源配额的重要性。这非常可惜,因为如果没有资源配额,弹性伸缩就是一头脱缰的野马。流量一涨,HPA拼命扩容,Pod越来越多,每个Pod都在抢占CPU和内存,最终所有Pod一起变慢,甚至触发节点OOM,整个集群陷入雪崩。By the time你反应过来,已经晚了。
资源配额从两个层面来限制这种风险。第一个层面是容器级别的requests和limits,这是最基础但也最容易被忽略的。requests是调度时的“预约量”,limits是运行时“上限”。很多开发在写Deployment时不设置requests,或者把requests和limits写成一样大。前者会导致调度器以为容器很轻,一台机器上塞了大量Pod,结果运行起来发现CPU和内存根本不够;后者则会浪费资源,尤其CPU,因为CPU是可压缩资源,设置了过高的limit反而限制了突发能力。
第二个层面是命名空间级别的ResourceQuota和LimitRange。ResourceQuota限制整个命名空间的资源总量,比如这个命名空间最多能用10核CPU、20Gi内存。LimitRange则给命名空间内的Pod设置默认requests和limits,并限制单Pod最大最小规格。这两个对象配合起来,可以非常精细地管理容量边界。
打个现实中的比方。资源配额就像飞机上的行李舱,每位乘客的行李标准是固定的(requests),超重了要加钱或者拒运(limits);整个货舱也有总容量限制(ResourceQuota),不可能让所有乘客都带超重行李,否则飞机根本飞不起来。弹性伸缩是“临时加开航班”,但如果机场跑道就这么宽(节点资源总量),加开太多航班反而会造成拥堵。
3.2 配额配置实操:request/limit的黄金法则
对于requests和limits的取值,每个团队都有自己的经验。我在多个项目里总结出来一套相对稳妥的配置逻辑。
- CPU requests:按业务高峰期的平均使用量来设定,留出20%-30%的余量,用于应对突发流量。不要用P99峰值去设定requests,那会导致调度过于保守,资源利用率低。
- CPU limits:建议设置为requests的1.2到1.5倍。由于CPU是可压缩资源,短时间内可以允许Pod突增到limit,但长时间超过limit会被内核降频(CPU Throttling)。如果业务对延迟极其敏感,可以不打too大。理想的CPU limits是让业务能享受到1.5倍瞬时加速,又不至于被长期限流。
- Memory requests:必须设置为接近稳定状态的内存使用量,最好包含JVM堆外内存、直接内存、线程栈等开销。内存是不可压缩资源,一旦超过limit就会触发OOMKill,所以memory requests必须保守。
- Memory limits:建议=memory requests。什么?内存是硬限制,你没得选。如果limits大于requests,当Pod使用量超过requests但低于limits时,有触发节点级别OOM重分配的风险。Kubernetes在节点内存压力下,优先回收超过requests部分的Pod,这种隐性驱逐很令人头疼。所以内存最好requests=limits,让它无法贪心。
具体到YAML配置,下面是一份我常用的模板:
yaml复制apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "8"
requests.memory: "16Gi"
limits.cpu: "12"
limits.memory: "24Gi"
persistentvolumeclaims: "10"
pods: "50"
services: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: dev-limitrange
namespace: dev
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "256Mi"
max:
cpu: "4"
memory: "4Gi"
min:
cpu: "10m"
memory: "32Mi"
type: Container
这套配置的意思是这个命名空间最大可用8核CPU、16Gi内存,Pod数量上限50个;如果某个Pod没有显式写requests/limits,LimitRange会施加默认值;单个Pod最大4核4Gi,防止有人手滑创建超规格容器。
3.3 配额踩坑实录:从“配额不够”到“看不见的驱逐”
生产环境里,我因为配额问题踩过不少坑。第一个坑是“配额充足但Pod调度失败”。当时我check一下ResourceQuota,显示cpu和memory都还有很大余量,但Pod一直Pending,报了exceeded quota错误。排查了好久,发现是配额里的pods和services等对象数量配额也有限制,我们的微服务数量太多,把pods: "50"占满了。所以设置ResourceQuota时,不光要考虑CPU和内存,还要对Pod数量、PVC数量、Service数量留出充足余量。
第二个坑是“内存都没超,却出现了大量Pod被驱逐”。这个现象很诡异,Node明明还有内存,但Pod不断被标记为Evicted。查了节点状态和kubelet日志才发现,原因是这个节点上其他非K8s管理的进程(比如一些监控Agent、日志采集器)吃了大量内存,超过了Node的allocatable内存。Kubernetes在内存压力下会优先驱逐那些实际使用量超过memory requests的Pod。所以你在设置requests时,一定不要只看应用本身的内存,还要考虑节点上系统预留(system-reserved)和Kubelet预留(kube-reserved)的份额。
第三个坑是“LimitRange影响到了集群插件”。我们把LimitRange创建在了kube-system命名空间(其实不太应该,但确实有人这样干过),结果CoreDNS等组件重建时被LimitRange施加了一个很紧的CPU限制,导致DNS查询超时。所以LimitRange和ResourceQuota一定要明智地规划命名空间,比如业务命名空间用一套配置,系统命名空间最好别去动。
4. 弹性伸缩:让容量跟着流量走
4.1 HPA原理与指标选型
弹性伸缩是“永不宕机三板斧”里最华丽的一斧,也是很多人误用最多的一斧。HPA(Horizontal Pod Autoscaler)的核心是一个控制循环,它周期性地获取Pod的监控指标,根据当前指标和期望指标计算出期望副本数,然后通过更新Deployment的replicas字段来调整副本数量。
计算副本数的公式是:
期望副本数 = ceil(当前副本数 × (当前指标值 / 期望指标值))
举个例子,当前有4个Pod,每个Pod的CPU使用率是80%,设置的期望CPU使用率是50%,那么期望副本数 = 4 × (80 / 50) = 4 × 1.6 = 6.4,向上取整得到7。HPA会滚动扩容到7个Pod。当指标降到期望值以下后,会进入缩容逻辑,但HPA默认有一个--horizontal-pod-autoscaler-downscale-stabilization窗口(默认5分钟),避免频繁缩容造成抖动。
HPA支持的指标有很多种:CPU使用率、内存使用率、自定义指标(比如QPS、P99延迟、队列长度)。我的建议是,关键业务至少同时配置两个HPA规则,一个基于CPU/内存(基础容量保障),一个基于业务指标(比如QPS或队列长度,应对突发流量)。只有CPU的HPA在流量型业务上会反应迟钝,比如一个服务CPU使用率很低,但下游队列已经积压了上万个请求,此时只靠CPU扩容是不够的。
拿我曾经负责的一个订单系统举例,流量模式是典型的“秒杀前平稳、秒杀开始后QPS瞬间翻20倍”。我们给订单服务配置了两条HPA规则:一条是CPU目标值60%,另一条是核心接口QPS目标值5000。两条规则同时作用时,HPA取计算结果的较大者。秒杀瞬间CPU还没打满,但QPS已经爆表,HPA根据QPS立刻扩容20个Pod,等CPU也上来了再去扩更多。这个配置上线后,秒杀场景再也没有出现过订单服务超时报错。
4.2 HPA配置详解:从YAML到实战调优
下面是一个实际的HPA配置示例,同时支持CPU和自定义指标(需要配合Prometheus Adapter或类似组件)。
yaml复制apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 30
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Pods
pods:
metric:
name: http_qps
target:
type: AverageValue
averageValue: 5000
这个配置里有两个关键点。第一是behavior字段,这是autoscaling/v2版本新增的,用来精细控制扩缩容策略。scaleUp里的stabilizationWindowSeconds: 0意味着扩容不用等待稳定窗口,立即响应;policies里100%的Percent意味着每次扩容最多翻倍副本数,15秒评估一次。这样能非常激进地应对流量尖峰。scaleDown则相反,稳定窗口300秒,每次最多缩1个Pod,避免流量抖动导致反复横跳。
第二是自定义指标http_qps如何采集。我们使用的是Prometheus + Prometheus Adapter。业务侧在代码里埋点,将QPS、P99延迟等暴露为Prometheus指标,然后通过Adapter将指标暴露给HPA。具体Adapter配置里,需要写一个seriesQuery去匹配指标,然后定义metricsQuery来获取每个Pod的平均值。这里有个容易踩的坑:HPA拿到的是所有Pod指标的平均值还是总和的某种聚合?如果Adapter的配置不对,HPA算出来的期望副本数可能完全不是你想要的效果。我建议在调试阶段,先用kubectl get hpa -o yaml查看HPA的状态,重点关注currentMetrics和desiredReplicas字段,能直接看出计算依据是否合理。
4.3 弹性伸缩的边界与局限:别把鸡蛋全放在一个篮子里
弹性伸缩虽然厉害,但不能神话它。有几个边界条件必须提前想清楚。
第一,有状态服务不能随便伸缩。如果你的Pod里挂了本地磁盘,或者依赖固定的节点身份,扩容后新Pod可能无法正常加入集群,缩容可能导致数据丢失。所以数据库、缓存、消息队列这类有状态服务,建议不要直接使用HPA,而是选择StatefulSet配合专门的自动扩缩容方案,或者依靠垂直扩容(VPA)。
第二,集群节点规模的限制。HPA扩容的Pod需要有足够的节点资源去调度。如果集群本身已经满了,即使HPA算出来需要20个副本,也没法调度成功,Pod会一直Pending。所以我强调“弹性伸缩”必须和“集群节点自动扩容”配合。在云环境里,这通常意味着Cluster Autoscaler——当Pod Pending时,CA自动创建新的云服务器并加入集群。本地机房的话,就得提前规划好低峰期备用节点或者集群容量。
第三,应用启动时间决定了扩容的生效速度。假设你的应用启动需要2分钟,HPA检测到流量尖峰到最终新Pod能接流量,可能已经过去3分钟了。如果流量在1分钟内就冲垮了现有实例,那中间这段“空窗期”你依然会损失请求。怎么缓解?提前把最小副本数维持在一个能抗住日常峰值的水位上,别为了省成本把minReplicas压得太低,尤其是核心链路。另外还可以用预测式扩容,基于历史流量规律提前扩容,不过那又是另一个话题了。
在这里分享一个我实际调优case。一个数据采集服务,每天早上会有大波数据投递,但集群里每个Pod的CPU使用率只有20%,而QPS却高达2万。我配置了HPA,指标是QPS目标值5000,minReplicas=3,maxReplicas=10。结果发现,数据投递高峰开始时,QPS瞬间涨到3万,HPA快速扩容到10个Pod,但因为Pod启动时间大约30秒,前30秒内请求还是全部打到了原来的3个Pod上,导致它们CPU迅速飙高,response time翻倍。后来我把minReplicas调成了5,又把“预热”逻辑加到了Deployment的preStop钩子里,让新Pod在接收流量前先完成缓存加载。这套组合下来,高峰期基本稳了。
5. 三板斧协同作战:完整架构与实战演练
5.1 三级防御体系的联动模型
探针、资源配额和弹性伸缩是三个独立的工具,但它们必须联动起来才能发挥1+1+1>3的效果。我把这套体系称为“三级防御”。
第一级是“发现”:探针发现问题。Liveness探针发现容器卡死,Readiness探针发现Pod暂时无法服务。第二级是“隔离”:资源配额限定故障半径。当探针频繁失败时,Kubernetes会重启或摘除故障Pod,但同一时刻重启的数量受限于PodDisruptionBudget(PDB)和ReplicaSet的策略;资源配额则确保即使扩容,也最多只能到达配额上限,不会把集群资源榨干。第三级是“恢复”:弹性伸缩补充容量。如果故障是流量造成的,HPA会在探针检测到指标升高后扩容来吸收流量;如果是代码Bug导致的进程崩溃,探针会不断重启,但最终的解决方式还是靠回滚或修复代码,伸缩只能缓解症状。
在一个具体的连锁反应里,协同关系是这样的:早上流量井喷,HPA检测到QPS超过目标值,开始扩容。扩容的新Pod要确认ResourceQuota有足够余量,如果配额不足,新增PodPend,HPA的扩容被阻塞。与此同时,老Pod因为流量压力开始出现响应变慢,ReadinessProbe超时,kubelet将不可用Pod从Service Endpoints中摘除。摘除之后,可用Pod数进一步减少,流量更加集中,老Pod的压力更大。这个恶性循环如果不加控制,就是典型的“雪崩”。而资源配额和PDB的作用就是踩刹车,宁可让部分请求返回503,也不能让所有实例被流量拖垮。稍微理解一下这层关系,你就明白为什么我强烈不建议“只配HPA不配配额”了。
5.2 模拟演练:一次流量突刺的完整处理过程
为了让你有更直观的感受,我描述一次实战模拟。假设我们有一个order-service,部署3个Pod,每个Pod requests是200m CPU、512Mi内存,HPA配置CPU目标为60%,QPS目标5000,minReplicas=3,maxReplicas=10。命名空间ResourceQuota设置了requests.cpu=10,requests.memory=20Gi。
某天下午2点,大促页面开始推送,流量在30秒内从平均QPS 2万涨到8万。HPA的metrics循环(默认15秒一次)抓取到QPS超过目标值,计算出期望副本数为ceil(3 × (80000 / (5000×3))) ≈ 16,但maxReplicas限制为10,所以HPA把Deployment的replicas更新为10。此时现有3个Pod的CPU已经飙升到90%,Liveness探针没有失败(因为进程还在),Readiness探针由于HTTP响应时间变慢开始出现失败,1个Pod被摘除流量。
接下来进入加速扩容阶段。HPA在下一个周期(大概15秒后)重新计算,发现当前QPS依然高,维持10个副本。集群调度器开始在空闲Node上创建7个新Pod,每个新Pod读配置、连数据库、加载缓存,整个启动过程大约40秒。由于新Pod启动期间ReadinessProbe处于失败状态,流量依然集中在老Pod上,但老Pod已经被摘掉了一个,剩下2个Pod压力更大。不过关键来了:由于ResourceQuota限制了requests.cpu上限是10,而2个老Pod加8个新Pod(假设其中2个已经Ready)的requests可能已经快接近配额上限,后续Pod开始Pending。HPA看到有Pending Pod,其实并不会阻塞,因为HPA不感知Pending,它只会继续更新replicas。问题在于配额不释放,Pending会一直存在。
40秒后,大部分新Pod完成启动,ReadinessProbe通过,Service Endpoints把流量转发给新Pod。老Pod的CPU和响应时间逐渐回落。QPS从峰值开始下降,HPA进入缩容稳定窗口,5分钟后副本数从10降到6,再过10分钟降到3。整个过程中,部分请求可能出现过一次或两次5xx,但绝大多数请求都正常返回。用户感受到的可能是“偶尔有点卡”,但不会出现大面积的不可用。
这段模拟想说明一个道理:HPA不是万能的,它的扩容速度、配额限制、启动速度,都决定了你在流量尖峰时到底会不会损失请求。如果不提前准备好资源池、不把quota放宽到合理水位,HPA就是一台“有心无力的跑步机”。
5.3 生产落地时的配置清单和优先级
我习惯在接手一个新服务做高可用改造时,按照如下顺序来配置这套体系。这也是一份可以直接对照检查的清单。
- 确认服务的requests和limits已经合理设置(至少内存requests=limits,CPU limits略大于requests)。
- 配置StartupProbe、ReadinessProbe、LivenessProbe,接口为轻量的/healthz和/ready,并确保initialDelaySeconds、periodSeconds、failureThreshold组合合理。
- 为命名空间创建ResourceQuota和LimitRange,确认配额上限给业务留出2倍以上余量。
- 为每个核心工作负载配置HPA,优先指标顺序:业务指标(QPS/延迟)> CPU > 内存。
- 配置PodDisruptionBudget,保证主动运维(节点维护、滚动升级)时也不会同时摘掉太多Pod。
- 如果使用云厂商,开通Cluster Autoscaler或节点池弹性伸缩,确保Pod扩容后调度器有资源可用。
- 配置监控告警,围绕“探针失败率”、“HPA扩缩容事件”、“配额使用率”、“Pending Pods数量”四个维度设置Alert。
- 最后进行混沌演练:主动kill一个Pod、模拟节点故障、压测流量突刺,观察整个系统能否自动恢复。
这套清单不要求一次全做完,但每一步都是下一层有效性的前提。很多团队只做第4步(配置HPA),却跳过了第3步(配额),结果遇到超大流量时扩容到几百个Pod,直接把整个集群打爆。这类事故在大促期间其实并不少见。
6. 常见问题与排查技巧实录
6.1 探针相关:一直重启、流量异常、误判
问题1:Pod已经Running,但不断被重启,怎么排查?
先看kubectl describe pod xxx,找到Events的最后几条。如果看到Liveness probe failed: HTTP probe failed with statuscode: 500,基本是健康接口返回了5xx。ssh进容器手动curl一下健康接口,看返回什么。常见原因有:健康接口依赖了Redis/MySQL,Redis抖了一下导致接口报错;或者健康接口处理线程池被业务请求占满,导致请求排超时。处理办法:给健康接口单独分配轻量逻辑,让它不依赖外部组件,如果必须要依赖,至少加超时和熔断。
问题2:服务正常,但流量总是不均匀,部分Pod被频繁摘除和加回。
这个情况多半是ReadinessProbe的阈值太敏感。比如periodSeconds=2,failureThreshold=2,那么一次网络抖动就会连续失败两次,导致Pod被摘流量。可以适当调大failureThreshold到3或4,同时把健康接口的超时时间设得更短(比如1秒内必须返回)。如果调整后还频繁抖动,那就看下节点的网络延迟和负载,也许你的探针接口真的写得有问题。
问题3:StartupProbe和LivenessProbe到底怎么区分?
一句话解释:StartupProbe管“第一次”,Liveness管“以后”。应用启动慢就用StartupProbe,把failureThreshold调大,给足启动时间。一旦StartupProbe成功之后,就不再管了,后面全程由Liveness和Readiness接管。千万不要把StartupProbe当成普通探针一直配置,那样反而会让启动先卡在StartupProbe上。
6.2 资源配额相关:配额不足、未知驱逐、LimitRange干扰
问题1:Pod创建时报exceeded quota,但明明配额还有空间。
先kubectl get resourcequota -n <namespace> -o yaml,把.status里的used字段和hard字段对照。你会发现不只是CPU和内存,可能pods、services、secrets、configmaps这些对象也有限额,业务一跑起来这些小对象数量增长很快,很容易打满。所以配额设置时,这些“隐藏配额”也要留足。
问题2:经常有Pod被Evicted,节点内存又没满。
交代两个方向。一是看这个Pod的实际使用内存是否超过了它自己的memory limit,如果超过会产生OOMKilled,而不是Evicted。二是看节点是否处于MemoryPressure状态,如果节点内存用量超过了allocatable,kubelet会开始驱逐Pod,优先驱逐使用量超过requests的Pod。这就要回到requests设置的合理性上,调大requests或减少节点上非K8s进程的内存占用。
问题3:LimitRange让整个命名空间的服务变得很慢。
这种情况是LimitRange的默认值太“小气”。比如defaultRequest.cpu设成了50m,那没写requests的服务就会用50m调度,几乎等于没有CPU保证。一旦节点忙碌,这些Pod的CPU会被压缩到很低,延迟自然飙升。解决方法是把defaultRequest调成贴近实际使用的值,或者干脆要求业务方都显式写requests,严禁“裸奔”部署。
6.3 弹性伸缩相关:HPA不生效、扩容慢、缩容抖动
问题1:HPA状态显示<unknown>,没有指标。
这是最常见的HPA问题。可能原因:Metrics Server没装(CPU类指标)或者Prometheus Adapter没有正确暴露自定义指标。先kubectl get apiservices | grep metrics,查看metrics相关服务是否健康。如果是自定义指标,去看Adapter的日志,确认seriesQuery能否匹配到指标series,metricsQuery能否按Pod维度聚合。
问题2:HPA扩容了,但是流量还是被打挂。
原因一般有两个:一个是扩容的Pod还不Ready,流量没打过来。可以通过看新Pod的ReadinessProbe失败次数和事件确认。另一个是HPA扩容的触发太慢,比如periodSeconds默认15秒,要连续几次采样超标才扩,而流量在10秒内就击垮了老Pod。解法是把scaleUp的policies配置得激进一点,甚至把stabilizationWindowSeconds设为0,让扩容量立即执行。
问题3:缩容时副本数上下抖动,看起来像振荡。
HPA默认有5分钟缩容稳定窗口,但如果有多个指标同时驱动HPA,且每个指标对副本数的期望不同,就可能出现抖动。比如CPU目标60%时算出5个Pod,QPS目标5000时算出4个Pod,两者轮流占优,导致反复伸缩。解法是合理设计目标指标,不要让两个指标差距太大,同时把scaleDown的stabilizationWindowSeconds调大到10分钟。另外一个思路是使用自定义指标时,目标值不要设太靠近某个临界值,留出15%-20%的缓冲。
问题4:弹性伸缩能不能自动增加集群节点?
HPA只管Pod副本数,不管节点。Pod扩容后如果没有空余节点,会处于Pending。要自动增加节点,需要部署Cluster Autoscaler或者使用云厂商的节点池弹性伸缩。注意两者之间有一个联动时间差:Pod Pending触发CA创建节点,云主机启动可能需要2-5分钟,这个时间窗口内流量压力依然存在。所以容量规划上一定要留足buffer,不要指望着自动扩容去兜底突发流量。
7. 实操过程中的几个关键心得
写到这儿,把主要技术点都过了一遍。最后想聊几个比较玄但很关键的心得,这些不是标准文档里容易看出来的东西,但直接影响你在生产环境能不能把“永不宕机”从口号变成现实。
第一点,监控告警要比K8s自带状态多走一步。K8s只能告诉你Pod有没有 Running、Ready,但一个Pod状态是Ready,不代表用户请求就是健康的。你得自己定义业务层的“健康”——比如核心接口的P99延迟、错误率、下游依赖的可用性。这些指标要和探针数据、HPA状态放在同一个看板里看,才能拼出全局图景。我见过团队只看Pod状态,Pod全绿但业务方已经报警,原因就是接口慢、数据库连接池耗尽。
第二点,生产环境的变更要遵循“先小后大、先低峰后高峰”的原则。探针参数调优、HPA策略调整、ResourceQuota改动,都必须在低峰期灰度验证,并且确认回滚方案。我吃过亏:在高峰期调了一个HPA的maxReplicas,期望是提高扩容上限,结果因为新参数触发了异常扩容,短时间内创建了50个Pod,把整个集群存储打爆。那次事故让我明白了,任何对“自动伸缩”的改动,其实都是在调整系统的失控边界,宁可在低峰期多花一小时验证,也不能在高峰期赌一把。
第三点,Chaos Engineering真的有用。你可以每个月挑一次低峰期,手动kill一个Pod、断开一个节点的网络、模拟一次CPU尖峰,观察系统能不能在数分钟内自动恢复。如果每次演练都能达成目标,你对“永不宕机”的信心才会是真的。演练中暴露出来的问题,往往就是生产环境下一次故障的预演。
最后,再补充一个我在团队里很看重的小习惯:每次流量的异常或者故障过后,不只写事故报告,还要把HPA的扩缩容历史、探针失败历史、配额使用率趋势拉出来对比,寻找“下一次可以被预测的规律”。很多时候,故障的根源不是机器不够,而是我们对系统行为的理解不够。这套三板斧组合,只是提供一个稳定可控的底座,底座之上,还需要你对业务有足够的理解,才能做出真正准确的容量决策。
