☰
AI推理服务K8s弹性伸缩实践:HPA配置与扩容避坑指南
2026/9/30 3:33:02 网站建设 项目流程

1. 从一次故障复盘说起:AI健康业务为什么必须自动扩容

先讲一个真实发生在我这边的场景。

某个工作日晚间八点半,我们的AI虚拟健康系统突然迎来一波流量高峰——用户集中上线做健康评估,智能问诊模块的请求量在十几分钟内翻了四倍。当时系统还是固定副本数部署,核心推理服务在Kubernetes集群里一共跑着8个Pod,正常情况下CPU水位在40%左右。那波流量冲上来之后,CPU直接拉满,Pod开始频繁OOM,健康检查探针连续失败,前端用户看到的是"服务繁忙,请稍后再试",客服群里一晚上进来了三十多条同类反馈。

那次故障持续了将近四十分钟,等我们手动扩容到24个副本、业务恢复平稳的时候,已经错过了用户线上问诊的黄金时段。事后复盘时大家达成了共识:像AI虚拟健康系统这种面向C端、流量天生带脉冲特性的业务,靠人肉扩容是救不回来的。凌晨三点加密的紧急工单、比平时贵好几倍的临时机器成本、用户流失的隐性损失,这些东西只要经历过一次就不想再来第二次。

所以那之后我们做的第一件事,就是把"弹性伸缩"作为系统的基础能力来建设,而不是一个可选的优化项。核心选型很明确:Kubernetes做容器编排底座,HPA(Horizontal Pod Autoscaler,水平Pod自动扩缩容)做自动扩缩容控制器。这套组合本身不算新鲜,真正花心思的地方在于——AI推理负载的特征跟传统Web应用不一样,CPU模型不能简单套用,GPU资源、推理时延、排队长度、服务预热,这些因素叠在一起,让"自动扩容"这件事比看上去要复杂得多。

这篇文章不打算整篇堆概念,我会直接从架构设计、HPA配置细节、AI负载适配、生产验证这几个维度展开。如果你正在做AI类服务上K8s、或者被流量高峰搞得焦头烂额,这篇应该能帮你少踩几个坑。另外说明一点:文中涉及的具体数值和配置,都是我们基于自身场景调整出来的,你可以参考思路和推导方法,不必照抄。

2. 整体架构设计与技术选型背后的取舍逻辑

2.1 系统模块拆解:哪些服务需要纳入弹性伸缩范围

AI虚拟健康系统不是单个服务,而是一组服务的集合。从流量入口到最终响应,大致可以分成四层:

  • 接入层:Nginx Ingress,负责TLS终止、路由转发、限流;
  • 业务逻辑层:用户服务、健康档案服务、预约服务等无状态API;
  • AI能力层:智能问诊推理服务、健康评估引擎、语音交互服务;
  • 数据与状态层:MySQL、Redis、向量数据库、对象存储。

最开始我们只对业务逻辑层做了HPA,AI服务因为依赖GPU,暂时没纳入。结果第二次流量高峰一来,业务API层倒是扛住了,AI推理服务的GPU利用率却冲到95%以上,推理请求在队列里排队,平均响应时间从800ms拉长到6秒。用户端的体感就是"问一句等半天",这等于没解决问题。

所以后来我们把弹性伸缩的范围扩大到了AI能力层,这也是这套架构跟常规HPA实践最不一样的地方。说白了,弹性伸缩不是给某个服务单独做的事,而是从接入层到推理层统一规划的事。任何一个环节卡住,整条链路都是瓶颈,用户感知到的永远是那一个卡住的环节。

2.2 为什么选择K8s+HPA,而不是自研扩缩容系统

这里先说个背景:我们团队早期用的是云厂商的VM+负载均衡,弹性伸缩靠的是云平台自带的"定时策略+CPU阈值策略"。当时看起来够用,实际跑了半年暴露出几个问题:

  • 冷启动时间太长。新机器从扩容到流量真正能打进去,普遍需要5到10分钟,赶上流量陡增根本来不及;
  • 策略维度太少。云平台的伸缩策略基本只看CPU、内存、带宽,拿不到业务自定义指标,比如"推理队列长度""请求排队数";
  • 跨服务协同弱。我们想让AI推理层和业务层联动扩容,在云平台原生方案里实现起来很别扭。

换到K8s+HPA之后,这几个问题基本都解决了。Pod级别的扩容粒度是秒级到分钟级,启动一个Java服务Pod从镜像拉取到Ready大约40到60秒,跟虚拟机不在一个量级。HPA支持通过API获取自定义指标,Prometheus Adapter可以把任何指标暴露给HPA做决策。最关键的是所有编排逻辑都落在代码和声明式配置里,整个系统是可控、可审计的。

那为什么没有直接用KEDA(Kubernetes Event-driven Autoscaler)?我们也评估过。KEDA确实在事件驱动方面更完备,对Kafka、RabbitMQ这类消息源有原生支持。但我们的核心流量特征是HTTP突增,不是消息队列积压,HPA的模型已经够用,KEDA反而在日常运维上多了一个组件要维护。所以在第一版架构里,我们选择了最直接、团队最熟的HPA方案,给KEDA留了扩展接口,后续如果引入异步任务体系再切换也不迟。

2.3 核心链路:从Metrics采集到扩缩容决策的完整流程

把HPA的工作链路拆开看,其实是一套"采集—存储—查询—决策—执行"的闭环:

  1. metrics-server或Prometheus Adapter采集Pod的资源使用数据(CPU、内存或自定义指标);
  2. 数据通过聚合API暴露给HPA控制器(HPA通过/apis/metrics.k8s.io查询);
  3. HPA控制器在每个同步周期(默认15秒)内计算当前副本数与期望副本数;
  4. 如果期望副本数偏离当前副本数超过容忍阈值,HPA触发扩容或缩容操作;
  5. Deployment控制器根据期望副本数创建或销毁Pod。

期望副本数是怎么算出来的?核心公式是:

期望副本数 = ceil(当前副本数 × (当前指标值 / 期望指标值))

举个例子,当前8个Pod,CPU平均利用率是85%,我们设的期望值是50%,那期望副本数就是ceil(8 × 85/50) = ceil(13.6) = 14个。之所以不是简简单单的"85比50",是因为副本数和单Pod负载之间存在线性假设下的等比关系。虽然实际系统里负载均衡不一定完全线性,但这个公式在绝大多数场景下能给出合理的结果。

这里有一个很多人忽略的点:HPA的算法是"比例估算+迭代逼近",不是一步到位精确算出最终副本数。所以流量剧烈变化时,HPA可能需要几个同步周期才能稳定到目标副本数,这也是我们后续要配合稳定窗口、behavior策略的原因。

3. HPA配置从能用到好用:稳定窗口、多指标与行为策略

3.1 一份能直接落地的HPA配置样例

直接贴一份我们生产环境在用的HPA配置,针对的是AI智能问诊推理服务。这版配置经过了好几轮压测和故障验证,参数不是随手填的,下面会逐个解释。

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: ai-inference-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: ai-inference minReplicas: 4 maxReplicas: 32 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 - type: Pods pods: metric: name: inference_queue_depth target: type: AverageValue averageValue: "5" behavior: scaleUp: stabilizationWindowSeconds: 60 policies: - type: Percent value: 100 periodSeconds: 30 - type: Pods value: 8 periodSeconds: 30 selectPolicy: Max scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 20 periodSeconds: 60

先说metrics部分。我们同时挂了CPU利用率和自定义指标inference_queue_depth。CPU利用率是保底指标,覆盖那些"流量没上来但代码死循环把CPU打满"的异常场景;自定义指标才是主力,具体含义是推理服务内部消息队列的积压深度,这个指标能更真实地反映"当前服务是不是处理不过来了"。

再说behavior,这是很多人容易忽略但价值极大的部分。HPA从v2版本开始支持通过behavior字段精细控制扩缩容行为。扩缩容策略由稳定窗口(stabilizationWindowSeconds)和扩缩容策略(policies)组成。我们这里设置的是扩容允许每30秒翻倍或最多增加8个Pod,取两者中更激进的(selectPolicy: Max);缩容则要求维持300秒稳定后,每分钟最多缩容20%。这组配置的价值在于:既能快速跟上流量突增,又不会在流量小波动时频繁抖动。

3.2 稳定窗口:为什么这个参数直接决定用户体验

稳定窗口这个参数,很多人第一次看文档会直接忽略,但它恰恰是"能不能在生产环境用起来"的关键。

HPA的容忍度逻辑是这样的:假设当前期望副本数是20,实际是18,偏差只有2个副本(10%),小于默认容忍度,HPA就不会调整。只有偏差超过容忍度才触发扩缩容。但是,如果指标在50%和90%之间来回跳动,HPA就可能在扩容和缩容之间反复横跳,每次调整都要创建或销毁Pod,引发不必要的资源浪费和服务抖动。

稳定窗口的作用就是给决策加一层"惯性"。扩容侧我们设置了60秒,意思是60秒内HPA会保留历史决策的最大值,避免刚扩容完又立刻缩回去。缩容侧设置300秒,是避免流量稍微降一点系统就急着缩容,结果下个高峰又得扩容回来,形成震荡。

调整稳定窗口时候有个经验:扩容稳定窗口可以短一点(30到90秒),缩容一定要拉长(至少3到5分钟)。原因是缩容的代价是闲置资源浪费,扩容的代价是服务质量下降。两害相权,宁可多跑一会闲置资源,也别在流量还没稳的时候就把Pod杀了。

3.3 多指标组合里的主次关系与判断逻辑

多指标同时存在时,HPA取的是所有指标计算出的副本数的最大值。比如CPU算出来要10个副本,队列深度算出来要16个副本,HPA按16个执行。

这个设计逻辑很容易理解:任何一个指标到了瓶颈,都说明系统容量不足,应当扩到满足最紧张那个指标的数量。反之,缩容时也要等所有指标都满足低于目标的条件,才会真正缩容。

实际使用中要留意一个问题:某些自定义指标容易瞬时尖刺。比如推理队列深度,可能在一两秒内因为某次大请求批量进入而暴增,触发扩容,但10秒后队列又消费完了。这种指标如果不加平滑处理就直接暴露给HPA,会导致扩容频率过高。我们的做法是在Prometheus Adapter的查询里做"最近1分钟平均",通过avg_over_time函数把瞬时毛刺磨平,而不是在HPA端增加稳定窗口硬扛。

3.4 自定义指标接入Prometheus Adapter的配置要点

自定义指标接入HPA,走的链路是:应用暴露指标 -> Prometheus采集 -> Prometheus Adapter转换成聚合API -> HPA查询。关键配置在Prometheus Adapter的规则里,截取一段我们在用的规则:

rules: - seriesQuery: 'inference_queue_depth_total{namespace!="",pod!=""}' resources: overrides: namespace: { resource: "namespace" } pod: { resource: "pod" } name: matches: "inference_queue_depth_total" as: "inference_queue_depth" metricsQuery: 'sum(rate(inference_queue_depth_total{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)'

这里面的核心思路是:先找到原始指标序列(seriesQuery),然后把namespace和pod标签映射成K8s资源(resources),最后用PromQL计算出每个Pod的指标值(metricsQuery)。注意<<.LabelMatchers>>和<<.GroupBy>>是Adapter的模板变量,作用分别是把当前请求的过滤条件和分组维度拼进去,这样HPA按Pod查询时,Adapter就知道该返回哪个Pod的数据。

配置完成后可以用一条命令验证指标是否生效:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/production/pods/*/inference_queue_depth" | jq

如果返回了每个Pod的指标值,说明链路是通的。这一步看似简单,其实很多团队卡在这里很久:Prometheus有数据,但HPA查不到,极大概率就是Adapter规则里的标签映射写错了。

4. AI推理场景的流量高峰应对:GPU、预热与队列协同

4.1 AI服务扩容为什么不能只盯CPU

常规Web服务的HPA,盯CPU或内存就够了,因为请求处理是均匀的,更多副本数意味着更多吞吐。但AI推理服务不一样,它是计算密集型负载,而且常常依赖GPU。

如果我们把GPU加速的推理服务也按CPU利用率来扩容,会遇到几个尴尬情况:

  • CPU利用率很低的Pod,可能GPU已经打满了。因为大部分推理框架(如TensorFlow Serving、TorchServe)把计算都卸载到GPU,CPU只负责数据预处理和调度;
  • GPU显存占用高不一定意味着计算繁忙。显存分配是模型加载时一次性完成的,即使没有请求进来,显存占用也在那里。拿显存利用率做扩容指标,会导致永远扩不上去或者过度扩容;
  • 推理延迟和吞吐之间存在非线性关系。吞吐到一定程度后,每增加一个并发请求,延迟会指数级上升,而CPU利用率可能才刚到70%。

所以AI服务的扩容指标,更应该关注"请求排队深度"和"P95推理延迟",而不是CPU或显存的绝对值。排队深度直接反映处理能力缺口,延迟则反映用户体验。我们在实践里把这两个指标都接进了自定义指标,效果比单纯用CPU准确不少。

4.2 模型加载与预热:HPA扩容成功后的下一道坎

HPA扩容成功,不等于服务就能立刻承担流量。AI推理服务有一个典型问题:新Pod从创建到Ready,需要加载模型权重、初始化推理引擎,这个时间可能是几十秒甚至几分钟,取决于模型规模和硬件。

拿我们用的健康评估模型来说,模型文件大概1.2GB,量化后也有400MB左右。新Pod启动时要把权重从对象存储拉到本地,再加载进显存,整个过程大约需要30到45秒。如果流量高峰期间HPA扩了10个Pod,但这10个Pod在预热完成前不允许接流量,那么实际扛峰容量并不是"当前副本数",而是"已完成预热的副本数"。

这个问题我们用了三个手段协同解决:

  • 配置就绪探针(readinessProbe),探针在模型加载完成后才返回成功。K8s只会把流量打给就绪的Pod,这样用户请求不会打到"半成品"Pod上;
  • 利用HPA的scaleUp策略,预先扩一部分副本。我们在压测时发现,如果等到队列深度超标再扩容,会出现一个尴尬窗口——扩容请求发出去了,但新Pod还在预热,这段时间服务质量继续恶化。所以后来配合定时策略,在业务高峰到来前5分钟先扩到预期水位,比如每天19:00自动扩到20个Pod,高峰期过了再让HPA慢慢缩回来;
  • 在推理服务的入口加了等待队列和超时控制,Pod预热期间队列深度会短暂上升,但不会导致请求直接失败,而是在队列里等待有Pod就绪后再消费。

这里有个反直觉的经验:不要把就绪探针的initialDelaySeconds设成0。应用进程启动后,模型加载和引擎初始化都需要时间,探针太早探测只会看到"未就绪",白白重试多次。我们设的是initialDelaySeconds: 10,periodSeconds: 5,基本上模型加载到一半左右,探针开始探测,正好能准确感知就绪状态。

4.3 流量高峰模式识别与扩容策略配置

观察了半年线上流量之后,我们总结出AI虚拟健康系统常见的三种流量高峰模式,每种模式的应对策略不一样:

  • 脉冲型:突发的、短促的流量尖峰,持续时间可能只有几分钟。典型场景是某个健康科普内容在外部渠道爆了,用户集中涌入。这种场景要求扩容速度足够快,稳定窗口不能太长,等流量过去后又要快速缩回来,否则资源成本扛不住;
  • 潮汐型:可预测的、有规律的流量起伏。比如工作日早高峰和晚间饭后是用户问诊的高峰时段,节假日可能整体偏低。这种场景适合用定时扩缩容打底,再用HPA做动态修正;
  • 持续型:活动期间(比如健康筛查推广),流量连续几天保持在高位。这种场景主要靠把maxReplicas上限调高,同时确保底层资源池容量充足。

针对这三种模式,我们在同一个HPA上通过behavior策略做了统一处理,再叠加CronJob来实现在特定时间段预先扩张副本数。CronJob做的事情很简单:到点修改Deployment的replicas字段,但会被HPA接管覆盖,因为HPA的副本数优先级高于手动设置的replicas。这里需要特别提醒:HPA开启后,不要直接去改Deployment的replicas,HPA会在下一个同步周期把你改的值覆盖掉。如果确实需要"定时扩容",合理做法是写一个临时调整HPA的minReplicas/maxReplicas的小工具。

我们实现的定时扩容CronJob逻辑大致如下:

apiVersion: batch/v1 kind: CronJob metadata: name: scale-out-before-peak spec: schedule: "0 18 * * *" jobTemplate: spec: template: spec: containers: - name: scale image: bitnami/kubectl:latest command: - /bin/bash - -c - | kubectl patch hpa ai-inference-hpa -n production \ --type merge \ -p '{"spec":{"minReplicas":20,"maxReplicas":32}}' restartPolicy: OnFailure

每天晚上18点把HPA的minReplicas抬高到20,等系统扛过晚间高峰,另一个CronJob在23点把minReplicas调回4。这样做既保证了高峰时段有基础容量兜底,又不影响HPA在min/max范围内动态调整。

5. 踩坑实录:三次告警轰炸后的完整排查链路

5.1 第一个坑:HPA一动不动,最终查出是metrics-server版本问题

上线初期,我们把HPA配置好之后,发现一个诡异现象:不管流量怎么涨,HPA始终显示副本数没变化。kubectl describe hpa里看到的状态一直是"Current: 8 pods, Desired: 8 pods",完全没有扩缩容动作。

先说排查思路。HPA不动作,可能出问题的环节有四层:指标采集层、指标查询层、HPA计算层、执行层。我们按这个顺序逐层排查:

  • 先看metrics-server的Pod状态:kubectl get pods -n kube-system | grep metrics-server,发现Pod在反复重启,CrashLoopBackOff;
  • 看日志:kubectl logs -n kube-system <metrics-server-pod> --previous,报错的是insecure skip verify相关的问题;
  • 查metrics-server版本和K8s集群版本的兼容性,才发现集群是1.20版本,但metrics-server用的是v0.4.x,这个版本对1.20的aggregator兼容性很差,需要升到v0.6.x。

根因就是版本兼容问题。metrics-server作为指标聚合组件,它需要通过kube-aggregator把指标API注册到K8s里,版本不匹配时注册失败,HPA查询指标的请求就得不到响应。HPA拿不到指标数据时,不会报错,而是静默保持当前副本数不变——这个"静默失败"的设计很容易让问题潜伏很久。

后来我们建了一个监控:定时检查kubectl get --raw /apis/metrics.k8s.io/v1beta1是否正常返回数据,如果返回异常就告警。这个检查放在HPA本身之前,算是"指标的指标"。

顺带说一下:如果你的HPA用的不是metrics-server而是Prometheus Adapter,同样需要注意版本和配置。Adapter的--metrics-relist-interval参数控制它多久重新加载一次规则,默认是1分钟,你改了规则后如果发现指标没有变化,多半是还没到重新加载的时间。

5.2 第二个坑:扩容之后性能反而下降,CPU指标解读出了偏差

还有一次印象很深的故障:流量高峰时HPA确实触发了扩容,从8个Pod扩到16个,但扩完之后系统整体吞吐量反而下降了,P95延迟比扩容前还高。当时第一反应是扩容出的Pod有问题,逐个检查新Pod的日志和资源使用情况,都没发现异常。

后来把监控面板的时间轴拉出来对比,才发现问题出在调度上:新扩容的8个Pod,有6个被调度到了同一台物理节点上。那台节点本来已经跑了其他服务,加上这6个推理Pod之后,CPU争抢激烈,反而拖慢了整体推理速度。

根因是集群的节点资源水位不均。我们的集群里有几台老节点,配置较低,而K8s默认调度器在做Pod调度时,虽然会计算节点资源可用量,但用的是"请求值(requests)"而不是"实际用量(usage)"。推理服务的requests设得比较保守(2C4G),但实际运行时会跑到4C8G甚至更高。调度器看到节点"还有资源",就把Pod塞上去了,结果实际运行时就超卖击穿了。

排查链路如下:

  1. 先确认HPA动作正常,kubectl describe hpa显示扩容触发了,且scaleTargetRef正确指向了Deployment;
  2. 检查新建Pod的分布:kubectl get pods -o wide | grep ai-inference,发现多个Pod集中在同一节点;
  3. 查看该节点的资源占用:kubectl describe node <node-name>,发现Allocated resources已经接近上限;
  4. 对比Pod实际资源使用:kubectl top pods | grep ai-inference,发现实际使用远超requests。

解决思路有两个方向:一是把Deployment的requests设置得贴近真实使用量,让调度器提前感知资源压力;二是给关键服务加PodTopologySpreadConstraints,让Pod尽量分散到不同节点,避免"扎堆扩容"。

spec: topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: ai-inference

maxSkew: 1的意思是,不同节点上的副本数差异最多1个。这样即使HPA一次性扩出很多Pod,调度器也会尽量把它们打散到不同节点,而不是一股脑塞到"看起来有空闲"的那台机器上。

这里多说一句:whenUnsatisfiable有DoNotSchedule和ScheduleAnyway两个选项。我们用的是DoNotSchedule,表示如果无法满足打散要求,这个Pod就不调度,宁可Pending等待,也不要扎堆。缺点是有时候会造成Pod长时间Pending,所以需要配合合理的maxReplicas上限和节点池规划,确保集群整体容量是够的。

5.3 第三个坑:扩缩容抖动导致资源反复创建销毁,最终靠behavior和阈值拯救

第三个坑是在某个促销活动期间遇到的。那几天流量波动特别剧烈,系统在"扩容-缩容-扩容"之间来回折腾。我们统计了一下,一个下午HPA触发了二十多次扩缩容操作,Pod频繁创建和销毁,镜像仓库拉取压力大,集群节点也一直在"调度Pod-回收Pod"的循环里,很多Pod还出现启动到一半被杀掉的情况。

问题出在哪里?两个原因叠加。第一,流量本身波动大,峰谷交替密集;第二,也是更关键的,我们把缩容稳定窗口设得太短了,只有60秒。流量稍微一降,HPA觉得"低负载持续了60秒",就开始缩容,结果流量刚下去又起来,又得扩容。

这次之后我们把behavior参数调成了上面配置里那组:扩容稳定窗口60秒、缩容稳定窗口300秒,同时缩容策略限制为每分钟最多20%。注意这里不是只改一个数字那么简单,而是想清楚了两个问题:

  • 缩容的代价是资源浪费,扩容的代价是服务不可用。在C端用户体验优先的场景里,我们宁可浪费资源,也不能让服务在高峰时处于"还在扩容途中"的亚健康状态;
  • 限制缩容速率能让"刚缩完又要扩"的窗口大幅缩短。就算流量确实长期下降了,缩容只是慢一点,不会造成成本失控。

另外还加了一道保险:scaleDown策略里把selectPolicy设成了Min(虽然上面示例里扩缩容都只写了一个策略,默认不冲突,但如果你配置了多个策略,建议缩容用Min,扩容用Max,避免缩容策略叠加出激进效果)。

5.4 关于排错,给一个通用的排查顺序

经历了这几次故障,我们整理了一份HPA异常的排查顺序,团队新人对着一路查就行:

  1. 指标数据存在吗?检查metrics-server或Prometheus Adapter是否正常返回数据,用kubectl top pods和kubectl get --raw分别验证;
  2. HPA状态怎么说?kubectl describe hpa看Events和Conditions,重点看FailedGetResourceMetric、FailedComputeFullReplicaCount这类异常原因;
  3. 指标值合理吗?对比监控面板,确认HPA拿到的指标值和Prometheus里看到的实际值一致;
  4. 扩缩容动作执行了吗?看Deployment的副本数和kubectl get events里的ScaledUp/ScaledDown事件;
  5. 新Pod正常吗?检查新Pod的启动日志、就绪探针、资源实际占用;
  6. 调度分布合理吗?用kubectl get pods -o wide检查Pod是否均匀分布在节点上。

这套排查顺序不能说覆盖所有情况,但90%的HPA问题都能通过这六步定位到根因。

6. 生产验证方案与扩容后稳定性加固

6.1 压测方案设计:怎么验证弹性伸缩真的可靠

配置写好了,总得用数据说话。我们做的验证分成两轮:一轮是功能性验证,确认HPA能扩能缩;另一轮是以扩容速度和稳定性为目标的压测。

压测工具用的可以是wrk、k6这类常见负载生成器。考虑到我们的场景是AI问答接口,请求特征是"单次请求耗时长、对响应时间敏感",所以我们采用并发阶梯式压测:每5分钟增加50个并发,一直加到600,保持30分钟,观察HPA的响应曲线。

压力测试期间的几项关键指标:

指标意义参考值(我们的场景)
首次扩容触发时间从流量升高到HPA首次扩Pod的时间30秒内可接受
扩容完成时间从触发到新增Pod全部Ready60秒内较理想
扩容期间P95延迟扩容过程中用户体验的恶化程度不超过正常值的1.5倍
缩容后的资源消耗流量回落后多久能降到正常水位10分钟以内

压测下来,我们这套HPA配置的表现还不错:流量上来后大约40秒触发扩容,60到90秒内新Pod全部就绪,P95延迟从正常时的1200ms涨到1600ms左右,但没出现请求大量超时的情况。重点改进在于把minReplicas周期性抬高和Prometheus Adapter的平滑查询结合起来后,扩容触发的灵敏度有了明显改善。

6.2 扩容之后的三座大山:连接池、缓存击穿与分布式锁

HPA扩容成功只是第一步,扩容之后稳定性能不能保证,才是真正决定上线成败的因素。这里说三个我们踩过、也坑了很多团队的点。

连接池打满。推理服务每个Pod都维持着到MySQL和Redis的连接池。扩容前8个Pod,连接池总共占用80个MySQL连接;扩容后32个Pod,连接池翻了四倍。如果MySQL的max_connections没预留够,连接就会在DB层堆积,直接拖垮数据库。我们的处理方法是给推理服务单独建了一个只读账号,连接数上限按"最大副本数×单Pod连接池大小"来预留,同时在代码层面接入了连接池动态调整逻辑,Pod数增加时连接池初始大小也随之调整,避免默认值过大导致启动期就占满连接。

缓存击穿。健康评估服务的部分结果是带缓存的,缓存Key设计上带了用户维度。流量高峰期间,大量新用户涌入,缓存全部Miss,请求直接打到数据库上。如果不做保护,扩容等于把流量压力从应用层转移到了DB层。我们的方案是两层:一是对热点评估结果做进程内短时缓存,TTL设30秒;二是对数据库查询做了单飞逻辑,同一时间同一评估Key只有第一个请求真正落库,其他请求等待缓存回填。这个"单飞"模式在高峰期能挡住90%以上的DB穿透请求。

分布式锁与任务重复执行。AI健康评估流程里有一些异步任务,比如生成健康报告、调用外部服务做数据补充。这些任务如果在多个Pod里并发执行,会造成严重的系统性能浪费。我们用Redis分布式锁做了互斥,锁的粒度细化到任务实例ID,而不是锁全局。这个点看起来和弹性伸缩无关,但扩容后副本数变多,并发冲突概率会指数级上升,必须提前做好隔离。

6.3 扩容后的优雅下线:不能让流量打到正在销毁的Pod上

缩容是弹性伸缩的另一半,缩得不好同样会引发故障。Pod被删除时,如果负载均衡还在往这个Pod转发请求,用户就会遇到连接重置或请求超时。

K8s提供了terminationGracePeriodSeconds和PreStop钩子来处理优雅下线。我们的配置思路是:

  • 接到SIGTERM信号后,先停掉服务注册(从负载均衡摘除),不再接收新请求;
  • 等待几秒,让已接收的请求处理完毕;
  • 再退出进程。

实现上,可以给Deployment加上PreStop钩子,执行一条sleep命令:

spec: template: spec: terminationGracePeriodSeconds: 60 containers: - name: ai-inference lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 10"]

这个sleep的10秒时间,就是给Ingress和Service的endpoints更新留出时间窗口。别小看这个动作,没有PreStop的话,新Pod和销毁Pod的节奏错位,常常导致请求成功率下降0.5到1个百分点。对于C端系统来说,这个损失不算小。

6.4 巡检脚本自动化:把"看HPA面板"从手工操作里解放出来

最后分享一个我们内部的小工具:一个巡检脚本,写在CI里定时跑,或者在故障时手动执行一次,可以快速定位HPA相关异常。核心逻辑也很简单,就是把我前面说的排查顺序固化成命令:

#!/bin/bash # 检查HPA状态 kubectl get hpa -n production | grep ai-inference # 检查metrics-server健康 kubectl get --raw /apis/metrics.k8s.io/v1beta1 | head -n 20 # 检查Pod分布 kubectl get pods -n production -o wide | grep ai-inference | wc -l kubectl get pods -n production -o wide | grep ai-inference # 检查节点水位 kubectl top nodes

输出结构化信息,人工扫一眼基本就能判断问题出在哪个环节。

7. 上线半年之后的复盘与几个值得调整的方向

系统上线半年多,经历了几轮真实的流量高峰验证,整体是稳的。这里复盘几个我们后续想继续优化的方向,也给你们做参考。

第一,预测性扩容值得探索。现在我们是"反应式扩容"——先有压力,再扩容,中间难免有几十秒的服务质量下降。要想做到"流量还没到,Pod已经就位",就得引入预测。我们规划了两个路径:一个是用历史流量数据做时序预测,比如基于Prophet或LSTM训练流量模型,每天定时把预测结果同步给HPA;另一个是结合业务侧的突发事件信号,比如运营后台确认要推一个健康专题时,主动触发预扩容。React式扩缩容保底,预测式扩缩容提体验,两者结合才是比较理想的方案。

第二,当前自定义指标只覆盖了推理队列和P95延迟,其实还可以把"用户排队等待时长"纳入指标。这个是业务侧的真实体感,比队列深度更贴近用户感受。因为队列深度相同的情况下,如果队列里的请求都是长耗时请求,用户等待时间会更长。把这个指标引入后,HPA就能照顾到更多真实体验维度的变化。

第三,GPU层面的弹性。现在的架构里GPU节点是固定的,HPA扩出来的Pod如果对GPU有需求,调度器会优先往GPU节点塞。但如果GPU节点池已经满了,新Pod只能Pending。下一步我们打算引入GPU资源共享方案,或者规划自动扩缩容的GPU节点池,让推理层的弹性伸缩更加彻底。

第四,多集群容灾。目前单集群架构能扛住单节点故障,但扛不住整个集群的故障。我们已经在规划第二集群,用Federation或者直接在Ingress层做流量切换。HPA的配置需要跨集群同步,这里肯定还有新的坑要踩,等落地后再来分享。

8. 一点个人体会:弹性伸缩是一把手工程,不是加个HPA就完事

最后说一句个人的感受。

很多人以为用了K8s和HPA,弹性伸缩就自动搞定了。但实际操作下来你会发现,HPA只是打开了"自动扩容"的开关,真正决定系统能不能扛住流量高峰的,是你对业务负载特征的理解深度——AI服务和Web服务不一样,GPU和CPU不一样,长耗时推理和短请求不一样,每次扩容都意味着"服务能力翻倍,同时依赖的系统压力翻倍"。

我们在生产环境踩过的每一个坑,背后都对应一个之前没想清楚的问题:CPU指标和真实负载之间的关系、扩容后新Pod能不能立刻接流量、调度分布是否均匀、缩容会不会太激进、数据库连接池够不够、缓存会不会被打穿。这些问题一个个解决了,HPA才真正从"一个配置"变成了"一个可靠的机制"。

如果你的系统正在经历类似的流量之痛,我建议别急着上来就写HPA配置,先花点时间把自己的业务负载特征、指标结构、历史流量曲线梳理清楚。设计弹性伸缩方案的时候,把下面几条原则刻在脑子里:

  • 扩容要快,缩容要慢,稳定窗口是防抖的关键;
  • 多指标取最大值,不要让单一指标成为盲区;
  • 扩容的目标是"新Pod能立刻干活",而不是"新Pod存在";
  • 扩出来的流量最终都会打到下游(数据库、缓存),扩容前先确认下游扛得住;
  • 每一条HPA策略都应该经过压测验证,别只做功能测试就上线。

把这几条想明白了,你再去配置HPA,会发现那些文档里一笔带过的参数,每一个都有它存在的理由。这套架构现在还在持续迭代,后面有新的实践和踩坑,我再来更新分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询