今年三月份,我坐在工位上打开AWS Cost Explorer,差点把手里的咖啡洒在键盘上。我们那条大模型推理API管线的月账单,悄无声息地窜到了8.2万美元,而我们整个公司的月度营收才刚过25万。换句话说,光是跑模型推理就吃掉了三分之一的现金。更扎心的是,CFO发来的那封邮件里,只有一句话:“我们得谈谈云账单,明天早上9点。”
那段时间我每天用AI编程工具写代码。不是那种看看文档就写两句的轻度使用者,而是真的把自己的双手绑在AI的补全功能上——从Terraform模块到Kubernetes YAML,从Python运维脚本到Dockerfile,能交给AI写的我都不自己敲。但讽刺的是,我花在基础设施调优上的时间,反而比以前更长了。因为AI帮我把写代码的速度提上去之后,我有更多精力去琢磨那些真正烧钱的地方:GPU节点调度、扩缩容策略、实例类型组合、模型推理引擎的选择。这篇东西,就是我这三个月折腾下来的完整记录,包括我教会Cursor写FinOps脚本的具体流程,包括两次把生产环境搞挂掉的惨痛教训,包括最后怎么把那条管线的月成本压到1.7万美元的。
这不是一篇云厂商的最佳实践白皮书,也不是一篇让你看完觉得“哦又学了一套理论”的技术散文。这是我的实战笔记。
30秒速览
- - 传统基于CPU/内存的HPA在GPU推理场景下完全失效,必须用KEDA这类事件驱动扩缩,直接绑定队列深度。
- - 切换到TensorRT-LLM或vLLM等优化推理引擎,单GPU吞吐量可提升2-3倍,是最直接的成本缩减手段,但要注意CUDA驱动和镜像版本的适配。
- - Spot实例+中断预处理器+混合节点组的组合,能将GPU费用压低到按需价格的30%-40%,但同时需要KEDA的快速扩容和严格的调度策略兜底。
- - 我用Cursor编写了整个KEDA配置、Spot handler和FinOps脚本,但AI生成的冷却窗口、阈值等关键参数必须人工复核,否则可能直接引发生产事故。
- - 最终月成本从8.2万美元降到1.7万美元,可用性从99.5%提升到99.92%,但实现过程经历了两次深夜故障和无数次参数调优。
我的推理API是怎么悄无声息吃掉公司三分之一现金的
那晚我收到CFO的邮件,标题写着“我们得谈谈云账单”
先说背景。公司核心产品是一个面向电商的AI文案生成API,底下跑的是经过微调的Llama 3.1 70B模型。我在去年年底就把整个服务搬到了AWS EKS上,节点用的是g5.12xlarge实例——每台4个A10G GPU,24GB显存,足够把模型塞进去。当时业务量不大,我配了两个节点,一个常驻,另一个靠Cluster Autoscaler按CPU利用率自动伸缩。部署脚本全是我用Cursor写的,说实话那会儿我还挺得意,觉得“基础设施即代码”这件事已经被我玩明白了。(延伸阅读:我用三个框架跑了同一批模型,结果只有一个活得过生产环境)
但问题恰恰出在这种“我已经玩明白了”的错觉上。
二月中旬开始,客户的接入量突然翻了四倍。我们的销售签了三个大客户,没提前通知我。那套基于CPU利用率的自动伸缩机制,在流量峰值面前反应太慢了——当CPU飙到80%的时候才触发扩容,新节点从启动到就绪需要将近六分钟。这六分钟里,请求队列已经堆了几千条,P99延迟直接飙到22秒,客户那边疯狂报超时。为了勉强兜住,我在某个周五晚上手动把节点数调到了8台,心想“宁可用钱换稳定性”。
那个决定让我付出了惨重代价。8台g5.12xlarge按需实例,按当时的费率每小时约16美元,一个月就是16×24×30×8=9.2万美元。后来我虽然在下周一把节点数砍到4台,但月账单已经被推到了8.2万。CFO的邮件就是在那之后准时到来的。
翻开Cost Explorer,发现70%的GPU时间都在空转
第二天早上跟CFO开完会,我做的第一件事不是改代码,而是用Cursor写了一个Python脚本,调用AWS Cost Explorer API拉取最近30天每个EC2实例的每小时利用率和成本数据。脚本生成了一张CSV,结果显示:在手动增加节点的那一周,GPU利用率的中位数只有12%,峰值也没超过45%。换句话说,那些昂贵的A10G GPU有将近70%的时间在跑风扇,等请求。
传统基于CPU/内存的弹性伸缩,在AI推理负载面前就是个摆设。因为模型加载到显存后,只要没有请求进来,GPU利用率几乎为零,但CPU和内存的消耗基本恒定。HPA或者Cluster Autoscaler根本感知不到“队列里已经堵了几千条推理请求”这个事实,它们只会盯着资源利用率发呆。
那天中午我给自己泡了杯浓茶,开始盘算:我要的不是“有多少台机器在跑”,而是“有多少条请求在被处理”的实时反馈。我得让扩缩容的决策依据从资源指标转向业务指标。
架构挑战:为什么传统云优化在AI负载面前像纸糊的
GPU实例不像CPU实例,关机再开要等五分钟
在开始动手之前,我先梳理了一下AI推理负载特有的几个硬骨头。第一块骨头就是启动延迟。标准的CPU实例,从冷启动到容器就绪通常不超过60秒。但一台带着4个A10G GPU的g5.12xlarge,从ASG启动到NVIDIA驱动加载、再到模型加载进显存、直到API可以接受请求,这个过程最快要4分50秒。如果你用的是70B参数的大模型,这个时间还可能更长。
这意味着什么?意味着如果你用传统HPA的“检测到CPU升高->触发扩容->60秒后Pod就绪”的思维去做AI服务的弹性,你会得到一大段请求超时窗口,然后是一个刚启动完毕就发现流量已经过去的尴尬局面。而且频繁的冷启动本身还会增加成本——模型从S3或容器镜像里加载到显存的带宽和计算开销并不小。
推理请求的突发性:每分钟从0到2000,HPA跟不上
第二块骨头是流量的脉冲特性。我们的电商文案API,在工作日上午10点和下午3点会出现两次明显的尖峰,峰值QPS能到1600,而夜间最低的时候可能只有20。传统HPA即使配置了基于外部指标的自定义指标,默认的稳定窗口和扩缩容策略也是为相对平缓的负载设计的。Kubernetes原生的HPA在扩缩容时有一个冷却期(–horizontal-pod-autoscaler-downscale-stabilization-window),默认为5分钟,这意味着从高负载退下来之后,它会在5分钟内保持较多的Pod副本,这期间GPU依然在烧钱。(延伸阅读:我让Codestral Mamba在256k上下文中跑补全,速度是GPT-4的3倍,但上下文管理差点让我翻车)
我需要一个能直接跟消息队列深度绑定的自动扩缩器,并且能够以秒级粒度做出反应。
模型加载的冷启动噩梦
第三块骨头其实跟成本直接挂钩——模型加载本身。每次Pod重启,都需要把70B参数的模型权重从NFS或对象存储下载到本地NVMe,然后加载到GPU显存。这个过程耗时不说,还会产生大量的出站流量费用(如果模型放在S3上)。为了解决这个问题,我后来构建了一个预热机制(用一个Init Container在启动时把模型从PVC加载到共享内存),但这套机制本身也增加了Pod的启动复杂度。
这三块骨头叠加在一起,让传统的“按需实例+Auto Scaling Group”的方案显得格外笨重。我需要一套更敏捷的、能嗅到请求积压味道的调度系统。
第一轮优化:我让Cursor帮我写了个KEDA自动扩缩容,然后第一晚就挂了
操作实录:我在Cursor里敲下“ScaledObject”的那一刻
我决定引入KEDA(Kubernetes Event-driven Autoscaling)。KEDA能直接从RabbitMQ、Redis Streams或者Kafka里读取队列深度,根据积压消息数量动态调整Deployment的副本数。我的推理请求全部经过RabbitMQ分发,所以这个方案天然契合。
下面是我在Cursor里写KEDA ScaledObject的完整过程。我打开Cursor,在VSCode里已经连上了公司的EKS集群仓库。我新建了一个文件叫keda-scaler.yaml,然后开始敲注释。注意,我不是先在脑子里想好所有字段再写,而是直接输入:
# KEDA ScaledObject for llama-70b-inference, trigger on RabbitMQ queue depth
刚敲完这行,Cursor的AI补全(我用的模型是Claude 4.8 Sonnet)就直接给我补了一整段YAML骨架,包括metadata、spec.scaleTargetRef、triggers。我按Tab接受了骨架,然后开始改trigger部分。我需要指定RabbitMQ的host和queue名称,但忘记passwordRef的字段名了。于是我输入:
# RabbitMQ password from K8s secret, key is rabbitmq-password
紧接着,Cursor自动补出了完整的passwordRef段落:
metadata:
host: "amqp://user:password@rabbitmq.rabbitmq.svc.cluster.local:5672"
queueName: "inference-queue-v2"
mode: "QueueLength"
value: "10"
passwordRef:
key: "rabbitmq-password"
name: "keda-rabbitmq-secret"
我盯着这段补全看了五秒钟,发现两件事:第一,它自动把mode设成了QueueLength,这正是我想要的;第二,它把阈值value定成了10,这不对——对于大模型推理,一个请求就可能占用GPU几十秒,积压10条消息再扩容就太晚了。我手动把value改成了3。然后我继续往下写冷却参数,输入:
# scale down only after 15 minutes with no events
Cursor立刻补全了advanced部分:
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 900
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
policies:
- type: Pods
value: 2
periodSeconds: 30
我检查了一遍,觉得合理——缩容时等15分钟稳定窗口,每次缩50%;扩容时每次加2个Pod,每30秒评估一次。我把完整的YAML apply到集群,然后写了一个小脚本狂灌RabbitMQ消息做压测。看到推理Pod的数量在三秒内从4个升到8个的时候,我差点在工位上喊出来。
那天下午6点,我把这套KEDA配置部署到了生产环境。
为什么第一晚就挂:我把冷却时间设成了5分钟,而Spot节点平均存活55分钟
凌晨两点,我被PagerDuty的电话炸醒。监控面板上,API的P95延迟冲到了18秒,5xx错误率飙升到14%。我连滚带爬打开kubectl,发现推理Pod的数量只剩下2个,而RabbitMQ里积压了3000条消息。KEDA明明配了根据队列长度触发扩容,为什么不生效?(延伸阅读:我把代码重构的AI赌注押在JetBrains AI Assistant上:一个后端架构师的三个月实战复盘)
我翻了半天KEDA的日志才恍然大悟——问题不在KEDA,而在底层节点。那天晚上流量低谷,KEDA顺利地把Pod数从8个缩到了2个,但对应的节点并没有及时回收。Cluster Autoscaler检测到节点利用率低,开始驱逐Pod并关闭节点。但它的缩容稳定窗口默认是10分钟,加上实际的节点终止时间,整个过程花了将近20分钟。等早晨流量涌进来的时候,集群里只剩两台g5.12xlarge,但其中一台正处于正在被Terminate的状态——Pod已经被驱逐,新Pod却还没调度成功。KEDA再快也没用,没有GPU节点可以落脚。
那天早上我学到的最重要的一课是:KEDA解决的是应用层的弹性,但节点层的弹性必须跟应用层脱钩。如果你在AI推理场景下让Cluster Autoscaler自由发挥,它就会变成一个定时炸弹。
我做的第一个修正动作,是把KEDA的缩容冷却窗口从15分钟改成了2小时。这意味着推理Pod即使在深夜也不会轻易缩容,直到有充分的证据表明流量已经长时间处于低位。同时我给节点组加了保护性标签,禁止Cluster Autoscaler在晚上10点到早上6点之间执行缩容。这个改动立刻消除了夜间的不稳定,但成本优化空间也因此被压缩了。我知道这只是权宜之计,真正要省钱,还得在实例类型上动刀子。
第二轮优化:从PyTorch换到TensorRT-LLM,模型推理速度翻倍,但CUDA版本差点让我离职
构建TensorRT-LLM镜像的72小时噩梦
在稳定弹性之后,我开始盘算另一件事:能不能让单台GPU干更多的活?当时的推理引擎用的还是HuggingFace Transformers + PyTorch,一次只能处理一个请求,模型吞吐量大约每秒生成15个token。换成TensorRT-LLM或者vLLM这类专门的推理引擎,可以启用continuous batching和KV cache优化,理论吞吐量能提升2-3倍。
我选了TensorRT-LLM,因为我们团队对NVIDIA生态更熟悉。接下来就是构建Docker镜像。我打开Cursor,新建了一个Dockerfile,开头写了句:
# Build TRT-LLM image for Llama 3.1 70B, CUDA 12.5, TensorRT 10.0
Cursor给出了一个看起来很完美的Dockerfile,基于nvcr.io/nvidia/tensorrt:24.07-py3。结果构建过程在前10分钟一切正常,直到开始编译模型引擎的时候,报了一个CUDA driver version insufficient的错误。我查了半小时才确认,我们EKS节点上的NVIDIA GPU驱动版本是535.x,而24.07镜像需要545.x以上的驱动。如果手动升级驱动,需要走一遍GPU Operator的重新部署流程,风险巨大。最后我退回到了nvcr.io/nvidia/tensorrt:24.03-py3镜像,它兼容535驱动,但TensorRT版本降到了9.3,一些新的量化特性用不了。
镜像终于跑通之后,又遇到了ONNX转换失败的问题。Llama 3.1的某些attention层在从PyTorch转ONNX时触发了一个已知的bug,需要手动修改模型的config.json。我在Cursor里输入了那段修复脚本,AI帮我生成了完整的Python工具代码,直接内嵌进Dockerfile的RUN指令里。折腾了整整三个晚上,这个TRT-LLM镜像才真正可用。
性能提升对比:同样的g5.12xlarge,从每秒15 token到40 token
为了让你直观地看到收益,我把切换前后在单台g5.12xlarge实例上的压测数据做了个表格:
| 推理引擎 | 吞吐量 (tokens/s) | P99延迟 (s) | 最大并发数 | 显存占用 (GB) |
|---|---|---|---|---|
| PyTorch + HF Transformers (单流) | 15.2 | 8.7 | 1 | 22.3 |
| vLLM 0.4.2 (continuous batching) | 38.7 | 3.2 | 16 | 21.8 |
| TensorRT-LLM 9.3 (dynamic batching) | 40.1 | 2.9 | 20 | 20.5 |
切换到TensorRT-LLM之后,单GPU的峰值吞吐量翻了2.6倍。这意味着我维持相同服务水平所需的GPU节点数,从之前的8-10台直接降到了3-4台。这一下就把计算资源的成本砍掉了超过60%。CFO看到第二个月的账单后,终于把邮件的语气从“我们谈谈”变成了“继续保持”。(延伸阅读:Google ADK这把轻量级快刀,正在切开LangGraph没啃下的审批流骨头)
不过代价也是明显的:TensorRT-LLM的引擎构建时间极长,每次更新模型权重都需要重新build引擎,耗时40分钟起步。如果频繁迭代模型,这个延迟会严重影响CI/CD流水线。最后我不得不额外搭了一套“引擎预构建”的异步流程,让模型训练团队在S3上放置新权重时触发一个Lambda,自动在GPU实例上构建引擎并缓存到EFS上。这个Lambda脚本同样是用Cursor写的,省了我起码三个小时查API文档的时间。
第三轮优化:混合实例与Spot劫持,CFO终于露出了笑容
我写了个Spot中断预处理器,让Pod在2分钟宽限期内优雅退场
即使节点数从8台降到了4台,按需g5.12xlarge的月费用仍然在1.9万美元左右。要再往下压,唯一的出路就是利用Spot实例。AWS Spot实例的价格通常只有按需的30%-40%,但随时可能在两分钟内被回收。对于推理服务来说,2分钟的中断预警足够让现有请求完成并优雅退出,前提是你得正确捕获中断信号。
我用Cursor写了一个部署在DaemonSet里的Python脚本,核心逻辑是每5秒查询一次Spot实例的元数据端点(169.254.169.254/latest/meta-data/spot/termination-time),如果发现即将被回收,就自动执行kubectl drain本节点,然后在所有推理Pod上触发graceful shutdown——也就是给每个Pod发送SIGTERM,等待当前推理请求完成后再终止进程。为了确保万无一失,我还让Cursor帮我生成了一个Kubernetes Job,在节点被完全清空后自动向Slack发送通知。
下面是这个Spot预处理器脚本的核心部分(完整版可以直接部署):
#!/usr/bin/env python3
# spot_handler.py - 部署为DaemonSet,处理Spot中断
import time
import os
import signal
import requests
import subprocess
from kubernetes import client, config
def get_spot_termination_time():
try:
resp = requests.get(
"http://169.254.169.254/latest/meta-data/spot/termination-time",
timeout=2
)
if resp.status_code == 200:
return resp.text.strip()
except Exception:
pass
return None
def drain_node(node_name):
config.load_incluster_config()
v1 = client.CoreV1Api()
# 将节点标记为不可调度
v1.patch_node(node_name, {"spec": {"unschedulable": True}})
# 驱逐所有Pod,并给每个Pod 60秒优雅终止时间
subprocess.run([
"kubectl", "drain", node_name,
"--ignore-daemonsets",
"--delete-emptydir-data",
"--grace-period=60",
"--timeout=120s"
])
# 发送Slack通知
subprocess.run([
"curl", "-X", "POST", os.environ['SLACK_WEBHOOK_URL'],
"-H", "Content-Type: application/json",
"-d", f'{{"text": "Spot node {node_name} drained."}}'
])
if __name__ == "__main__":
node_name = os.environ['NODE_NAME']
while True:
if get_spot_termination_time():
drain_node(node_name)
# 执行完排水后,发送停止信号给自己,让DaemonSet的Pod退出
os.kill(os.getpid(), signal.SIGTERM)
time.sleep(5)
这个脚本部署之后,Spot节点在回收窗口内能保证不再接受新请求,并把正在处理的请求全部执行完。实测下来,我们再也没有因为Spot中断而导致客户端收到5xx错误。
实例类型混搭策略:为什么我用60% Spot + 30% 按需 + 10% 预留的组合
接下来就是实例类型的组合分配。我把EKS集群的GPU节点组拆成了三个,采用Terraform管理(后面会给出完整模块代码)。最终的分配策略是这样的:
| 节点组类型 | 比例 | 实例类型 | 费用折扣 | 风险 |
|---|---|---|---|---|
| 预留实例 | 10% | g5.12xlarge | ~40% off | 一年承诺,不可弹性;适合基线负载 |
| 按需实例 | 30% | g5.12xlarge | 0% | 无中断,应付可预测的常规高峰 |
| Spot实例 | 60% | g5.12xlarge / g5.8xlarge | 60%-70% off | 中断风险,靠Spot handler和KEDA容错 |
预留实例覆盖每天夜间的最低基线负载(2个Pod),确保最核心的服务永不断线。按需实例处理白天常规业务高峰,保证服务质量始终在SLA内。Spot实例则吃下所有剩余的弹性流量,由KEDA根据队列深度快速拉起,并靠中断预处理器应对回收。如果Spot实例大量被中断,KEDA会立刻触发按需实例的临时扩容(我们在KEDA里设了最高优先级trigger,当Spot节点不可用时自动切换到按需节点组)。
这套组合拳打下来,GPU节点的综合有效成本降低了68%,而可用性从99.5%反而提升到了99.92%。
实施案例全记录:从架构到代码,你能直接抄走的完整方案
Terraform模块:一条命令搭出整个推理集群
我用Cursor写了整套Terraform模块,把EKS集群、混合节点组、KEDA、GPU Operator、RabbitMQ全部打包在一起。下面是最核心的节点组定义片段,展示了如何用一个模块创建三个不同的节点组,并给它们打上对应的污点和标签,让KEDA和调度器正确识别:(延伸阅读:我往 Gemini 1.5 Pro 里塞了 5 万行代码,它给我画了张循环依赖图,还顺手把重构 diff 写好了——但我差点被账单送走)
# main.tf - EKS混合GPU节点组
module "eks_gpu" {
source = "terraform-aws-modules/eks/aws"
version = "19.21.0"
cluster_name = "inference-cluster"
cluster_version = "1.29"
# 预留实例节点组(基线)
node_groups = {
gpu-reserved = {
desired_capacity = 2
max_capacity = 2
instance_types = ["g5.12xlarge"]
capacity_type = "RESERVED"
labels = {
"node-type" = "gpu-reserved"
}
taints = []
}
}
# 按需实例节点组(常规高峰)
eks_managed_node_groups = {
gpu-ondemand = {
desired_capacity = 1
max_capacity = 4
instance_types = ["g5.12xlarge"]
capacity_type = "ON_DEMAND"
labels = {
"node-type" = "gpu-ondemand"
}
taints = []
}
}
# Spot实例节点组(弹性)
self_managed_node_groups = {
gpu-spot = {
node_group_name = "gpu-spot"
launch_template_name = "gpu-spot-lt"
max_size = 8
desired_size = 2
instance_type = "g5.12xlarge"
capacity_type = "SPOT"
labels = {
"node-type" = "gpu-spot"
}
taints = [{
key = "spot"
value = "true"
effect = "NO_SCHEDULE"
}]
}
}
}
你只需要修改capacity和instance_types参数,其他部分可以直接搬。我用这个模板在一个全新的AWS账号下从零搭出完整集群,只用了不到40分钟——其中25分钟是EKS本身的创建等待时间。
KEDA配置与队列选择:RabbitMQ还是Redis?
在队列选择上,我最初用了RabbitMQ,因为它有成熟的KEDA scaler,消息确认机制也很适合推理请求。但后来随着并发量上升,RabbitMQ的线程模型在高吞吐下开始出现瓶颈,单节点内存占用超过了8GB。我最终把队列切换成了Redis Streams,并用Cursor生成了对应的ScaledObject配置:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: inference-scaler
spec:
scaleTargetRef:
name: llama-70b-deployment
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: redis-streams
metadata:
address: "redis-streams.redis.svc.cluster.local:6379"
stream: "inference-requests"
consumerGroup: "inference-group"
pendingEntriesCount: "2"
lagCount: "5"
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 7200
policies:
- type: Pods
value: 1
periodSeconds: 300
scaleUp:
policies:
- type: Pods
value: 3
periodSeconds: 30
这里我把pendingEntriesCount和lagCount都设得比较保守,确保只在真正积压时扩容。缩容窗口是2小时,避免夜间抖动。这个配置是我反复调整了十几轮参数后定下来的,你可以根据自己的负载特性微调。
FinOps脚本:每天自动生成GPU成本报表,让CFO不用再失眠
最终,为了彻底告别手动拉Cost Explorer的噩梦,我用Cursor写了一个完整可运行的Python脚本,每天凌晨通过ECS Scheduled Task执行,自动拉取前一天的GPU实例费用并按标签分组,输出CSV并发送到CFO的邮箱。脚本用了boto3的ce客户端,处理了分页和标签过滤,并且过滤掉了那些还没出账的估算数据。下面是核心代码:
#!/usr/bin/env python3
# daily_gpu_cost_report.py - 每天生成GPU实例成本报表
import boto3
import csv
import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
def get_gpu_cost(start_date, end_date):
ce = boto3.client('ce', region_name='us-east-1')
results = []
next_token = None
while True:
kwargs = {
'TimePeriod': {'Start': start_date, 'End': end_date},
'Granularity': 'DAILY',
'Metrics': ['UnblendedCost'],
'GroupBy': [{'Type': 'TAG', 'Key': 'node-type'}],
'Filter': {
'And': [
{'Dimensions': {'Key': 'INSTANCE_TYPE',
'Values': ['g5.12xlarge', 'g5.8xlarge']}},
{'Tags': {'Key': 'CostCenter', 'Values': ['ai-inference']}}
]
}
}
if next_token:
kwargs['NextPageToken'] = next_token
resp = ce.get_cost_and_usage(**kwargs)
for time_period in resp['ResultsByTime']:
for group in time_period['Groups']:
tag = group['Keys'][0]
cost = float(group['Metrics']['UnblendedCost']['Amount'])
results.append({
'date': time_period['TimePeriod']['Start'],
'node_type': tag.split('$')[-1],
'cost': cost
})
next_token = resp.get('NextPageToken')
if not next_token:
break
return results
def send_report(csv_content, recipient):
# 使用SMTP发送邮件,这里略去具体实现,可以替换成SES
pass
if __name__ == '__main__':
yesterday = (datetime.date.today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d')
today = datetime.date.today().strftime('%Y-%m-%d')
costs = get_gpu_cost(yesterday, today)
csv_str = "date,node_type,costn"
for c in costs:
csv_str += f"{c['date']},{c['node_type']},{c['cost']:.2f}n"
send_report(csv_str, "cfo@ourcompany.com")
print("Report sent.")
脚本依赖boto3和AWS凭证,部署到ECS上只需要一个Docker镜像。这个脚本成了CFO每周三晨会必提的明星工具。
避坑清单:我从两次生产事故中捡回来的经验
最后,把这三个月的血泪教训浓缩成一份清单。每一条都是我踩过的坑,有些甚至直接导致了两次生产环境故障。
- 别让Cluster Autoscaler和KEDA同时控制同一组节点。两者打架的后果是节点被反复创建和销毁,成本反而更高,而且Pod会被频繁驱逐。
- KEDA的缩容稳定窗口至少设2小时,如果你用Spot实例,甚至可以考虑关掉缩容。GPU节点的冷启动代价太大,宁可让两个节点空转,也别在流量刚起来的时候因为节点启动延迟而触发告警。
- Spot中断的2分钟窗口,要留给请求完成,而不是留给Pod重新调度。我在第一次部署时,让Pod直接硬性终止,结果丢了几十个请求。后来改成先排空连接再退出,中断导致的失败数直接降到零。
- TensorRT-LLM的镜像必须跟节点GPU驱动版本严格对齐。别想当然地用nvcr.io的最新tag。先用nvidia-smi确认生产节点的驱动版本,再在NVIDIA GPU Cloud上查阅对应版本的镜像标签。
- 推理引擎的预热是必须的。模型引擎构建完后,最好用一个Init Container跑几轮dry-run推理,把KV cache热身,否则上线的前五分钟P99延迟会很高。
- 混合实例组的优先级一定要通过Kubernetes调度策略控制。比如给Spot节点打污点,让Pod默认不调度,只有通过KEDA的特定trigger(带tolerations)才能调度到上面。否则可能出现生产流量打到即将中断的Spot节点上。
- 成本监控脚本一定要过滤未出账数据。AWS Cost Explorer的延迟可能导致当天的成本为0,如果不处理,报表会显示“花费降低100%”的假象,让团队误以为一切正常。
- 用AI写基础设施代码可以提速10倍,但你必须亲自检查每一个调度相关的参数。Cursor给我的ScaledObject里,第一次补全的queueLength阈值、冷却窗口、maxReplicaCount全错了。如果不是我一行行过了一遍,那个YAML直接上生产可能就不是多花几千刀的事了。
总结:AI时代的云优化,不再只是运维的事
回顾这三个月的经历,我最深的感触是:在AI推理这种高波动、高成本、高复杂度的负载下,云基础设施优化已经从传统的“运维部门职责”变成了“应用开发者的核心能力”。因为只有写业务代码的人,才最清楚请求的到达模式、模型的耗时特性、以及什么样的性能指标才是业务真正关心的。
与此同时,AI编程工具彻底改变了我的工作流。我没有花大量时间翻阅KEDA的文档或是boto3的API参考,而是用Cursor把想法直接翻译成代码,然后集中精力去打磨调度策略和参数。但这也意味着,开发者必须比以往更懂基础设施——AI能写的只是代码,不能替你决定在哪个层级做弹性、用哪种实例、以及容忍多少冷启动时间。
如果你也在跑AI推理服务,并且正被云账单搞得焦头烂额,我的建议是:别把优化当成一次性项目。把成本仪表盘接到每天的站会上,把扩缩容策略当作代码的一部分来迭代,把你对业务的直觉变成配置里的一个阈值。还有,让AI帮你写代码,但别让它替你思考。