版本化把覆盖和删除都变成了追加。它不去替换字节,而是新铸一个版本 ID 并把旧的留着,于是一次写错的 PUT、一次删错的 DELETE 都还有退路。对于任何通过桶而不是通过数据库管理的数据,这是真正便宜的容灾。

同一个性质也正是麻烦所在。你留下的每一个版本都是要付钱的存储,而它的失败方式并不戏剧化——账单随写入量一路爬升,你能看到的对象数量却纹丝不动。更要命的是,这条曲线通常不会触发任何告警,因为它长得太像正常的业务增长了。

版本 ID 与 null 版本

打开版本化后,每次 PUT 都会产生一个新的版本 ID,最新的那个成为当前版本。不带版本 ID 的 GET 返回当前版本,带版本 ID 的 GET 精确返回那一个。所以「这个对象」的真实含义是「这个对象的当前版本」,任何假定「一个 key 一个对象」的工具都在悄悄出错。

在版本化启用之前写入的对象带着版本 ID null,而启用版本化并不会追溯地把它们变成有版本的。把这串操作重放一遍:启用前先 PUT 一次,存下 null;再启用版本化,然后覆盖两次,这个 key 现在有三个版本、合计 80 字节。那个 null 版本还在那里,而且仍然是一个可以被非版本化写入直接盖掉的单一槽位。

删除标记是墓碑,不是删除

在版本化的桶上不带版本 ID 执行 DELETE,会写入一个删除标记,并由这个标记成为当前版本。此后 GET 返回 404——但什么都没被释放。重放里,桶在删除前持有 80 字节,删除后还是 80 字节。标记是一个零字节的表项,把底下的一切都遮住了。

正确的撤销是删掉这个删除标记,上一个版本重新成为当前版本,200 回来了。错误的撤销是在标记仍然压顶时去删最新的对象版本:key 依旧是 404,而最新那份可恢复的副本没了。这正是「清理桶」脚本会犯的错,而且不可挽回。MFA delete 就是为这一类操作准备的——永久删除某个版本、或者关掉版本化,都必须提供 MFA 令牌。

生命周期是唯一给账单封顶的东西

两条彼此独立的规则,而只有其中一条能止住增长。Expiration 作用于当前版本;NoncurrentVersionExpiration 作用于底下所有版本,没有它,桶会为每一次覆盖永久累积一个活着的版本。模拟同一个 key 连续 365 天每天覆盖一次:没有非当前版本规则时,年底桶里躺着 365 个版本、14,742 字节;加上 30 天非当前版本规则后,只剩 29 个版本、1,186 字节,相差 12.4 倍。两种情况下当前版本规则都配了,而对这件事毫无作用。

你也可以把非当前版本转入更冷的存储类,而不是直接过期,这样可恢复窗口从免费变成廉价。对于确实可能要用到的数据,这通常是更划算的取舍,但它按版本收一次迁移请求费;在一个装着几百万小对象的桶上,这些请求费可能超过省下来的存储费。

坏掉的样子

第一种失败,是没有任何东西让非当前版本过期,桶于是被填满。它的表象是存储账单随写入量线性上涨,而可见的对象数量却是平的,很容易被误读成泄漏。诊断方法是把普通列举得到的对象数和带版本的列举得到的版本数放在一起比:一百比一就是它的特征,而解法是加一条 NoncurrentVersionExpiration 规则,不是换一个更大的桶。

第二种失败,是「恢复」操作恢复出来的其实是删除标记而不是对象。一个把「最新版本」拷回原位的恢复脚本,拷的就是那个标记,因为标记就是最新版本。key 继续不可见,运维于是断定数据没了——其实它就在下面一层。恢复必须按明确的版本 ID 来做,永远不要用「最新」,并且在你真正需要之前先演练一次。

快速验证

先读一个 key 的版本栈,再读桶上的规则:

aws s3api list-object-versions --bucket B --prefix reports/q3.pdf \
  --query 'sort_by(Versions,&LastModified)[-3:].[VersionId,IsLatest,Size]' --output table
aws s3api list-object-versions --bucket B --prefix reports/q3.pdf --query 'DeleteMarkers'
aws s3api head-object --bucket B --key reports/q3.pdf --version-id <vid>
aws s3api get-bucket-lifecycle-configuration --bucket B

第一条列出最新的三个版本及大小;这里出现 null,说明存在一段无法按版本过期的、版本化之前的数据。第二条点出当前正遮住这个 key 的删除标记。第三条证明某个具体版本真的读得出来,而这是唯一值得测试的恢复形式。第四条告诉你到底有没有非当前版本规则——如果没有,这个桶就没有天花板。

来源