作为 Cassandra 5.0 最受瞩目的特性之一,基于 Accord 协议的全局多 Key 无锁 ACID 事务(CEP-15)彻底改变了 Cassandra 过去只能依靠 LWT(轻量级事务)实现单行一致性的局限。
然而,分布式共识协议往往伴随着沉重的历史包袱:为了确保并发事务的正确顺序,Accord 在运行期间需要记录极其复杂的事务依赖关系图(Dependency Graph)。每一个事务(Command)都需要知晓并记录与它产生冲突的前序事务。
如果不加控制,这些依赖日志和事务状态元数据将会随着系统运行呈线性甚至指数级膨胀,最终不仅会撑爆磁盘,还会因为检索历史依赖过长而拖垮读写性能。
本文将深入拆解 Cassandra 5.0 中 Accord 引擎的垃圾回收(GC)与元数据清理机制,探究它是如何在保证共识安全性的前提下,实现依赖日志的自我净化与阶段性截断的。
1. 为什么 Accord 会产生无限膨胀的元数据?
在理解清理机制之前,我们需要先看看 Accord 到底在存储中写了什么。
Accord 协议的核心是 Command(命令/事务)。每个 Command 在生命周期中会经历多个阶段:
PreAccepted(预接受:收集时间戳与初步依赖)Accepted(已接受)Committed(已提交:确定了最终的执行顺序和依赖集)ReadyToExecute(准备执行)Executed/Applied(已执行/已应用)
为了在无 Leader 架构下实现单 RTT 的共识,Accord 必须在本地的 CommandStore(命令存储区,负责管理特定 Key 范围的事务状态)中物化这些状态。
每一次事务提案,都需要去查询当前 Key 范围内所有尚未结束的、或者可能与之冲突的历史事务 ID(TxnId)。这意味着:
- 依赖链条的延伸:新事务必须显式记录它依赖哪些旧事务。
- 状态留存:即使一个事务已经执行完毕并返回给客户端,它的元数据也不能立刻删除。因为如果有网络延迟导致的迟到提案(Late-arriving proposals),系统仍然需要知道那个历史事务的最终决策是什么(Commit 还是 Abort)。
如果任由其发展,system_accord 键空间下的系统表就会积压数以亿计的过往事务记录。
2. 破局之道:Accord 的持久性状态(Durability States)
Accord 能够安全清理历史元数据的前提,是确信**“这个事务在全网范围内都已经尘埃落定,且绝对不会再被重新拉起或查询”**。
为了量化这种安全程度,Accord 引入了明确的持久性(Durability)状态机:
- Local:事务在当前节点已经完成本地持久化。
- MajorityOrEpochDone:事务已经安全地同步到了对应 Epoch(纪元)的法定人数(Majority)节点上。
- UniversalOrContinuous(全网持久化):这是最关键的状态。它意味着集群中所有相关的副本节点都已经确认收到了该事务的执行结果(Apply),并且都已经将其本地化。
一旦一个事务被标记为 Universal,就意味着在整个分布式系统里,没有任何一个正常的或者落后的节点会再次就这个事务发起询问(Re-evaluate)。这是元数据可以被安全回收的终极绿灯。
3. 核心清理机制:Redundant Before 与 Command Store 截断
有了持久性状态作为标尺,Accord 内部通过两个核心机制来执行实际的物理清理:Redundant Before 与 Truncation(截断)。
3.1 动态计算 “Redundant Before” 阈值
每个 CommandStore 都会在内存中维护并不断推进一个安全边界,称为 redundantBefore。
这是一个多维度的映射表,它记录了:
对于某个特定的 Key 范围(Range),在某一个 Epoch 下,所有 TxnId 小于 $T_{safe}$ 的事务都已经达到了全网持久化(Universal Durable)状态。
这个 $T_{safe}$ 就是垃圾回收的“分水岭”。
- 任何 $TxnId < T_{safe}$ 的事务,其状态都已经被确认为
Redundant(冗余的)。 - 此后,如果由于网络极度延迟,收到了一个针对 $TxnId < T_{safe}$ 的古老事务的
PreAccept请求,Accord 节点甚至不需要去查表,可以直接判定其为“已过期/已废弃”,并予以拒绝或直接按Abort处理。
3.2 级联截断(Cascading Truncation)与物理删除
一旦 redundantBefore 阈值向前推进,后台的清理机制就会被触发。这个过程在 Accord 中被称为 Truncation,其具体步骤如下:
- 状态升格为 Truncated:在
system_accord.commands等系统表中,这些低于安全线的事务记录,其状态字段会被原子性地修改为Truncated。 - 剥离依赖项(Pruning Dependencies):在内存和活跃数据结构中,将这些已截断的
TxnId从后续活跃事务的“等待依赖列表”(Waiting On)中彻底移除。这极大地释放了并发控制器的 CPU 内存开销。 - 墓碑化与 Cassandra Compaction 协作:
- 在写路径上,Accord 会向底层的系统表写入带有 TTL 或者 Tombstone(墓碑)的更新,直接抹去这些 Command 的详细执行日志、写集(Write Set)和依赖明细。
- 仅保留极度简化的状态占位符(例如:只记录这个区间已经被截断,而不保留单个事务的明细)。
- Cassandra 原生的 LSM-tree 压实(Compaction)机制会随后跟进,在后台将这些被覆盖或打上墓碑的 SSTable 数据物理删除,真正释放磁盘空间。
4. Epoch 变更与拓扑重建时的元数据收敛
Cassandra 作为一个动态集群,经常面临节点加入、离开或数据副本范围(Token Ranges)转移的情况。Accord 使用 Epoch(纪元) 来管理这种拓扑变化。
拓扑变化是元数据膨胀的另一个高危期:新节点加入时,需要拉取历史范围的事务数据;旧节点交出所有权时,需要清理不再属于自己的元数据。
Accord 通过以下机制进行收敛:
- Bootstrap 期间的状态剪枝:当一个新节点通过 Bootstrap 流程接管某个 Key 范围时,它不需要从 Epoch 0 开始重放所有的事务日志。它只需要获取拉取时刻对应的
redundantBefore状态快照。旧节点会将已经Universal Durable的数据作为普通 Cassandra 数据(SSTables)同步过去,而将那些已经截断的历史事务元数据直接过滤掉。 - Epoch 退休(Retiring Epochs):当集群完全过渡到一个新的 Epoch,且所有旧 Epoch 内发起的事务都已经达成
Universal Durable状态后,整个旧 Epoch 的拓扑信息和元数据会被宣告“退休”。这允许 Accord 物理删除整个 Epoch 级别的状态索引。
5. 对运维与调优的启示
得益于这套严密的闭环设计,Cassandra 5.0 的 Accord 事务引擎在默认情况下能够高度自治地控制其存储开销。但深入了解其机制后,在实际生产运维中,有几个关键点需要特别注意:
- 掉线节点的负面效应:
如果集群中某个节点长时间宕机且未被移出拓扑(或未进行nodetool replace),会导致事务无法在全网达成Universal Durable状态。这会阻止redundantBefore阈值的推进,进而导致健康节点上的事务元数据无法被截断,引起日志堆积。因此,及时处理故障节点在 5.0 中变得更加不容拖延。 - Compaction 吞吐量:
由于元数据清理深度依赖 Cassandra 底层的 LSM Compaction 来释放空间,如果节点的磁盘 I/O 极度饱和,导致 Compaction 积压(Compaction Pending Tasks 持续高位),即使 Accord 在逻辑上完成了截断,物理磁盘空间也无法及时释放。需合理配置compaction_throughput_mb_per_sec。 - 监控核心指标:
运维人员应当重点关注 Accord 相关的监控指标,特别是与redundantBefore推进速度、CommandStore内存占用以及system_accord键空间数据量相关的 Metric。如果发现特定范围的截断进度停滞,应排查是否存在因网络分区导致的共识不一致或部分节点响应慢的问题。
通过将分布式共识的“持久性状态机”与底层的“LSM-tree 物理剪枝”深度结合,Cassandra 5.0 的 Accord 引擎精妙地解答了如何在提供强一致性事务的同时,保持存储引擎轻量化与高吞吐这一经典难题。这也是分布式数据库理论走向工业落地的一次优雅示范。