本节摘要:开启版本控制后,覆盖写保留历史版本,删除只生成一个删除标记——数据本体原封不动。这一节用一次真实的误删恢复串起全部核心操作:开启、列版本、恢复、清理,以及"版本会吃容量"这个必须提前规划的副作用。
先讲反例。某团队的配置桶没开版本控制,一个调试脚本的路径拼接错误,把生产环境几百个配置对象批量覆盖成了空文件。没有历史、没有备份、没有后悔药,两个工程师通宵手工重建配置。事后复盘的结论只有一条:配置类桶必须在建桶当天开启版本控制。版本控制的本质是给"删除"和"覆盖"这两个不可逆动作加上缓冲层——代价是每份历史都要占空间,这笔账 5.2 会有清法。
开启版本控制后,桶的行为变成三条:
规则一:每次写入生成新版本。 同一个键被覆盖写入,旧版本不会消失,而是带着自己的版本号继续存在。最新版本(latest)指向最近一次写入。
规则二:删除生成删除标记。 对一个键执行删除,实际写入的是一个零字节的"删除标记"(delete marker)作为新的最新版本——所有旧版本原地不动。从客户端视角看,这个键"没了";从存储视角看,什么都没丢。这就是"删除不再是终点"的字面意思。
规则三:彻底删除要指定版本号。 想真正抹掉某个版本,必须显式指名道姓。没有版本号的删除永远只是盖删除标记。
三个规则落到命令上是这样的:
# 为桶开启版本控制 mc version enable fleet/app-config # 查看一个键的全部版本:两份数据加一个删除标记 mc ls --versions fleet/app-config/prod/app.yaml # [2026-05-10 ... v3 DELETE MARKER] <- 最近一次"删除"其实是个标记 # [2026-05-02 ... v2 1.2 KiB ...] <- 被覆盖前的版本,还在 # [2026-04-28 ... v1 1.1 KiB ...] <- 最初版本,还在 # 误删恢复法一:删掉删除标记本身(指定它的版本号) mc rm --version-id '删除标记的版本号' fleet/app-config/prod/app.yaml # 误删恢复法二:把旧版本复制成新版本(生成 v4,内容同 v2) mc cp --version-id '旧版本的版本号' fleet/app-config/prod/app.yaml fleet/app-config/prod/app.yaml
两种恢复法的差别值得咀嚼:法一把删除标记移除,历史序列恢复如初,像是删除从未发生;法二追加一个新版本,保留了"曾经被删过"的完整历史。审计场景偏爱法二,日常救急用法一更干净。

版本控制有三个状态:关闭(未开启)、开启(enabled)、挂起(suspended)。挂起状态最迷惑人——它不关闭版本链,只是新的写入不再生成新版本。已存在的历史版本原样保留,删除标记照样生成。于是"挂起"经常被误解成"关掉省空间",实际省不了几个字节。我们的建议干脆得很:桶一旦开启版本控制就不要挂起。要么开着,要么建桶时就没开过;半开半挂的状态既不能省空间,又让排错时的行为推理多一个分支。
与对象锁定:锁定特性要求桶必须开启版本控制(5.3),保留策略作用在具体版本上。这是个单向依赖——开了锁定的桶,版本控制是前提条件。
与条件写:版本号给了应用层"比较并交换"的原子语义:带版本号的条件写只在"你看到的版本仍是最新"时成功。3.3 秒杀案例里的乐观锁,机制上就是这条。应用设计里所有"读改写"竞态,都应该在这个机制上解决,而不是自己造锁。
背景:开头那个配置桶开启版本控制三个月后,桶的统计显示逻辑数据 2 GB、实际占用 7 GB——历史版本在膨胀。
操作:排查发现一个 CI 流程每天全量覆盖十几个配置对象,历史版本按天累积。处理方案不是关版本,而是加生命周期规则:非当前版本保留三十天后自动清理,删除标记在无后继版本时自动清除。
结果:实际占用回落到 2.4 GB 并长期稳定,误删恢复能力完整保留。
解读:这是"版本控制必须与生命周期配套"的现场版——版本保护解决安全性,生命周期解决经济性,两条腿走路才走得远。规则的具体写法就是下一节的第一课。
变式:日志类桶的版本需求与配置桶相反——日志天然只追加、几乎不覆盖,版本控制收益极低,反而徒增元数据。判断标准是"覆盖与误删的概率",而不是"数据重不重要"。
历史的寿数怎么定?下一节讲生命周期规则——让数据按规则自己流动。
不能,而且这个误解值得专门辟谣。版本控制与数据同在一个集群:机房级故障、集群级损坏、被同步的恶意覆盖,版本链与数据一起遇难。它防的是"应用层的逻辑错误"(误删、误覆盖),防不了"基础设施层的灾难"。两者的关系是分工:版本控制管高频小事故,备份与站点复制管低频大灾难——5.4 的三腿备份策略里,版本控制甚至不是其中一条腿。
几乎不会。移除删除标记或按版本读取,都只是元数据层面的指针操作,不触发数据搬移,延迟与普通读写同级。真正的性能注意点在列举:一个键积累了成百上千个版本后,带版本列举会变慢——这又是"版本要配生命周期"的一条论据。
最后补一个运维冷知识:桶级开启的版本控制没有"只对某个前缀生效"的选项。需要差异化保护的桶,正解是拆桶——按"重要到需要版本"与"不需要"分开,而不是在一个大桶里纠结粒度。拆桶同时还顺便解决了策略与生命周期的差异化配置,一举两得。