告别对象存储误删事故:版本控制与生命周期自动治理的实战部署
1. 先说一个每天都在发生的悲剧
跑批脚本里把s3://bucket/data_2025误写成s3://bucket/data,或某员工拿到高权限AK后顺手执行了一次aws s3 rm --recursive。对象存储没有回收站,普通桶一旦物理删除,神仙也救不回来。版本控制就是为这种事故准备的“后悔药”。但现实中很多团队开了版本控制就不管了,结果三个月后发现历史版本堆积了一整个T级存储成本,API List延迟翻倍,账单爆表。
所以本文不聊“要不要开”,直接写一套完整方案:开启版本控制 → 用生命周期自动老化清理 → 脚本自动巡检版本规模并动态治理。这是一份可以直接抄的部署实录。
2. 第一阶段:开启版本控制并验证生效
所有兼容S3 API的对象存储都支持以下操作。环境变量准备:
export AWS_ACCESS_KEY_ID=YOUR_AK
export AWS_SECRET_ACCESS_KEY=YOUR_SK
export AWS_DEFAULT_REGION=ap-southeast-1
# 如果你用的是非AWS兼容存储(如轻云互联对象存储),追加:
# export AWS_ENDPOINT_URL=https://s3.youridc.com
开启版本控制,只需要一条命令:
aws s3api put-bucket-versioning \
--bucket app-data \
--versioning-configuration Status=Enabled \
--endpoint-url $AWS_ENDPOINT_URL
验证是否生效:
aws s3api get-bucket-versioning --bucket app-data
输出Status: Enabled即成功。此时你向同一个Key上传3次相同对象,用下面的命令就能看到3条版本记录:
aws s3api list-object-versions --bucket app-data --prefix config/app.yaml
注意输出中会多出VersionId字段。如果是DeleteMarker(删除标记),说明这个对象发生过删除操作。
关键常识:删除标记(DeleteMarker)本身也是一个对象。开启版本控制后执行删除操作,并不会物理删除任何数据,只是新增了一条DeleteMarker版本。普通API请求(如GetObject)拿到的是当前最新版本,如果最新版本是DeleteMarker,就会显示404。这条特性下文写自动化治理时会用到。
3. 第二阶段:生命周期规则,把“清理历史版本”变成全自动
版本控制开了不设生命周期,等于埋雷。每次写入同Key新版本,历史版本都会永久保存,直到桶被删除。我们需要设计一条“冷热分层+历史版本老化”的规则。
下面是一份生产环境使用的生命周期JSON配置,可以直接存为lifecycle.json:
{
"Rules": [
{
"ID": "app-data-auto-tier",
"Status": "Enabled",
"Filter": {
"Prefix": "logs/"
},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 180,
"StorageClass": "GLACIER"
}
],
"NoncurrentVersionExpiration": {
"NoncurrentDays": 90
},
"Expiration": {
"Days": 365
}
},
{
"ID": "app-data-version-sweeper",
"Status": "Enabled",
"Filter": {
"Prefix": "config/"
},
"NoncurrentVersionExpiration": {
"NoncurrentDays": 7
}
}
]
}
应用规则:
aws s3api put-bucket-lifecycle-configuration \
--bucket app-data \
--lifecycle-configuration file://lifecycle.json
NoncurrentVersionExpiration用于清理历史版本——这是防误删体系的“自动回收站自动清空”功能;几天后自动删除非当前旧版本。Expiration.Days针对当前版本,指“当前版本创建多少天后删除”。Transitions做冷热分层,把180天前的日志自动转入Glacier省成本。
3.1 时间计算的一个坑:UTC零点是生活常识
对象存储的生命周期计算基准是UTC 0点(部分厂商是UTC+0每天固定时刻)。这意味着Days=7并不等于精确的168小时,而是“第7个自然日0点后开始处理”。如果你在周五14:00写入一个对象,生命周期可能在下下周周一0点才清除,实际存活时间比你想的多出34小时。不要设计“精确回收过期N天对象”的业务逻辑,尤其是配额控制场景,一律留出2天冗余。
4. 第三阶段:自动化巡检脚本,让版本规模始终可控
生命周期规则处理的是配置内已覆盖的路径。更常见的现实问题是:很多人会临时上传大文件、忘记排除某个前缀、或者生命周期规则被后人误改。所以我们要再加一道保险:一个每天定时执行的巡检脚本,扫描全桶版本总量和各前缀版本规模,发现异常自动收紧生命周期。
下面这个脚本使用Python + boto3,使用threading.ThreadPoolExecutor并发遍历,避免全桶串行扫描太慢。实际部署时把脚本放在一台能稳定访问存储的服务器上,如轻云互联的香港BGP云服务器,跨区域调用对象存储的List接口延迟稳定在10ms以内,长期巡检不会成为瓶颈。
4.1 核心巡检代码:object_version_audit.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import boto3
import os
import json
import logging
from datetime import datetime
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logger = logging.getLogger("version-audit")
session = boto3.session.Session()
s3 = session.client(
's3',
endpoint_url=os.getenv('AWS_ENDPOINT_URL'),
aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY'),
region_name=os.getenv('AWS_DEFAULT_REGION', 'ap-southeast-1')
)
BUCKET = os.environ.get("AUDIT_BUCKET", "app-data")
MAX_VERSION_PER_KEY = int(os.environ.get("MAX_VERSION_PER_KEY", "10"))
REPORT_PATH = f"/var/log/s3_audit_{datetime.now():%Y%m%d}.json"
def list_all_versions(bucket, prefix=""):
"""分页遍历所有版本,返回 (key, version_id, is_delete_marker, size, last_modified)"""
kwargs = {"Bucket": bucket, "MaxKeys": 1000}
if prefix:
kwargs["Prefix"] = prefix
while True:
resp = s3.list_object_versions(**kwargs)
for v in resp.get("Versions", []):
yield (
v["Key"],
v["VersionId"],
False,
v.get("Size", 0),
v["LastModified"]
)
for dm in resp.get("DeleteMarkers", []):
yield (
dm["Key"],
dm["VersionId"],
True,
0,
dm["LastModified"]
)
if resp.get("IsTruncated"):
kwargs["KeyMarker"] = resp.get("NextKeyMarker")
kwargs["VersionIdMarker"] = resp.get("NextVersionIdMarker")
else:
break
def scan_prefix(prefix):
"""统计单个前缀下的版本数,若某Key版本数超标则记录"""
version_count = defaultdict(int)
total_versions = 0
total_size = 0
to_alert = []
try:
for key, vid, is_dm, size, last_mod in list_all_versions(BUCKET, prefix):
version_count[key] += 1
total_versions += 1
total_size += size
for key, cnt in version_count.items():
if cnt > MAX_VERSION_PER_KEY and not key.endswith("DELETE_MARKER_PLACEHOLDER"):
to_alert.append({"key": key, "versions": cnt})
except Exception as e:
logger.error("scan %s failed: %s", prefix, e)
return None
return {
"prefix": prefix,
"total_versions": total_versions,
"total_size_bytes": total_size,
"over_version_keys": to_alert[:20] # 只存前20个告警,避免报告过肥
}
def main():
# 扫描所有一级前缀 "a/", "b/", "c/",如果桶是平铺结构,则直接用空串全量扫
prefixes = ["a/", "b/", "c/", "logs/", "config/", "tmp/"]
result = []
with ThreadPoolExecutor(max_workers=6) as executor:
future_map = {executor.submit(scan_prefix, p): p for p in prefixes}
for future in as_completed(future_map):
r = future.result()
if r is not None:
result.append(r)
with open(REPORT_PATH, "w") as f:
json.dump(result, f, indent=2, default=str)
logger.info("audit finish, duplicated /tmp/ only version anomaly... check %s", REPORT_PATH)
# 这里可以加一段:若发现超标Key,自动调用 put_bucket_lifecycle_configuration 收紧 NoncurrentDays
# 示例:自动将 tmp/ 前缀下的 NoncurrentDays 从90改为 7
anomaly = any(len(r["over_version_keys"]) > 0 for r in result)
if anomaly:
logger.warning("found over-version keys under one or more prefix, run auto-sweep...")
# 实际动作就是重复第3节的 put-bucket-lifecycle-configuration,只是替换JSON中的NoncurrentDays
# 具体调用函数见 auto_lifecycle_tighten.py,这里省略重复代码
if __name__ == "__main__":
main()
上面代码中total_size只统计了当前版本的大小。准确计算历史版本总体积需累加所有版本Size,脚本里为了跑得快没做全量累计。真要统计成本,可以在扫描时把size累加,代码保留你自行完善。
4.2 部署为crontab定时任务
# 每天凌晨2点执行版本规模巡检
0 2 * * * cd /opt/s3-audit && \
AWS_ENDPOINT_URL=https://s3.youridc.com \
AWS_ACCESS_KEY_ID=xxx \
AWS_SECRET_ACCESS_KEY=xxx \
AWS_DEFAULT_REGION=ap-southeast-1 \
python3 object_version_audit.py >> /var/log/s3_audit.log 2>&1
5. 运维排错与避坑指南
5.1 生命周期只清理非当前版本,但删除标记可能“残留吃钱”
很多S3兼容存储里DeleteMarker不能通过生命周期直接物理删除——除非执行删除的桶开启版本控制,并且该Key下所有旧版本都已被清理。现实中经常出现:删掉了对象全部版本,只留下删除标记,List看不到对象,但DeleteMarker本身仍占用元数据存储空间并产生小额费用。解决方式:用一次性脚本批量清理孤儿DeleteMarker。
# 列出所有DeleteMarker,并显示版本号
aws s3api list-object-versions --bucket app-data --query "DeleteMarkers[].{Key:Key,VersionId:VersionId}" --output text | \
awk '{print "aws s3api delete-object --bucket app-data --key "$1" --version-id "$2}' | \
head -100 | bash
注意:在正式清理前先确认这些DeleteMarker对应的旧版本是否已清空,否则会误删数据。这里仅用于隔离死标记。
5.2 生命周期规则执行是异步的,最长延误48小时
测试时用put-bucket-lifecycle-configuration后立刻查看对象,会发现并没有立即消失。这是正常的,规则生效一般在24小时内(AWS官方SLA为48小时)。不要反复重发配置,多次更新同一前缀的生命周期会延长整体生效时间。
5.3 关闭版本控制 ≠ 清空历史版本
如果已经通过put-bucket-versioning关闭版本控制,历史版本仍然存在,并通过
5.4 别在高峰时段用ListObjectVersions全量扫描大桶
如果一个桶内对象版本数超过几千万级,全量扫描会对存储侧产生较大请求压力,实测可能触发存储网关限流导致大量503(SlowDown)。建议把巡检频率改为24h一次,且只在业务低谷期执行。上面的脚本通过并发度为6已经可以跑完中小型桶;真遇到超大桶,需分批按前缀切分,或者基于存储侧的事件通知实时统计版本新增数。
6. 总结:这套方案到底帮你兜住了什么?
恢复误删文件、自动清理过期历史版本、防版本膨胀击穿成本,这三个能力组合起来,对象存储才算真正成为可用、可控的持久化层。整套方案我们已经在轻云互联的云服务器上连续跑了大半年,脚本部署在轻云互联的香港BGP实例上做跨域巡检、异常告警和生命周期策略自动下发,从来没有因为执行节点网络抖动导致漏跑任务。
最后送你一句话:版本控制是所有对象存储运维的底线,而自动化生命周期治理才是拥抱这条底线的真正姿势。