我让Cursor写了一套KEDA规则和Spot切换器,推理成本从8万暴跌到1.7万——但挂了两次生产

今年三月份,我坐在工位上打开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帮你写代码,但别让它替你思考。

✨ 本文由 AI 辅助生成(作者人设:林默),已经自动化事实核查流程处理,但仍可能存在不准确之处,具体信息请以官方文档为准。

觉得有用?

零垃圾邮件 · 随时退订

林默

全栈开发者,写了8年代码,从jQuery时代一路写到AI Copilot。目前专注AI编程工具链的深度使用和评测,相信好的工具能让开发者事半功倍。喜欢用实际项目验证技术方案,不写没踩过坑的教程。

📖 系列文章:AI 编程工具链实战

Claude Code / Copilot 深度评测与 CI/CD 集成

  1. 我把Claude Code塞进CI管道的那天,团队以为我要删库跑路——现在他们求着我别停
  2. 当SonarQube还在报误报时,我的Copilot Action已经修好了三个SQL注入——一条自动审查流水线的拆解
  3. 我给Copilot Code Review喂了团队过去一年的全部PR,它挖出的硬编码密钥让我后背发凉
  4. 我推Copilot推了半年,技术问题全是小意思,人的问题差点把我逼疯
  5. 我花了六周给AI Copilot绑上心率带,发现它正在偷走我的深度思考
  6. Docker容器化部署完全指南:从零到生产环境(实战经验总结)
  7. AI编程助手:2026年的发展趋势
  8. 代码重构实战经验
  9. 我用VS Code的3年血泪史:从菜鸟到高手的蜕变之路
  10. AI Coding工作流优化:Prompt工程与高效协作技巧
  11. Prompt写得好不好,AI代码质量差了一倍:我用Claude 4重构电商推荐系统的血泪史
  12. 把Claude API塞进CI流水线后,代码评审效率直接翻了3倍
  13. AI编程调试实战:我如何用智能工具把Bug定位时间从3天缩短到2小时
  14. AI辅助代码重构:我把10年老代码从3小时改写到15分钟的血泪史
  15. Tech Future主题开发完整教程 Part 2: 样式系统 - 我如何在3天内重构出可维护的CSS架构
  16. AI代码生成实战:我把业务逻辑开发从3天压缩到4小时,代价是200次调试
  17. 从Cursor切到Windsurf后,我的AI编程效率提升了47%但调试时间翻倍
  18. AI代码补全实战:我测了5个真实场景,结果好坏参半
  19. Claude Code Team Work的协同陷阱:我如何把Agent失忆率从40%干到3%
  20. 让AI帮我重构2000行遗留代码:从3小时到15分钟的代价
  21. 从Cursor切到Windsurf后,我的开发效率提升了47%,但调试时间翻了一倍
  22. 用Claude Code重构2000行烂代码:我的血压和代码质量一起飙升了
  23. 让AI重构2000行“屎山”:代码量减半,性能提升40%,但我掉了两周头发
  24. AI辅助代码重构:拆一个日处理10万单的PHP订单模块,测试覆盖率从12%到78%,但差点炸了对账
  25. 砸了用了三年的CI流水线后,我用AI重构了从编码到部署的每一步,效率翻了3倍
  26. AI代码审查流水线实战:Code Review时间从4小时压到20分钟,但Bug率反而上升了12%
  27. 别高估LLM的品味,它闻得到代码腐烂,但分不清脚气和坏疽——我在重构流水线里加了三道安全阀
  28. Cursor Agent 能帮你重构整个项目,也能趁你不注意删掉支付回调——我的三周踩坑实录
  29. 云IDE+AI原生不是换工具,是拆了10人团队重来
  30. 我把Vercel AI SDK 3.0的streamUI接进项目后,React组件像有了生命一样逐行“长”出来——这是我今年最接近魔法的一次
  31. 我推演了Devin的内部循环,发现它根本不是个IDE插件,而是一个带壳的操作系统
  32. 我在WPF病历系统里塞了一个本地Copilot微服务,结果异步死锁让我想删库跑路
  33. 为什么Cursor 0.46的Agent终端让我重写了安全审计清单——内核沙箱、cgroup v2与Seccomp的三层防线拆解
  34. 我半夜把Copilot Runtime塞进Surface Pro,NPU推理快得离谱,但矢量搜索差点让我把机器砸了
  35. 我在Amazon Q和Copilot之间反复横跳30天,发现自己不是在换工具,是在赌AWS的下一手棋
  36. VS Code这AI代码解释器,我调了半年才敢把它塞进CI流水线
  37. 用Codestral Mamba重构遗留系统,比Copilot快3倍的爽感,差点毁在一次上下文崩溃上
  38. 我把代码重构的AI赌注押在JetBrains AI Assistant上:一个后端架构师的三个月实战复盘
  39. 我把单元测试覆盖率从12%拉到87%,但AI第一次生成的Mock直接干穿了生产库
  40. 我往 Gemini 1.5 Pro 里塞了 5 万行代码,它给我画了张循环依赖图,还顺手把重构 diff 写好了——但我差点被账单送走(2024)
  41. ▸ 我让Cursor写了一套KEDA规则和Spot切换器,推理成本从8万暴跌到1.7万——但挂了两次生产
  42. 多智能体审批的“三体难题”:我在LangGraph、CrewAI和ADK上重构分布式事务的160小时,以及为什么Saga模式是唯一解
  43. 我用Copilot Agent给10万行Java单体画了张依赖图,生成的拆分方案差点让CTO以为我通宵了三个月
  44. GitHub把Copilot塞进Xcode,苹果的封闭花园终于开了一道门缝
  45. Vite 6.0迁移Rolldown翻车实录:快是真的快,坑也是真的深
  46. 我的工厂AI质检系统用Rust 1.85异步闭包重构后,消息积压从20分钟降到2分钟(2025)
  47. JetBrains AI Assistant实测:在单体工程里,它比Copilot更懂你的架构意图
  48. VS Code 1.95 AI代码审查:从理论到实践的跨越
  49. 我让Copilot Agent单挑了一个4年前的数据库竞态bug——账面省下$37,000人力成本,但我开始焦虑Agent的定价陷阱
  50. 我把一个27万行的monorepo从Webpack切到Vite 6.0 Rolldown,CI构建从8分钟掉到了42秒
  51. Copilot Chat免费了,我让我妈试了试自然语言编程,然后她真写出个网页来
  52. 我让5个iOS开发者用Copilot for Xcode跑了两周,他们写Swift 6的效率涨了34%,但隐性成本比想象中高
  53. 我让Copilot for Azure管了三个月云服务器,省下$14,700,但也差点把生产配置搞丢
  54. Code Llama 70B离Copilot杀手还有多远?我在A100上跑了三周,得出了几个残酷结论
  55. 救命,Rust 1.85的异步闭包让我把1200行砍到200行,编译器再也不骂人了(2025)
  56. 我把Copilot Agent塞进真实项目,它自己把Bug给修了——但这盘棋GitHub还没下完
  57. GitHub Copilot Chat的上下文感知就像论文里的RepoCoder,但生产环境里它用了一套让索引工程师沉默的捷径
  58. Copilot for Azure省下了$21,000,我却连夜删掉了它的“闲置回收”自动化——一个5年投资顾问的技术账
  59. 我把汽车零部件厂的质检系统升级Next.js 15:构建从55秒降到4秒,但一次路由缓存失误差点引发批量召回
  60. Vercel AI SDK 3.0 这一步棋,下在了所有 LLM 应用开发者的心坎上
  61. 微软在VS Code里埋了颗规则引擎的种子,SonarLint该紧张了
  62. Vite 6 的 Rolldown 还没正式发布,我们已经在工厂的 12 个前端项目上把冷启动砍到 230ms,但第一天就翻了车
  63. 我评估Copilot for Azure的降本ROI:每月省下$2100的真实案例背后,认知偏差差点让一个集群宕机——投资顾问的技术账
  64. 我们把工厂20个前端项目的Webpack全下了,构建从8分钟掉到11秒,但Rolldown的一个动态导入bug差点让质检停了4小时
  65. 我让Copilot Workspace把整个JWT认证模块重写了,PR通过只花了3轮——但监控没跟上差点又半夜被叫醒
  66. Cursor Agent把我从CRUD里开除了:一行命令生成API,测试自己写自己修,人工干预0次
  67. 我让Copilot里三个模型轮番写SQL,结果Gemini差点让我半夜被客户电话轰炸,现在我把默认锁死在Claude 3.7 Sonnet(2024)
  68. Amazon Q的代码补全抄了ACL那篇RepoCoder的作业,但运维时它忘了一半——我实测了一整个订单微服务周期
  69. Copilot多模型切换评测:我拿三个模型轮番干了6件事,差点删库跑路,最后我选了它
  70. 开发服务器启动2.1秒,生产构建却卡了我26秒——Next.js 15升级的72小时硬件实测
  71. VS Code的本地AI重命名,是微软写给合规部门的一封密信
  72. 用Fleet AI和上海的同事结对写预测维护代码,省了120小时,但第一天就让工厂停了4小时
  73. Meta 的 Toolformer 论文让我对工具调用充满幻想,直到我用 Vercel AI SDK 3.0 在流式UI上连栽三个跟头
  74. Cursor Teams的代码审查不是更快,而是把老手30%的精力变成了团队的肌肉记忆——我配置完自动化流水线后,新人的PR三天没被我打回去一次
  75. 我把截图丢给Copilot X,张嘴说几句需求,代码直接出来了?爽了一周后,它偷偷改了我的配置文件,差点让我删库跑路
  76. 为什么我最终选择了Mistral Codestral Mamba:256K超长上下文代码生成模型的架构决策
  77. 我喂了Claude 4.8整个Spring Boot仓库,现在它比我还懂我的数据库事务
  78. 我用Copilot X踩坑实录:截图+语音直接生成代码,差点把项目整废了!
  79. 我们给工厂喂了OpenAI o1,结果它把数百万条传感器数据跑崩了:慢思考在工业代码里的真实边界
  80. 我让 GitHub Copilot Workspace 写完了整个项目,结果它差点把我的生产库干废
  81. 别再只盯着代码补全了,Cursor 2.0 这一步棋,下在了“架构师”位置上
  82. 我用 VS Code Copilot 调试助手写代码,再也不怕逻辑炸锅了
  83. Cursor 2.0 团队版:AI 审查不是替代人类,而是把老手30%的精力变成了团队的肌肉记忆
  84. 别再只盯着代码补全了,GitHub Copilot Workspace 这一步棋,下在了“项目经理”位置上
  85. 我让 Vercel v0 一晚上搭完了一个暗黑模式 Dashboard,代码量比以前少了一半
  86. OpenAI o1 暴力破解数学与代码(2024):我为什么在架构里砍掉 GPT-4o 的计算资源
  87. 我们砍掉了 60% 的云账单,但差点把 CI/CD 管道炸了:FinOps 2.0 与 Spot 实例实战复盘
  88. Cursor 2.0 VS VS Code Copilot:AI原生编辑器在多文件重构与上下文理解上的代际差异
  89. Cursor 2.0 VS VS Code Copilot:从概率补全到意图执行,我为什么把架构重构工具换成了Cursor
  90. OpenAI o1 那篇关于思维链的论文里说能解决数学题,但在我重构遗留代码库时,它只会把逻辑搞乱
  91. Cursor 2.0 炸了我的生产环境,VS Code 1.91 救了我?从 AI 原生到 AI 增强的代际差异
  92. OpenAI o1 那篇关于“推理时间缩放”的论文里说能解决数学题,但在我重构遗留代码库时,它只会把逻辑搞乱
  93. 这个坑我踩了三个月,GitHub Copilot Workspace差点让我从独立开发者变成摆烂摸鱼艺术家
  94. Cursor 1.0 深度评测:当 IDE 拥有了‘上帝视角’,AI 原生编辑器如何颠覆 VS Code?
  95. 我们用AI Agent重构了汽车零件厂的质检线,ROI是预期外的
  96. 这个坑我踩了三天,GitHub Copilot Workspace差点让我从独立开发者变成摆烂摸鱼艺术家
  97. Cursor 1.0+ 与 GPT-5.5 时代的 CRUD 终结者:初级开发者如何从代码搬运工进化为系统架构师
  98. Cursor 1.0+ 与 GPT-5.5:CRUD 开发正在变成“系统审查”,初级开发者如何从代码搬运工进化为架构师
  99. Cursor 1.0 暴力重构我的上下文窗口:从边缘推理到 IDE 架构师,我的技能树重构手记
  100. VS Code 1.70 深度评测(2022):官方 AI 助手与 Copilot 的博弈,谁才是 IDE 的未来?
  101. CRUD 开发正在变成“系统审查”:Cursor 与 GPT-5.5 的架构博弈与初级开发者的生死线
  102. 别再手动切代码了(2024):Claude 3.5 Artifacts 让我在浏览器里直接“画”出了 UI
  103. 别再跟Tailwind Class较劲了:v0是如何把“写代码”变成“写文案”的
  104. Cursor 2.0 炸了我的工作流:从马尔可夫补全到图状推理,AI 原生 IDE 的架构代差
  105. Vercel v0:为什么说 AI 编程的拐点已经来了
  106. Vercel v0:当 AI 把 Tailwind Class 写成了诗歌,前端开发者的“造物主”游戏结束了
  107. 凌晨三点被报警叫醒的教训:Vercel v0深度实战,AI原生开发如何重塑我的前端工作流
  108. 回到2022:VS Code 1.70 与早期 Copilot 插件的体验回顾
  109. 为什么说 GitHub Copilot Chat 正在改写开发者与代码的交互棋局
  110. 别让AI生成的代码在K8s里跑了:Vercel v0实战的血泪复盘
  111. 我用 Cursor 2.0 重构了 50 万行代码库:从马尔可夫链到图神经网络
  112. 我用 AWS 新一代云服务器实例重构了整个 AI 开发环境:成本与性能的完美平衡
  113. Cursor 2.0 团队版:AI 审查如何改写团队协作棋局
  114. 别再手动装Python了:我用Docker重构了我的AI开发地狱,GPT-5.5跑在RTX 5090上
  115. 这个坑我踩了半年,GitHub Copilot X 让我怀疑人生——AI编程的未来到底在哪儿
  116. 我用AWS新云服务重构了AI处理架构,成本砍了60%
  117. 我把5万份代码文件一次性塞给Gemini 2.5 Pro,它反手揪出21个循环依赖,还差点把我忽悠瘸了
  118. Cursor 2.0:我用它重构了50万行代码库,但也踩了两个大坑
  119. 我用Cursor写了一周代码后,AI Agent彻底改变了我的职业轨迹
  120. 为什么说AI编程的拐点已经来了:GitHub Copilot与Cursor的新功能对比深度分析
  121. 凌晨三点被报警叫醒的教训:VS Code 官方 AI 助手深度实战与本地化部署冲击
  122. 这个工具救了我的命,但这个 Bug 让我心态崩了:V0 前端开发实录
  123. Cursor 1.0:AI 编程的范式变革与架构挑战
  124. AI 编程工具的冲击:初级开发者如何从“代码搬运工”进化为“架构师”
  125. 我用VS Code Copilot X重构了50万行代码库,但也踩了两个大坑
  126. 讲真,这个AI编程助手Cursor救了我的命,但有个Bug让我心态崩了
  127. 凌晨三点被报警叫醒的教训:Cursor 2.0 DeepSeek 集成实战与成本对比
  128. 这个AI生成UI工具差点让我砍掉前端团队,后来我们发现了它的软肋
  129. 从系统架构视角审视 VS Code 1.90 AI 编辑器:性能、扩展性与实际落地挑战
  130. 这个AI编程助手差点让我砍掉前端团队,后来我们发现了它的软肋
  131. Llama 3 零运维成本部署:Serverless AI 推理实战与成本博弈
  132. 这个坑我踩了三天,Vercel v0 UI生成差点让我辞职
  133. GitHub Copilot 2.0:AI 编程的效率革命与多语言新战场
  134. Copilot X:重塑后端开发范式的AI工具革命
  135. 讲真,这个工具救了我的命:Cursor 1.0 发布,但我差点因为本地推理把它删了
  136. GPT-5.5 Instant 把我的思维链写成了代码:全栈开发者的推理幻觉实测
  137. 我的手指停止移动了:Cursor AI 编辑器实录,但我差点被幻觉坑死
  138. GitHub Copilot Workspace:AI 辅助编程工作流的架构抉择与落地实践
  139. Cursor 2.0 深度集成 DeepSeek:我把思维链塞进了编辑器,但监控差点没跟上
  140. VS Code 1.70 遗留架构复盘:当我在 2026 年重构旧调试链路时,为什么还要死磕当年的扩展上下文键
  141. AI编程的拐点:GitHub Copilot Workspace如何重塑开发者的角色
  142. Copilot 和 Copilot Workspace 的开发工作流革命:从代码补全到端到端自动化
  143. Cursor 1.0 这步棋,下在了“编辑器”而非“插件”上

发表评论