暗无天日

=============>DarkSun的个人博客

读:把成本当作 SLI

有一类故障很特殊:所有监控指标都是绿的,但钱在不停地流走。

生产事故有即时反馈——告警响了、错误率飙了、客户邮件来了。但成本问题是另一回事:你月底才看到账单,盯着一个数字琢磨这钱到底是三周前的哪个操作花的。因果链太长,追都追不回来。

David Iyanu Jonathan 最近在 DZone 上写了篇文章讨论这个问题。他的核心主张很简单:把 cost-per-request (每个请求的平均成本)当成 SLI(服务水平指标,Service Level Indicator),跟延迟、错误率一样接入监控告警体系。本文提炼原文的核心观点,加上国内云环境的对应工具和一份可以立刻动手的清单。

为什么成本问题不容易发现

两种故障的反馈延迟不同,这是根因。

可用性出问题,当场就知道。成本出问题,月底看账单才知道。而且账单是汇总数字,你得倒推:哪个服务?什么时候开始的?那次部署改了什么东西?是不是有人误删了索引导致全表扫描?因果链又长又模糊,查起来像是做财务审计,不是做故障排查。

原文举了一个例子。假设你的服务跑在 AWS Lambda(一种按调用次数和运行时长计费的无服务器计算服务)上,下游依赖开始返回 HTTP 429(表示"请求太频繁,我被限流了")。Lambda 函数捕获了这个错误,按指数退避重试。听着很正常。

问题出在参数上。退避参数是照着"依赖偶尔短暂抽风"配的,实际遇到的是"依赖在大规模限流"。这时候 jitter(退避时间的随机扰动,避免所有实例扎堆重试)不够大,多个实例退避到了相近的时间窗口,同时重试、同时被拒、再同时重试,形成了 retry storm(重试风暴)。

单次调用很便宜——几分之一美分,执行时间几十毫秒。但你现在同时跑着几千个实例,每个实例在退避等待期间仍然占用着内存,按 GB-秒(1 GB-秒就是分配了 1 GB 内存跑了 1 秒)计费。而且 Lambda 是无状态的,各实例之间没法共享"下游已经扛不住了"这个信息,AWS 看到的是需求旺盛、容量充足,继续全并发调用。

整个过程没有任何告警。错误率不算高,大部分请求后来都成功了,p99 延迟虽然上升但还在 SLO 范围内。问题在于这三小时的 Lambda 账单顶平时一周。

结论很简单:系统可以跑得正确,但跑得很贵。缺陷不以毫秒计,以人民币计。

钱具体漏在哪些地方

僵尸资源。 云环境里被遗忘的东西比你以为的多得多:

  • EBS 卷——实例销毁了,没勾"随实例删除",卷还在那挂着,继续计费
  • 弹性 IP——不关联实例的话每小时几分钱,单个不起眼。100 个闲置弹性 IP 跑一年就是四千多美元
  • NAT 网关——VPC 里的业务早就迁走了,网关没拆,因为 Terraform 配置没清理,而没人愿意碰 Terraform 的状态文件
  • RDS 快照——自动备份策略设得很勤快,但没人写清理脚本,快照只增不减
  • 空载负载均衡——后端没有健康实例了,ELB 还在那运行着,对着空气做健康检查

单个不致命,但月月叠加,每次做预算都发现基数比上回高一点,没人说得清为什么。

出口流量。 原文讲了个例子:一张 $240 万的云账单,80% 是数据出口费。架构师设计系统的时候想的是 CPU、内存、IOPS,没人把网络出站流量当计费项。但云厂商的收费模式一直是入口免费、出口收费。

弹性伸缩只扩不缩。 弹性伸缩的本意是跟着负载走,高负载扩、低负载缩。但实际配置经常是扩得猛、缩得保守。因为扩慢了影响可用性,会出事;缩快了也可能影响可用性,也会出事。工程师自然更怕出事——可用性故障立刻告警,成本故障月底才寄发票。

结果就是"棘轮效应":集群扩上去了,不缩回来。原文算了笔账:一个节点 $0.20/小时,跑 30 个节点实际只用到 12 个的容量,一年就多花近 $25,000。一个中型平台几十个服务加起来,浪费的钱够招好几个工程师。

解法:把成本当成监控指标

做法不复杂:像盯延迟一样盯成本。把 cost-per-request 拉出来,设基线,设告警,让它也能触发值班。

成本的数据原料其实都有:请求数有、执行时长有、内存配置有、出口字节有。大多数团队缺的不是数据,是把这些数据拼起来持续计算、然后接入告警管道的习惯。

原文给了一条 Prometheus 告警规则,就是最直接的做法:

- alert: CostPerRequestAnomaly
  expr: |
    (
      increase(cloud_spend_dollars_total{service="payment-processor"}[30m])
      /
      increase(http_requests_total{service="payment-processor"}[30m])
    ) > 0.02
  for: 15m
  labels:
    severity: warning
  annotations:
    summary: "Payment processor cost/request exceeding $0.02 threshold"
    runbook: "https://wiki.internal/runbooks/cost-anomaly"

这条规则的意思是:过去 30 分钟内,支付服务的每请求成本超过 $0.02 并且持续 15 分钟,就触发告警。实际落地需要处理几个边界:请求量降到零时分母为零怎么办、固定成本(跟请求量无关的部分)怎么单独核算、每个服务的阈值需要各自调。原则没有问题。

国内平台可以用阿里云的 cloud-exporter 把费用数据接入 Prometheus,或者直接用费用中心 API 拉数据写入自建监控。

在接好完整的 Prometheus 流水线之前,最基本的计算逻辑用一段 Shell 就能说明白:

#!/bin/bash
# cost-per-request-demo.sh —— 用模拟数据演示每请求成本的计算和异常检测
set -euo pipefail

# 模拟数据:每 5 分钟一个采样点(通常来自云厂商费用中心导出的 CSV)
cat > /tmp/cost_data.csv <<'CSVEOF'
timestamp,requests,cost
08:00,1000,12.50
08:05,1100,13.00
08:10,1050,12.80
08:15,1200,13.50
08:20,1150,13.20
08:25,1000,12.50
08:30,1300,35.00
08:35,1250,33.00
08:40,1350,36.50
08:45,1400,38.00
08:50,1200,32.00
08:55,1100,30.00
CSVEOF

BASELINE=0.012  # 历史基线:$0.012/request
THRESHOLD=$(awk "BEGIN {print $BASELINE * 2}")

echo "=== cost-per-request 监控演示 ==="
echo "基线: \$${BASELINE}/request,告警阈值: \$${THRESHOLD}/request(基线 × 2)"
echo ""

tail -n +2 /tmp/cost_data.csv | while IFS=, read -r ts req cost; do
  cpr=$(awk "BEGIN {printf \"%.4f\", $cost / $req}")
  if awk -v cpr="$cpr" -v thresh="$THRESHOLD" 'BEGIN {exit !(cpr > thresh)}'; then
    printf "  %s  req=%s  cost=\$%-6s  cpr=\$%s  ← 超过阈值!\n" "$ts" "$req" "$cost" "$cpr"
  else
    printf "  %s  req=%s  cost=\$%-6s  cpr=\$%s\n" "$ts" "$req" "$cost" "$cpr"
  fi
done

echo ""
echo "08:30 开始 cpr 翻倍。排查方向:"
echo "  1. 新部署是否引入了 N+1 查询"
echo "  2. 索引被误删导致全表扫描"
echo "  3. Serverless 函数是否有 retry storm"
echo "  4. 是否有人在跨 region 拉数据"
=== cost-per-request 监控演示 ===
基线: $0.012/request,告警阈值: $0.024/request(基线 × 2)

  08:00  req=1000  cost=$12.50   cpr=$0.0125
  08:05  req=1100  cost=$13.00   cpr=$0.0118
  08:10  req=1050  cost=$12.80   cpr=$0.0122
  08:15  req=1200  cost=$13.50   cpr=$0.0112
  08:20  req=1150  cost=$13.20   cpr=$0.0115
  08:25  req=1000  cost=$12.50   cpr=$0.0125
  08:30  req=1300  cost=$35.00   cpr=$0.0269  ← 超过阈值!
  08:35  req=1250  cost=$33.00   cpr=$0.0264  ← 超过阈值!
  08:40  req=1350  cost=$36.50   cpr=$0.0270  ← 超过阈值!
  08:45  req=1400  cost=$38.00   cpr=$0.0271  ← 超过阈值!
  08:50  req=1200  cost=$32.00   cpr=$0.0267  ← 超过阈值!
  08:55  req=1100  cost=$30.00   cpr=$0.0273  ← 超过阈值!

08:30 开始 cpr 翻倍。排查方向:
  1. 新部署是否引入了 N+1 查询
  2. 索引被误删导致全表扫描
  3. Serverless 函数是否有 retry storm
  4. 是否有人在跨 region 拉数据

实际使用时,把模拟数据换成云厂商账单 API 的返回值,加到 cron 里每 5 分钟跑一次。成本异常就能在第一个 30 分钟窗口内收到告警,不用等到月底对账。

可以立刻做的三件事

完整的 FinOps 转型要花几个季度,涉及跨部门协作,工程师团队推不动很正常。但下面三件本周就能做。

  1. 找出花钱最多的 3 个服务。 用 AWS Cost Explorer 按服务类型排序,或者阿里云费用中心按产品维度排。然后问自己:你知道每个服务的正常 cost-per-request 是多少吗?不知道的话,缺的不是"成本优化",是"成本可观测性"。先把指标加上、基线算出来、告警设在基线两倍。有数据才有调优。
  2. 跑一次闲置资源报告。 不光是清理,更要搞清楚这些资源是怎么产生的。谁建的?谁忘了?有没有项目下线的标准流程?资源标签是不是强制要求?闲置资源是表面,流程缺失是根。对应的修法:资源创建必须打标签(团队 / 环境 / 项目缺一个不许建)、超期未打标签的自动回收——不是发提醒,是直接停掉、推行 chargeback(让每个团队看到自己花了多少钱,不只是看到一份不痛不痒的统计报表)。
  3. 把账单异常当事故处理。 这件事需要跟管预算的人谈。不追责,不当财务审计做。就跟生产故障一样:写复盘、画时间线、找根因、定改进措施。账单飙升就是生产事故,只不过影响的是钱,不是用户。

收尾

原文结尾说得很直白:系统已经是分布式的,成本数据就在那里,差的就是成本可观测性这一块。现在去补。

请求数、执行时间、出口流量——这些数据本来就有。把它接进告警管道,让它跟延迟和错误率一样能触发值班。等到 CFO 来问的时候,你已经不用手忙脚乱翻账单了。

可观测性 : 成本优化 : 云原生 : SLA : 运维