云上省钱,别再从"找闲置"开始

举报
yd_286463689 发表于 2026/10/22 09:16:55 2026/10/22
【摘要】 资源层决定你能省多少,单位成本决定你该不该省。一套 200 行、可运行的云成本体检脚本,把账单、P95 指标和流量对齐成可执行的动作清单。

云上省钱,别再从"找闲置"开始

先算单位成本,再排优化动作。顺序反了,省下来的钱会以另一种方式花回去。

几乎每个季度的成本优化,都是从同一张表开始的:按资源类型排序,找出 CPU 5% 以下的机器、没人绑的 EIP、三个月没访问的存储桶。这条路没错,见效也快。

但它有个天花板。闲置资源是一次性的,清完就没了;而真正决定账单结构的,是"每个服务花多少钱、换来多少业务量"——也就是单位成本。

先说结论:资源层决定你能省多少,单位成本决定你该不该省。

下文代码里的账单和监控数据由脚本生成,用来说明判断逻辑;接真实数据时替换 generate() 即可。


一、成本优化分三层

层级 典型动作 见效周期 天花板 风险
资源层 释放闲置、降配、冷数据归档 天 低(一次性) 低
架构层 弹性伸缩、副本调整、缓存命中率 周 中 中
单位成本层 下线/合并低价值服务、改计费模型 月 高 高

大部分团队停在第一层,因为第一层的动作最明确、最容易汇报。但连续做几个季度就会发现:能清的都清完了,账单还是一条缓慢上升的曲线。


二、单位成本:一个除法,三个坑

公式很简单:

每千次请求成本 = 服务月成本 / (月请求数 / 1000)

坑一:分母选错。 网关适合用请求数,交易服务适合用订单数,内容服务适合用活跃用户。分母选错,结论会完全反过来。一个"每千次请求很贵"的服务,可能只是因为它每次请求干的活更重。

坑二:共享成本不摊。 网关、日志、监控、CI 这些成本如果不按规则摊到服务上,每个服务看起来都很便宜,加起来却对不上总账单。

坑三:只算资源不算人力。 一个单位成本很低、但每周要两个人维护的服务,真实成本可能更高。这一项很难量化,但决策时不能假装它不存在。

示例数据里有个很直观的对比:gateway 是 ¥0.32/kreq,recommend 是 ¥40/kreq,差 125 倍。后者不是"降一档规格"能解决的问题——降配最多省 35%,而真正该回答的是"这个服务还有没有人在用"。


三、三张表必须先对齐

要做归因,至少需要三份数据:

数据 关键字段 来源
账单 resource_id、规格、月成本、计费方式 成本中心导出
监控 CPU/内存 P95、连续低水位天数 CES / AOM
流量 请求数、QPS、最近访问时间 ELB 日志 / APM

对齐的键只有一个:resource_id。所以标签规范(service / env / owner)不是形式主义,它决定了你能不能把成本算到具体的人和具体的服务上。

没有这三个标签,后面的所有分析都会退化成"按资源类型看总额"——也就是本文开头那条路。


四、完整实现

逻辑很直白:逐条资源做规则判断,产出"动作 + 节省额 + 风险 + 依据";再按服务算单位成本,找出异常。

"""cost_review.py —— 云成本体检:先算单位成本,再排优化动作。

单文件、零依赖:
    py -3 cost_review.py

账单与监控数据由脚本生成,用于演示判断逻辑;接真实数据时替换 generate()。
"""
from __future__ import annotations

import statistics
from dataclasses import dataclass
from collections import defaultdict


@dataclass
class Resource:
    rid: str
    service: str
    env: str
    rtype: str          # ecs / rds / obs / eip
    spec: str
    cpu_p95: float
    mem_p95: float
    cost: float         # 月成本(元)
    requests: int = 0   # 月请求数
    storage_gb: int = 0
    idle_days: int = 0  # 连续无流量天数
    night_idle: bool = False


@dataclass
class Finding:
    action: str
    rid: str
    service: str
    reason: str
    saving: float
    risk: str           # 低 / 中 / 高
    confidence: float


def review(r: Resource) -> list:
    out = []
    if r.env != "prod" and r.cpu_p95 < 2 and r.requests == 0 and r.idle_days >= 14:
        out.append(Finding("释放闲置资源", r.rid, r.service,
                           f"{r.idle_days} 天无流量,CPU P95 {r.cpu_p95}%",
                           r.cost, "低", 0.95))
        return out

    if r.rtype in ("ecs", "rds") and r.cpu_p95 < 15 and r.mem_p95 < 35:
        out.append(Finding("降配超配实例", r.rid, r.service,
                           f"CPU P95 {r.cpu_p95}%、内存 P95 {r.mem_p95}%,长期低水位",
                           round(r.cost * 0.35, 0), "中", 0.8))

    if r.rtype == "ecs" and r.env != "prod" and r.night_idle:
        out.append(Finding("非生产夜间关机", r.rid, r.service,
                           "仅工作时间有流量,22:00-08:00 可停机",
                           round(r.cost * 0.3, 0), "低", 0.9))

    if r.rtype == "obs" and r.idle_days > 90:
        out.append(Finding("冷数据转归档", r.rid, r.service,
                           f"{r.idle_days} 天未访问,{r.storage_gb} GB",
                           round(r.cost * 0.6, 0), "低", 0.85))

    if r.rtype == "eip" and r.requests == 0:
        out.append(Finding("释放闲置 EIP", r.rid, r.service,
                           "未绑定任何实例", r.cost, "低", 0.95))

    return out


def unit_cost(rows):
    """按服务算每千次请求成本,只统计生产环境、有流量的服务。"""
    cost, req = defaultdict(float), defaultdict(int)
    for r in rows:
        if r.env != "prod":
            continue
        cost[r.service] += r.cost
        req[r.service] += r.requests
    return {
        s: cost[s] / (req[s] / 1000)
        for s in cost if req[s] > 0
    }


def report(rows):
    findings = [f for r in rows for f in review(r)]
    total = sum(r.cost for r in rows)
    saving = sum(f.saving for f in findings)

    print(f"月度总支出        ¥ {total:,.0f}")
    print(f"可优化金额        ¥ {saving:,.0f}({saving / total:.1%})")
    low = sum(f.saving for f in findings if f.risk == "低")
    print(f"低风险可立即执行  ¥ {low:,.0f}(占可优化金额 {low / saving:.0%})")
    print()

    uc = unit_cost(rows)
    median = statistics.median(uc.values())
    print(f"单位成本(每千次请求),中位数 ¥ {median:.2f}")
    for s, v in sorted(uc.items(), key=lambda kv: -kv[1]):
        flag = "  <-- 偏高" if v > median * 3 else ""
        print(f"  {s:<12}¥ {v:>7.2f} / kreq{flag}")
    print()

    print(f"{'动作':<12}{'项数':>4}{'月节省':>12}{'风险':>6}")
    print("-" * 40)
    by_action = defaultdict(list)
    for f in findings:
        by_action[f.action].append(f)
    for action, items in sorted(by_action.items(), key=lambda kv: -sum(f.saving for f in kv[1])):
        risks = {f.risk for f in items}
        risk = "低" if risks == {"低"} else "中"
        print(f"{action:<12}{len(items):>4}{sum(f.saving for f in items):>11,.0f}{risk:>5}")
    print()

    print("优先执行(按节省额 / 风险排序):")
    for f in sorted(findings, key=lambda x: (x.risk != "低", -x.saving))[:6]:
        print(f"  [{f.risk}风险] {f.action}  {f.rid}({f.service})  ¥ {f.saving:,.0f}/月")
        print(f"            依据:{f.reason}")


def generate():
    rows = []

    def add(prefix, service, env, rtype, spec, cpu, mem, cost, req=0, storage=0,
            idle=0, count=1, night_idle=False):
        for i in range(count):
            rows.append(Resource(f"{prefix}-{i + 1:02d}", service, env, rtype, spec,
                                 cpu, mem, cost, req // count, storage // count,
                                 idle, night_idle))

    add("ecs-order", "order", "prod", "ecs", "8c16g", 52, 68, 1600, 3_200_000, count=4)
    add("ecs-cart", "cart", "prod", "ecs", "4c8g", 38, 55, 850, 1_600_000, count=4)
    add("ecs-search", "search", "prod", "ecs", "8c32g", 61, 74, 2100, 900_000, count=3)
    add("ecs-gw", "gateway", "prod", "ecs", "4c8g", 44, 40, 900, 6_000_000, count=3)
    add("ecs-reco", "recommend", "prod", "ecs", "8c16g", 9, 31, 1600, 120_000, count=3)
    add("ecs-report", "report", "prod", "ecs", "4c8g", 12, 28, 850, 20_000, count=2)
    add("ecs-ml", "ml-train", "prod", "ecs", "16c64g", 78, 85, 4200, count=2)

    add("ecs-stg", "order", "staging", "ecs", "4c8g", 1, 3, 850, idle=21, count=4)
    add("ecs-dev", "cart", "dev", "ecs", "2c4g", 0.5, 2, 320, idle=30, count=6)
    add("ecs-test", "search", "test", "ecs", "4c8g", 18, 22, 850, count=2, night_idle=True)

    add("rds-order", "order", "prod", "rds", "8c32g", 55, 70, 2600, count=2)
    add("rds-stg", "order", "staging", "rds", "4c16g", 1.5, 8, 980, idle=18, count=1)
    add("rds-ro", "report", "prod", "rds", "4c16g", 6, 25, 1400, count=2)

    add("obs-log", "log", "prod", "obs", "-", 0, 0, 900, storage=12_000, idle=180, count=3)
    add("obs-asset", "asset", "prod", "obs", "-", 0, 0, 500, storage=3_000, idle=5, count=2)

    add("eip-idle", "gateway", "dev", "eip", "-", 0, 0, 385, count=2)
    add("eip-gw", "gateway", "prod", "eip", "-", 0, 0, 385, req=5_000_000, count=2)

    return rows


if __name__ == "__main__":
    report(generate())

跑出来的结果:

月度总支出        ¥ 54,940
可优化金额        ¥ 12,456(22.7%)
低风险可立即执行  ¥ 9,200(占可优化金额 74%)

单位成本(每千次请求),中位数 ¥ 5.31
  report      ¥  225.00 / kreq  <-- 偏高
  recommend   ¥   40.00 / kreq  <-- 偏高
  search      ¥    7.00 / kreq
  order       ¥    3.62 / kreq
  cart        ¥    2.12 / kreq
  gateway     ¥    0.32 / kreq

动作            项数         月节省    风险
----------------------------------------
释放闲置资源        11      6,300    低
降配超配实例         7      3,256    中
冷数据转归档         3      1,620    低
释放闲置 EIP       2        770    低
非生产夜间关机        2        510    低

三个观察,比总数更重要:

1. 一半的节省额来自一次性动作。 11 项闲置资源贡献 6300,占可优化金额的 50.6%。但清完之后,下个季度不会再有了。把它当成"季度奖金",别当成"持续收益"。

2. 降配是持续性的,但风险等级不同。 7 项超配合计 3256/月,每个月都能省;代价是要承担性能风险,必须灰度,不能批量执行。

3. 单位成本异常的两个服务,节省额加起来不如一次降配,但它们指向的问题更大。 report 是 ¥225/kreq,recommend 是 ¥40/kreq,而中位数只有 ¥5.31。这两个数字不该直接变成"降配工单",而应该变成一个提问:这两个服务的业务价值,配得上这个成本吗?


五、执行顺序:按风险,不按金额

排序很简单,但很多人会按节省额从大到小做,结果第一批就踩了中风险动作。

低风险,直接做。 释放闲置、冷数据归档、闲置 EIP。前提是两步确认:释放前打快照,或确认没有任何绑定关系。这两步不能省。

中风险,先灰度。 降配一次只动一台,观察 7 天的 P95、错误率和下游依赖,再推全量。特别是有状态服务,降内存比降 CPU 危险得多。

高风险,先出方案。 降副本、换计费方式、下线服务——这些不该在季度末冲指标的时候做。指标压力会让你跳过验证。


六、三个反模式

1. 看均值降配。 平均值会掩盖尖峰:一台 8 核机器日均 10%,但每天 10 点冲到 90%,按均值降到 2 核,第二天就出事。所有降配判断都应该看 P95 或 P99。

2. 砍日志、备份、快照。 这类成本在账单上很显眼,省起来也快,但它们是事故时的唯一证据链。要优化就做生命周期分层,不要直接关掉。

3. 先买包年。 归因还没做清楚就锁定一到三年,等于把未来的优化空间提前卖掉。包年适合"用量稳定且已确认长期存在"的负载,不适合"还没想清楚要不要继续做"的服务。


七、落到华为云上

  • 分账:成本中心按企业项目或标签出账,先把 service / env / owner 三个标签补齐,再谈归因。
  • 指标:CES 取 P95 和连续低水位天数;应用侧指标用 AOM 补,避免只看基础设施。
  • 存储:OBS 生命周期规则承接冷数据,标准 → 低频 → 归档逐级下沉,别手工搬。
  • 快照:EVS 快照策略定期清理,快照费用悄悄超过盘本身的情况并不少见。
  • 弹性:CCE 节点池配合弹性伸缩承接架构层优化,比单纯降配更可持续。
  • 网络:闲置 EIP 直接释放,只解绑仍然会计费。

八、上线前 checklist

  • [ ] 每个资源都能归到 service / env / owner
  • [ ] 单位成本的分母按业务类型选对
  • [ ] 共享成本有明确、可复查的分摊规则
  • [ ] 降配依据是 P95/P99,不是均值
  • [ ] 所有释放动作有快照,或确认无绑定
  • [ ] 中风险动作先灰度 7 天再推全量
  • [ ] 优化前后记录的是单位成本,不只是总额
  • [ ] 每季度复盘一次"省下来的钱有没有变成事故"

写在最后

云成本优化不是一次清理活动,而是一条持续的度量线。

资源层帮你把明显的浪费扫掉,这件事该做,也有明确的终点。单位成本帮你判断哪些投入值得继续、哪些服务该合并或下线,这件事没有终点,但它决定了账单的长期形状。

先算那个除法,再动资源。

代码在文中,直接复制就能跑。如果你们团队也在做成本归因,欢迎聊聊共享成本是怎么摊的——这块比找闲置难得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。