成本优化

大多数云上资源,都比实际需要的大一号

我们接手的客户环境里,普遍存在一批 CPU p95 长期偏低的实例。这部分是纯粹的浪费,降配通常没有业务风险。

优化动作与收益

优化动作典型收益风险实施难度
清理未挂载 EBS 卷与旧快照存储费降 10%-25%低(先确认无引用)简单
释放闲置弹性 IP 与负载均衡器小额但纯收益简单
实例换代(m5 → m7i)10%-15%低(需重启)简单
实例降配(利用率低的降一档)25%-50%中(需压测确认)中等
切换到 Graviton(ARM)15%-25%中(需兼容验证)中等
gp2 → gp3约 20% 存储费极低(在线转换)简单
非生产环境定时启停非生产成本降 60%-70%简单
无状态服务混部 Spot40%-70%中(需容错设计)中等
Serverless 化低频任务80%-95%中(需改造)较高

闲置资源扫描脚本

python find_waste.py
"""闲置资源扫描:未挂载卷、闲置 EIP、空负载均衡器、低利用率实例。

输出 CSV 供人工确认后再批量处理。不做自动删除——
自动删资源这件事,风险远大于省下的钱。
"""

from __future__ import annotations

import csv
import sys
from datetime import datetime, timedelta, timezone

import boto3


def unattached_volumes(ec2) -> list[dict]:
    """未挂载的 EBS 卷。注意排除刚创建还没挂载的。"""
    out = []
    cutoff = datetime.now(timezone.utc) - timedelta(days=7)
    paginator = ec2.get_paginator("describe_volumes")
    for page in paginator.paginate(Filters=[{"Name": "status", "Values": ["available"]}]):
        for vol in page["Volumes"]:
            if vol["CreateTime"] > cutoff:
                continue  # 太新,可能正在使用中
            gb = vol["Size"]
            vtype = vol["VolumeType"]
            # 粗略估价,实际以区域价格为准
            unit = {"gp3": 0.096, "gp2": 0.12, "io2": 0.142, "st1": 0.054, "sc1": 0.018}
            out.append({
                "type": "unattached-volume",
                "id": vol["VolumeId"],
                "detail": f"{gb}GiB {vtype}",
                "est_monthly_usd": round(gb * unit.get(vtype, 0.1), 2),
                "created": vol["CreateTime"].date().isoformat(),
            })
    return out


def idle_elastic_ips(ec2) -> list[dict]:
    """未关联任何实例或网卡的弹性 IP,按小时计费。"""
    out = []
    for addr in ec2.describe_addresses()["Addresses"]:
        if addr.get("AssociationId"):
            continue
        out.append({
            "type": "idle-eip",
            "id": addr.get("AllocationId", addr.get("PublicIp",")),
            "detail": addr.get("PublicIp","),
            "est_monthly_usd": 3.6,
            "created":",
        })
    return out


def empty_load_balancers(elbv2) -> list[dict]:
    """目标组里没有健康实例的负载均衡器。"""
    out = []
    for lb in elbv2.describe_load_balancers()["LoadBalancers"]:
        arn = lb["LoadBalancerArn"]
        tgs = elbv2.describe_target_groups(LoadBalancerArn=arn)["TargetGroups"]
        total_targets = 0
        for tg in tgs:
            health = elbv2.describe_target_health(TargetGroupArn=tg["TargetGroupArn"])
            total_targets += len(health["TargetHealthDescriptions"])
        if total_targets == 0:
            out.append({
                "type": "empty-load-balancer",
                "id": lb["LoadBalancerName"],
                "detail": f"{lb['Type']} / {len(tgs)} 个目标组全空",
                "est_monthly_usd": 18.0,
                "created": lb["CreatedTime"].date().isoformat(),
            })
    return out


def low_utilization_instances(ec2, cw, days: int = 14, threshold: float = 20.0) -> list[dict]:
    """CPU p95 长期低于阈值的运行中实例,建议降配。"""
    out = []
    end = datetime.now(timezone.utc)
    start = end - timedelta(days=days)

    paginator = ec2.get_paginator("describe_instances")
    for page in paginator.paginate(
        Filters=[{"Name": "instance-state-name", "Values": ["running"]}]
    ):
        for res in page["Reservations"]:
            for inst in res["Instances"]:
                iid = inst["InstanceId"]
                stats = cw.get_metric_statistics(
                    Namespace="AWS/EC2",
                    MetricName="CPUUtilization",
                    Dimensions=[{"Name": "InstanceId", "Value": iid}],
                    StartTime=start, EndTime=end,
                    Period=3600,
                    ExtendedStatistics=["p95"],
                )
                points = [d["ExtendedStatistics"]["p95"] for d in stats["Datapoints"]]
                if not points:
                    continue
                p95 = max(points)   # 取观察期内最高的小时级 p95,保守判断
                if p95 < threshold:
                    name = next(
                        (t["Value"] for t in inst.get("Tags", []) if t["Key"] == "Name"),
                       ",
                    )
                    out.append({
                        "type": "low-utilization-instance",
                        "id": iid,
                        "detail": f"{inst['InstanceType']} {name} peak-p95={p95:.1f}%",
                        "est_monthly_usd": 0.0,   # 需结合机型价格计算
                        "created": inst["LaunchTime"].date().isoformat(),
                    })
    return out


def main(region: str = "ap-northeast-1") -> None:
    session = boto3.Session(region_name=region)
    ec2 = session.client("ec2")
    elbv2 = session.client("elbv2")
    cw = session.client("cloudwatch")

    findings = (
        unattached_volumes(ec2)
        + idle_elastic_ips(ec2)
        + empty_load_balancers(elbv2)
        + low_utilization_instances(ec2, cw)
    )
    findings.sort(key=lambda f: f["est_monthly_usd"], reverse=True)

    writer = csv.DictWriter(
        sys.stdout,
        fieldnames=["type", "id", "detail", "est_monthly_usd", "created"],
    )
    writer.writeheader()
    writer.writerows(findings)

    total = sum(f["est_monthly_usd"] for f in findings)
    print(f"\n# 共 {len(findings)} 项,预估月度可节省 ${total:,.2f}", file=sys.stderr)


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "ap-northeast-1")

低利用率判断用 p95 而不是均值,避免把有短时峰值的实例误判成闲置。

降配前必须确认的三件事

一是有没有内存瓶颈(CPU 低不代表内存也低,默认 CloudWatch 不采内存指标,需要装 Agent);二是有没有突发峰值(大促、月结、批处理窗口);三是有没有网络或 EBS 带宽依赖(降配会同时降低带宽上限)。

下一步

把云的复杂度交给我们,你只管做业务

留下需求,沐杉云的解决方案架构师会在一个工作日内联系你,提供免费的现状评估、迁移方案草案与 TCO 测算表。