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