数据库基础体系 · 第 51/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
Cassandra 一致性与运维:复制、Quorum、Compaction、Repair 和故障
Cassandra 的一致性与传统单机关系型数据库不同:它通常不通过单一主节点、集中式锁或全局事务来保证所有请求立即看到同一份数据,而是把数据复制到多个节点,由客户端在每次读写时选择一致性级别,再通过后台的反熵修复、提示移交和存储压实逐步恢复副本状态。
因此,理解 Cassandra 不能只记住“它是最终一致的”。必须同时回答几个问题:
- 一行数据由哪些节点保存?
- 一次写入需要多少副本确认?
- 一次读取需要询问多少副本?
- 节点故障时,请求是失败、等待,还是由其他节点代为保存?
- 删除数据后,如何避免旧副本把数据“复活”?
- SSTable 越来越多时,Compaction 如何影响读写和磁盘?
- Repair 到底修复什么,为什么不能被 Compaction 替代?
QUORUM、LOCAL_QUORUM与“强一致”之间是什么关系?
下文以 Apache Cassandra 的公开稳定语义为准。具体命令选项可能随版本变化,生产环境应先执行 nodetool help 或对应子命令的 --help 确认。
一、先建立数据模型:复制发生在 Partition 层,而不是任意一行之间
1. Partition Key 决定数据落在哪里
Cassandra 的表通常由以下部分组成:
- Partition Key:决定一个分区的哈希值和负责它的节点;
- Clustering Columns:决定同一分区内部的排序和组织方式;
- 普通列:保存属性值。
例如:
CREATE KEYSPACE app
WITH replication = {
'class': 'NetworkTopologyStrategy',
'dc1': 3
};
CREATE TABLE app.user_events (
user_id text,
event_day date,
event_time timestamp,
event_id uuid,
event_type text,
payload text,
PRIMARY KEY ((user_id, event_day), event_time, event_id)
);
这里:
(user_id, event_day)
是复合 Partition Key。相同用户、相同日期的事件属于同一个 partition,并被映射到同一组副本节点。
event_time 和 event_id 是 Clustering Columns,它们只影响该 partition 内部的排序,不能把一条记录分散到多个 partition。
因此,复制的基本单位可以理解为:
一个 partition key
↓
哈希为 token
↓
根据复制策略找到多个副本节点
这也是 Cassandra 查询必须围绕 Partition Key 设计的原因。若查询不知道 Partition Key,协调节点无法直接确定有限的副本集合,可能需要扫描大量节点;这类查询通常不是 Cassandra 的正常访问模式。
二、复制:一个 partition 的数据为什么会存在多个节点
2.1 Token Ring 与副本集合
Cassandra 使用 partition key 的哈希值映射到 token。节点负责 token 环上的某些范围。对一个 partition,系统会根据复制策略选择若干节点保存副本。
设某个 partition 的复制因子为:
则该 partition 的数据逻辑上有 个副本。例如 RF=3 时,数据可能保存于:
Node A
Node B
Node C
这并不意味着客户端一定连接到其中某个固定节点。客户端可以连接到任意节点,该节点充当本次请求的 Coordinator,再把读写请求转发给真正负责该 partition 的副本节点。
所以一次请求至少包含两个角色:
- Coordinator:接收客户端请求、确定副本、收集响应;
- Replica:实际保存该 partition 数据的节点。
Coordinator 不一定是数据副本。
2.2 NetworkTopologyStrategy 才是多数据中心的正常选择
常见复制策略有:
SimpleStrategy
SimpleStrategy 沿 token ring 顺序选择后续节点,不考虑机架或数据中心拓扑。它适合测试,不适合生产多机架、多数据中心部署。
NetworkTopologyStrategy
NetworkTopologyStrategy 按数据中心分别指定复制因子,并尽量将副本分布到不同机架。
例如:
CREATE KEYSPACE app
WITH replication = {
'class': 'NetworkTopologyStrategy',
'dc1': 3,
'dc2': 3
};
这表示每个 partition 在 dc1 有 3 个副本,在 dc2 也有 3 个副本。全局副本数为 6,但“每个数据中心内的副本数”仍分别是 3。
需要注意:
- 复制因子是 keyspace 级别 的配置;
- 同一集群中,不同 keyspace 可以有不同复制策略;
- 修改 keyspace 的复制配置后,已有数据不会自动完成所有物理副本迁移,通常需要结合
rebuild、repair等运维流程使数据状态达到预期; - 复制因子不应大于实际可用的合适节点数,否则会造成副本分布和运维目标不合理。
可以通过以下命令检查:
nodetool getendpoints app user_events 'u-100|2025-01-01'
该命令根据给定的 partition key 查找副本端点。复合 Partition Key 在命令行中的序列化方式依赖版本和实现,生产环境应以当前版本 nodetool help getendpoints 为准。
也可以在 cqlsh 中查看 keyspace:
DESCRIBE KEYSPACE app;
三、Cassandra 的一致性模型:副本数和确认数是两个不同概念
复制因子只表示“有多少份数据”,不表示“每次操作必须等多少份确认”。
每次读写还要选择一个 Consistency Level,简称 CL,一致性级别。
3.1 常见写一致性级别
对于单数据中心部署,常见写 CL 包括:
ANYONETWOTHREEQUORUMALL
多数据中心部署还常用:
LOCAL_ONELOCAL_QUORUMEACH_QUORUM
ANY 是一个特殊的写级别:协调节点可以把写入作为 hint 保存,而不要求真正的副本当时可用。它只适用于普通写,不适用于读;使用它会降低写入语义的直观性,生产系统通常慎用。
3.2 常见读一致性级别
读操作常用:
ONETWOTHREEQUORUMLOCAL_ONELOCAL_QUORUMALL
读取时,Coordinator 向足够数量的副本发送请求,比较返回值,并可能在后台修复发现不一致的副本。
3.3 Quorum 的定义
对某个副本集合,设副本数为 ,则:
也就是超过半数。
例如 RF=3:
RF=4:
RF=5:
在 Cassandra 中,QUORUM 的具体计算基于本次请求涉及的副本集合。多数据中心场景下,LOCAL_QUORUM 只等待本地数据中心内超过半数的副本;它不是整个集群的全局 quorum。
对于某个数据中心的局部复制因子 :
例如:
dc1 RF = 3
dc2 RF = 3
在 dc1 发起 LOCAL_QUORUM 请求时,通常只需等待 dc1 的 2 个副本,而不是等待 6 个全局副本中的 4 个。
四、用 R+W>N 推导 Quorum 的读写重叠
设:
- :副本数;
- :一次写入至少等待的副本确认数;
- :一次读取至少查询的副本数。
若:
则读集合和写集合至少有一个副本相交。
4.1 RF=3、读写都使用 QUORUM
设:
N = 3
W = 2
R = 2
因为:
假设一次写入 v2:
Node A: v2
Node B: v2
Node C: v1
写入因为已经得到 A、B 两个确认而成功。
随后读取需要两个副本返回。可能的读集合有:
A + B → 都是 v2
A + C → 至少 A 是 v2
B + C → 至少 B 是 v2
无论读到哪两个节点,至少有一个看到 v2。Coordinator 会基于 Cassandra 的冲突解决规则选择最新值,并可以把较新的值修复到落后的副本。
4.2 反例:ONE 写加 ONE 读
仍然是 RF=3,但:
W = 1
R = 1
于是:
一次写入可能只落在 Node A:
Node A: v2
Node B: v1
Node C: v1
下一次读取如果只读到 Node B,就可能返回 v1。
这不是 Cassandra 违反了自身语义,而是客户端选择了允许较弱可见性的 CL。
4.3 这个公式不是“绝对强一致”的完整证明
R+W>N 只说明读写集合存在交集,实际语义还受到以下因素影响:
-
Cassandra 的冲突解决方式
普通列值通常通过写入时间戳进行 last-write-wins 冲突解决,而不是通过事务日志顺序。 -
客户端时间戳问题
如果客户端显式提供了错误的时间戳,未来时间戳可能长期压制后续写入。 -
删除也是带时间戳的墓碑
一个较新的 tombstone 可以压过旧值;如果墓碑被错误清理,问题会变得更复杂。 -
超时不等于没有写入
Coordinator 在超时前可能已经把数据写入部分副本。客户端重试时,必须考虑请求是否幂等。 -
一致性级别只约束本次操作
一次写用QUORUM,下一次读用ONE,不能把整个业务流程简单称作“Quorum 强一致”。
更准确的说法是:在正常故障模型和正确时间戳条件下,合适的读写重叠可以提高读到最新已确认值的概率和保证;它不等同于所有操作都具有数据库事务的全局线性一致性。
五、LOCAL_QUORUM、EACH_QUORUM 与多数据中心取舍
假设:
dc1 RF=3
dc2 RF=3
LOCAL_QUORUM
在 dc1 中:
LOCAL_QUORUM = 2
读写主要等待 dc1 的副本完成。这通常适合:
- 应用有明确的本地数据中心;
- 希望跨数据中心链路故障时本地仍可读写;
- 接受远端副本稍后通过异步机制追平。
但它不能保证一次本地读一定观察到另一数据中心刚完成的写入。如果写入和读取发生在不同数据中心,还需要更谨慎地设计 CL 和故障模型。
EACH_QUORUM
EACH_QUORUM 要求每个数据中心都达到本地 quorum。
上述配置中,一次操作至少需要:
dc1: 2
dc2: 2
它提供更强的跨数据中心确认语义,但代价是:
- 任一数据中心无法达到 quorum,操作就可能失败;
- 跨地域网络延迟直接进入请求路径;
- 数据中心级故障会明显降低可用性。
不要把 QUORUM 和 LOCAL_QUORUM 混用后再套简单公式
全局 QUORUM 的计算与本地 quorum 不同。多数据中心下,使用 LOCAL_QUORUM 时,正确的分析单位通常是“目标数据中心内的副本集合”,还要明确:
- 写入在哪个数据中心发起;
- 读取在哪个数据中心发起;
- 应用是否允许跨数据中心读取;
- 远端副本是否必须在请求成功前确认。
六、普通写入的路径:从 Coordinator 到副本
执行一个普通写入:
INSERT INTO app.user_events
(user_id, event_day, event_time, event_id, event_type, payload)
VALUES
('u-100', '2025-01-01', '2025-01-01 10:00:00+0000',
11111111-1111-1111-1111-111111111111, 'login', '{}')
USING TIMESTAMP 1735725600000000;
这里的 USING TIMESTAMP 单位通常是微秒。若不指定,Cassandra 会使用协调节点生成或接收的写入时间戳。手工指定时间戳时必须极其谨慎,因为较大的时间戳可能使后续正常写入都被判定为旧值。
典型流程是:
- 客户端连接到任意 Cassandra 节点;
- 该节点成为 Coordinator;
- Coordinator 对 Partition Key 计算 token;
- 根据 keyspace 的复制策略确定副本;
- 向副本发送写入;
- 副本先写入 Commit Log,再更新内存中的 Memtable;
- 达到写一致性级别所需的确认数量;
- Coordinator 向客户端返回成功。
Commit Log、Memtable 和 SSTable 的关系
写入路径可简化为:
客户端
↓
Coordinator
↓
Replica
├─ Commit Log:保证节点崩溃后的恢复能力
└─ Memtable:内存中的有序结构
↓ flush
SSTable:磁盘上的不可变文件
Commit Log 不是副本间的复制日志。每个节点本地记录自己的写入,用于节点崩溃后恢复。副本间的数据复制由 Cassandra 的请求转发和后台同步机制完成。
Memtable flush 后会生成新的 SSTable。SSTable 通常不可变,更新同一主键并不是原地覆盖旧文件,而是生成新的记录,后续由读取合并或 Compaction 清理。
七、读取路径:为什么读一个值可能访问多个副本
执行:
CONSISTENCY QUORUM;
SELECT *
FROM app.user_events
WHERE user_id = 'u-100'
AND event_day = '2025-01-01';
对于 RF=3、QUORUM 的情况,Coordinator 至少需要得到 2 个副本的结果。
读取过程通常包括:
- Coordinator 根据 Partition Key 找到副本;
- 向足够的副本发起读请求;
- 某些情况下向额外副本发送 digest 请求,用于比较数据摘要;
- 如果摘要不一致,Coordinator 请求完整数据;
- 根据列、时间戳和墓碑等信息合并结果;
- 对发现落后的副本执行 read repair 相关动作,具体行为和版本实现有关;
- 返回给客户端。
因此,读到“不一致”的中间状态不一定意味着 Cassandra 无法工作,而可能表示:
- 某副本尚未被 Repair;
- 某次写入只在部分副本上完成;
- 读取 CL 较低;
- 节点曾经离线;
- 发生了时间戳冲突;
- tombstone 与旧值在不同副本上状态不同。
不能把 read repair 当作完整 Repair 的替代品。读取只覆盖被访问到的 partition,不能系统性检查整个 token 范围。
八、故障时会发生什么:失败、超时、Hint 和部分成功
8.1 节点不可用与请求超时不是同一件事
Cassandra 客户端通常会遇到两类重要错误:
- Unavailable:Coordinator 根据故障检测器判断,当前可响应的副本数量不足以满足 CL;
- Timeout:在等待时间内没有收到足够响应,但可能已有部分副本完成了操作。
Timeout 的关键特征是:客户端不知道操作到底有没有生效。
例如 RF=3、写 CL=QUORUM:
Node A: 已写入
Node B: 已写入,但响应丢失
Node C: 未完成
Coordinator 可能返回 timeout,但 A、B 实际已经保存了数据。客户端如果把 timeout 当作“肯定没写入”并立即执行非幂等重试,可能造成业务重复。
因此:
- 使用业务唯一键设计幂等写入;
- 对计数器、追加事件等操作单独分析重试语义;
- 客户端应使用与驱动匹配的重试策略,不要简单地对所有异常无限重试;
- 对“结果未知”的请求,可以先查询状态,或者使用业务请求 ID 去重。
8.2 Hinted Handoff:副本暂时不可用时由其他节点代存
当一个目标副本暂时离线时,协调节点或其他节点可以为该副本保存 hint,记录“以后要把哪些写入交给它”。
简化流程:
目标副本 B 离线
写入到达 Coordinator A
A 将数据写给可用副本 C
A 或相关节点保存给 B 的 hint
B 恢复
hint 被回放给 B
B 追上部分缺失写入
Hint 不是永久的完整副本,也不能替代 Repair。它主要降低短时间节点故障造成的数据缺口。
若节点长时间离线:
- hint 可能不能覆盖全部离线时段;
- 节点恢复后仍需要 Repair;
- 在涉及删除时,不能只依赖 hint 来证明所有副本都看到了 tombstone。
8.3 读写可用性示例
RF=3、写 CL=QUORUM 时,至少需要 2 个副本可完成写入:
可用副本数 3 → 可以写
可用副本数 2 → 可以写
可用副本数 1 → Unavailable 或无法达到 CL
写 CL=ONE 时,理论上 1 个副本可确认:
可用副本数 1 → 可能写成功
但这不代表其他副本立即拥有同样的数据,也不代表读 CL=ONE 能读到该值。
高可用与一致性之间不是简单的“选择 Cassandra 参数”,而是业务对以下问题的选择:
- 能否接受短暂读旧值?
- 跨数据中心是否必须同步确认?
- 节点故障时能否继续写?
- 是否能处理 timeout 后的未知结果?
九、Compaction:把许多不可变 SSTable 合并成更适合读取的状态
9.1 为什么需要 Compaction
Cassandra 的 SSTable 不可变。更新和删除不会立即修改旧文件,而是写入新的 SSTable:
旧 SSTable:
k → v1
新 SSTable:
k → v2
读取时需要检查多个 SSTable,再根据时间戳和删除标记合并结果。随着 SSTable 增多,会带来:
- 更高的读放大;
- 更多 Bloom Filter、索引和摘要需要检查;
- 更多磁盘文件;
- 更高的合并与 I/O 成本。
Compaction 就是后台把多个 SSTable 读取、合并并生成新 SSTable 的过程。它不是副本同步机制,也不是事务提交机制。
Compaction 的主要目标是:
- 减少读取需要检查的文件;
- 清理被新值覆盖的旧版本;
- 在满足条件时清理 tombstone;
- 控制磁盘空间和写放大。
9.2 Tombstone 为什么重要
执行删除:
DELETE FROM app.user_events
WHERE user_id = 'u-100'
AND event_day = '2025-01-01'
AND event_time = '2025-01-01 10:00:00+0000'
AND event_id = 11111111-1111-1111-1111-111111111111;
Cassandra 通常不会马上从所有 SSTable 物理擦除数据,而是写入一个 tombstone,表示某个值或范围已被删除。
原因是:如果只物理删除当前节点上的值,而另一个副本还保存旧值,旧值在后续同步时可能重新出现。墓碑可以传播“这个值已经被删除”的事实。
墓碑也有代价:
- 读取时需要处理墓碑;
- 大量删除会增加磁盘和读取开销;
- 范围删除可能造成更广泛的 tombstone 扫描;
- tombstone 不能在副本尚未充分同步前随意清理。
9.3 gc_grace_seconds 的作用
gc_grace_seconds 表示 tombstone 在满足条件前至少保留一段时间,避免某个长期离线副本恢复后,把已删除的旧数据重新带回集群,也就是所谓的 zombie resurrection。
关键风险路径:
T0: Node B 离线
T1: A、C 写入 tombstone
T2: tombstone 在 A、C 上被 Compaction 清理
T3: B 恢复,仍保存旧值
T4: B 的旧值被重新同步到其他节点
如果 tombstone 的保留时间、节点离线时间和 Repair 计划不匹配,就可能出现删除复活。
因此,调整 gc_grace_seconds 不是简单的磁盘优化。必须同时考虑:
- 节点最长可能离线多久;
- 是否按计划完成 Repair;
- 使用的 Compaction Strategy;
- 是否有大范围删除;
- 是否存在网络分区或集群运维窗口。
在没有可靠 Repair 能力和验证机制的情况下,不应为了节省磁盘盲目把它设得很小。
9.4 常见 Compaction Strategy
Size-Tiered Compaction Strategy(STCS)
将大小相近的 SSTable 归为一组进行合并。适合写入更新较随机、数据生命周期相对均匀的场景。
特点:
- 写放大通常较低;
- 可能需要额外磁盘空间完成合并;
- 不同年龄的数据可能被合并到一起;
- 时间范围删除的清理不一定理想。
Leveled Compaction Strategy(LCS)
把 SSTable 组织成层级,并尽量限制同一 key 在同一层的重叠范围。
特点:
- 适合读多、更新较频繁、希望控制读放大的场景;
- Compaction 活跃度和写放大可能更高;
- 对磁盘和 I/O 有更高要求。
TimeWindowCompactionStrategy(TWCS)
按时间窗口组织 SSTable,适合时间序列和“写入后很少更新、按时间过期”的数据。
例如:
ALTER TABLE app.user_events
WITH compaction = {
'class': 'TimeWindowCompactionStrategy',
'compaction_window_unit': 'DAYS',
'compaction_window_size': '1'
};
配置含义是按一天一个时间窗口组织数据。实际窗口大小要结合写入时间跨度、TTL、查询范围和删除模式选择,不能机械套用“一天”。
TWCS 的优势是让旧时间窗口更容易整体过期,降低跨时间段的 Compaction。它并不自动解决:
- 错误的数据模型;
- 超大 partition;
- 过多 tombstone;
- 未完成的 Repair;
- 跨窗口更新。
9.5 查看和手动触发 Compaction
nodetool compactionstats
nodetool tablestats app.user_events
compactionstats 可以观察正在运行的 Compaction、待处理任务等。输出字段和格式会随版本变化,重点关注:
- pending tasks 是否长期堆积;
- 正在执行的任务数量;
- 读写延迟是否与 Compaction 高峰同时升高;
- 磁盘空间是否足够。
手动触发:
nodetool compact app user_events
这会强制对表执行一次主要 Compaction。它可能:
- 消耗大量磁盘 I/O;
- 需要额外临时磁盘空间;
- 与自动 Compaction 竞争;
- 在业务高峰造成延迟抖动。
不能把 nodetool compact 当成日常“清理磁盘”命令。先确认 Compaction Strategy、磁盘余量、SSTable 数量和业务窗口。
十、Repair:用反熵同步修复副本差异
10.1 Repair 解决什么问题
Repair 用于发现并修复副本之间的数据差异。它的核心思想是:对同一个 token 范围的副本计算摘要,找出不同部分,再通过流式传输同步缺失或较新的数据。
可抽象为:
相同 token 范围的多个副本
↓
各自构建摘要 / Merkle Tree
↓
比较摘要
↓
定位不一致范围
↓
Streaming 传输缺失数据
↓
副本收敛
Repair 不是:
- Compaction;
- 备份;
- 删除误操作的恢复;
- 应用级冲突解决;
- 自动纠正错误时间戳;
- 对所有历史错误数据的语义判断。
如果错误值已经按照 Cassandra 的冲突规则成为“最新值”,Repair 会把它传播得更一致,但不会知道业务上它是错误的。
10.2 为什么 Repair 是删除安全的一部分
考虑 RF=3:
Node A: 旧值
Node B: 旧值
Node C: 旧值
删除后,两个节点成功保存 tombstone:
Node A: tombstone
Node B: tombstone
Node C: 旧值
如果 C 长时间离线,而 A、B 上的 tombstone 先被清理,C 恢复后可能仍带着旧值。及时 Repair 的作用是让 C 也看到 tombstone,使所有副本在墓碑清理前完成同步。
所以 Repair 与 gc_grace_seconds 是一组约束:
节点可能离线的最长时间
+ 故障发现和恢复时间
+ Repair 调度延迟
必须小于墓碑安全保留窗口,且实际运维需要留出余量。若无法满足,就不能仅依赖默认参数保证删除安全。
10.3 Repair 的范围
Repair 可以针对:
- 某个 keyspace;
- 某个 table;
- 全部 token 范围;
- 某些 token 范围;
- 某个数据中心或拓扑范围,具体取决于版本和命令选项。
常见查看方式:
nodetool help repair
某些版本中可以使用类似:
nodetool repair app user_events
但不要直接把历史命令参数复制到所有版本。Repair 的选项、增量 Repair 行为和运维推荐会随 Cassandra 版本变化,应以当前集群版本的命令帮助和官方文档为准。
10.4 Repair 期间要观察什么
Repair 不是“执行命令后等成功”这么简单,应同时观察:
nodetool status
nodetool netstats
nodetool compactionstats
nodetool tpstats
重点包括:
nodetool status:节点状态、数据中心、机架、负载;nodetool netstats:Streaming 是否持续、是否失败;nodetool compactionstats:Repair 触发的读写压力及 Compaction 堆积;nodetool tpstats:线程池是否出现大量 pending 或 rejected;- 监控系统中的读写延迟、超时、磁盘使用率和 GC。
Repair 失败后不能简单认为“下次会自动补齐”。应检查:
- 失败发生在哪个节点和 token 范围;
- 是否由磁盘空间、网络、超时或节点状态造成;
- 是否需要重试特定范围;
- 是否已接近 tombstone 保留窗口;
- 是否需要暂停业务变更或扩大维护窗口。
十一、Compaction 和 Repair 的区别
二者经常被混淆,但作用完全不同。
| 机制 | 主要对象 | 解决的问题 |
|---|---|---|
| Compaction | 单个节点上的 SSTable | 减少 SSTable、合并版本、处理可清理墓碑 |
| Repair | 多个副本之间的数据 | 找出副本差异并同步 |
| Hint | 暂时不可用的目标副本 | 缓存短期缺失写入 |
| Read repair 相关机制 | 被读取到的 partition | 在读路径发现并修复部分差异 |
一个具体反例:
Node A 的 SSTable 有 v2
Node B 的 SSTable 有 v1
对 A 和 B 各自运行 Compaction,只会分别把本机文件整理得更好:
Node A: 整理后的 v2
Node B: 整理后的 v1
副本之间仍然不一致。只有 Repair 或某些读路径同步机制,才可能让 B 得到 v2。
反过来,Repair 也不等于压实 SSTable。Repair 传输数据后,目标节点可能生成新的 SSTable,仍然需要正常 Compaction。
十二、故障转移与节点替换:状态变化比命令本身更重要
12.1 Gossip 与故障检测
Cassandra 节点通过 Gossip 传播节点状态。故障检测器根据心跳和响应情况估计某个节点是否不可达。
这里的“节点被怀疑故障”不等于“节点数据已经被安全迁移”。它只影响请求路由和副本可用性判断。
网络分区时可能出现:
节点 A 认为 B down
节点 B 认为 A down
如果运维人员在没有确认网络状态的情况下强制移除节点,可能造成:
- 同一节点被错误替换;
- 数据复制不完整;
- token 所属关系与实际数据不一致;
- 后续 Repair 需要处理更大范围的差异。
12.2 节点重启不等于节点替换
节点短暂重启通常保留原来的 token 和本地数据。启动后,它会通过 Commit Log 恢复内存状态,并继续参与集群。
节点永久损坏或需要替换时,运维语义不同,常见流程包括:
- 判断原节点是否真的永久不可恢复;
- 防止旧节点在网络中“复活”并使用相同身份;
- 选择适合的替换或移除流程;
- 等待新节点完成数据流式传输;
- 对相关范围执行 Repair;
- 验证复制因子、节点状态和查询结果。
Cassandra 提供 removenode、assassinate、replace_address 等不同操作,但它们的危险程度和适用条件不同。特别是强制移除类操作不能作为“节点一时没有响应”的常规处理方式。
执行任何拓扑变更前,应先确认:
nodetool status
nodetool ring
nodetool describecluster
不同版本对 ring 输出的推荐程度可能不同;关键是确认节点身份、状态、数据中心、机架和 token 分布,而不是只看某一行命令输出。
12.3 扩容和缩容也是复制一致性问题
新节点加入后,不是自动立即拥有完整数据。它需要通过 Bootstrap 从负责相关 token 范围的节点流式获取数据。
扩容期间应关注:
- 新节点是否处于正常加入状态;
- Streaming 是否完成;
- 旧节点是否有异常负载;
- 磁盘和网络是否足够;
- 扩容后是否需要 Repair;
- 客户端驱动是否及时发现拓扑变化。
缩容、移动 token 或替换节点同样会触发数据流动。不能只修改配置文件就认为数据已经完成迁移。
十三、常见故障案例
案例一:使用 QUORUM 但仍读到旧值
可能的错误推理是:
RF=3,写和读都是 QUORUM,所以任何情况下都不可能读旧值。
实际还要检查:
- 读写是否针对同一个 Partition Key;
- 是否使用了不同 keyspace 或错误的复制配置;
- 是否跨数据中心并混用了
LOCAL_QUORUM; - 是否有客户端显式时间戳;
- 是否使用了轻量级事务、普通写和批处理的不同语义;
- 是否是写入 timeout 后的重试;
- 是否返回的是不同 clustering key 的另一行;
- 是否发生了时钟异常或应用层缓存。
诊断可以从以下开始:
nodetool getendpoints app user_events '...'
nodetool status
nodetool gossipinfo
再用不同 CL 查询同一个明确的完整主键,并核对应用传入的时间戳。
案例二:删除后数据又出现
优先排查:
- 是否有节点在删除期间长期离线;
- tombstone 是否在 Repair 前被清理;
gc_grace_seconds是否被不当缩短;- 是否存在错误的未来时间戳;
- 是否有旧备份或离线导入重新写回数据;
- 是否使用了范围删除并误判删除范围。
恢复思路不是立刻反复执行 DELETE。应先确认所有副本状态、执行适当 Repair,并在必要时用明确、递增且受控的时间戳重新写入删除标记。直接盲目降低 gc_grace_seconds 可能把问题变成不可逆的数据复活。
案例三:Compaction pending tasks 长期增长
这通常说明生成 SSTable 的速度超过 Compaction 消化速度,可能原因包括:
- 写入吞吐上升;
- 磁盘 I/O 饱和;
- Compaction Strategy 与数据模型不匹配;
- 大量更新或删除;
- Repair、Streaming 与 Compaction 竞争;
- 磁盘剩余空间不足导致 Compaction 无法安全进行。
不能只执行一次 nodetool compact。强制 Compaction 可能进一步增加 I/O 和临时空间压力。应先查看:
nodetool compactionstats
nodetool tablestats app.user_events
df -h
iostat -x 1
再判断是短期积压还是持续性容量问题。
案例四:节点恢复后状态正常,但数据仍不一致
节点能加入 Gossip 并显示 UN,只说明它在集群中可通信、状态正常,不等于它已经拥有所有应有数据。
还要检查:
- hint 是否回放完成;
- Bootstrap 或替换流程是否完成;
- Repair 是否覆盖离线期间的 token 范围;
- 是否发生了 tombstone 清理窗口风险;
nodetool netstats是否仍有 Streaming;- 关键 partition 在不同副本上的结果是否一致。
十四、事务边界:普通写入、Batch 和 LWT 不能混为一谈
14.1 普通写入不是跨 partition 事务
Cassandra 的普通 INSERT、UPDATE、DELETE 主要围绕单 partition 设计。即使应用连续执行多个语句,也不会自动获得关系型数据库那样的全局事务。
Cassandra 的 BATCH 也不能简单理解为通用事务。Logged Batch 主要用于把多个写操作作为一个批次传播,并降低同一批次部分写入的暴露;跨多个 partition 的大 Batch 可能把协调和日志压力集中到一个节点,不能用来替代合理的数据模型或分布式事务系统。
14.2 Lightweight Transaction(LWT)
需要条件判断时,可以使用轻量级事务:
INSERT INTO app.user_events
(user_id, event_day, event_time, event_id, event_type, payload)
VALUES
('u-200', '2025-01-01', '2025-01-01 10:00:00+0000',
22222222-2222-2222-2222-222222222222, 'login', '{}')
IF NOT EXISTS;
该操作使用 Cassandra 的 Paxos 机制,在单个 partition 范围内提供条件更新语义。结果会表明条件是否成立。
例如:
applied
-------
True
或:
applied
-------
False
LWT 不是普通写的“更强版本”这么简单。它有更高的协调和消息交互成本,也有独立的 SERIAL、LOCAL_SERIAL 一致性选项。它适合并发条件竞争,例如:
- 创建唯一记录;
- CAS 更新;
- 防止同一状态被多个客户端同时覆盖。
不适合把大量普通写全部改成 LWT,也不能据此获得跨多个 partition 的全局事务。
十五、一个可复现的最小实验
以下示例假设:
- 已有 Cassandra 集群;
- keyspace 使用
NetworkTopologyStrategy; dc1中至少有 3 个节点;- 客户端通过
cqlsh连接; - 具备创建 keyspace 和 table 的权限。
1. 创建测试表
CREATE KEYSPACE IF NOT EXISTS consistency_lab
WITH replication = {
'class': 'NetworkTopologyStrategy',
'dc1': 3
};
CREATE TABLE IF NOT EXISTS consistency_lab.kv (
id text PRIMARY KEY,
value text
);
2. 使用一致性级别写入和读取
CONSISTENCY QUORUM;
INSERT INTO consistency_lab.kv (id, value)
VALUES ('k1', 'v1');
SELECT * FROM consistency_lab.kv
WHERE id = 'k1';
预期结果:
Consistency: QUORUM
id | value
----+-------
k1 | v1
这里的逻辑是:
- RF=3;
QUORUM需要 2 个副本确认;- 读取
QUORUM需要查询 2 个副本; - 正常条件下读写集合相交。
3. 构造 timeout 后的未知结果
在测试环境中人为停止一个或多个副本,或者制造网络延迟,然后执行:
CONSISTENCY QUORUM;
INSERT INTO consistency_lab.kv (id, value)
VALUES ('k1', 'v2');
可能出现:
WriteTimeout:
received: 1
required: 2
这并不能推出:
k1 一定仍然是 v1
也不能推出:
k1 一定已经是 v2
正确处理方式是:
- 记录请求的业务 ID;
- 等待集群状态稳定;
- 重新读取该 key;
- 根据业务幂等规则决定是否重试;
- 检查副本和 Repair 状态。
4. 查看表的存储状态
nodetool tablestats consistency_lab.kv
nodetool compactionstats
nodetool status
这些命令不能直接证明业务数据已经跨所有副本一致,但可以帮助确认:
- 表是否产生大量 SSTable;
- Compaction 是否积压;
- 节点是否正常;
- 集群是否处于拓扑变更或故障状态。
十六、生产诊断的因果顺序
当出现“读旧值、删除复活、写入超时、磁盘暴涨”时,建议按因果链排查,而不是先执行破坏性命令。
第一步:确认请求目标
核对:
- keyspace 和 table;
- 完整 Partition Key;
- clustering 条件;
- 读写 CL;
- 是否跨数据中心;
- 是否使用 LWT;
- 是否带客户端时间戳。
很多所谓“一致性问题”其实是读写了不同 partition,或者 clustering 条件不完整。
第二步:确认副本集合
nodetool getendpoints <keyspace> <table> '<partition-key>'
确认实际副本分布在哪些节点、数据中心和机架。
第三步:确认节点和网络状态
nodetool status
nodetool gossipinfo
nodetool netstats
重点区分:
- 节点是否真正 down;
- 是否只是响应慢;
- 是否正在 Bootstrap、搬迁或 Streaming;
- 是否存在数据中心间网络问题。
第四步:确认存储压力
nodetool compactionstats
nodetool tablestats <keyspace>.<table>
df -h
重点观察:
- SSTable 数量;
- pending compaction;
- tombstone 相关读取警告;
- 磁盘空间;
- Compaction 与 Repair 是否争抢资源。
第五步:确认 Repair 与墓碑窗口
如果问题涉及删除、长期离线节点或节点替换,必须确认:
- 上次 Repair 的范围和时间;
- 是否存在失败或未覆盖的 token 范围;
- 节点离线时间;
- tombstone 是否已经可能被清理;
- 是否需要先隔离异常节点,避免旧数据重新传播。
十七、必须避免的几个错误结论
“RF=3,所以坏一个节点就完全没问题”
错误。RF=3 只说明有三份副本。能否继续读写取决于一致性级别:
RF=3,CL=QUORUM → 通常需要 2 个可用副本
RF=3,CL=ALL → 需要 3 个可用副本
RF=3,CL=ONE → 可能只需要 1 个可用副本
可用性、延迟和一致性是共同决定的。
“Compaction 完成后副本就一致了”
错误。Compaction 只整理本地 SSTable,不负责副本间同步。
“Repair 是备份”
错误。Repair 会让副本收敛,但不能提供按时间点恢复、误删回滚或历史版本保留。需要恢复能力时,应使用专门的备份和恢复方案。
“写入返回 timeout 就可以安全重试”
错误。timeout 可能代表部分副本已经成功。重试是否安全取决于操作是否幂等,以及业务是否能识别重复请求。
“所有读写都用 QUORUM 就是强一致数据库”
错误。QUORUM 通过副本集合重叠提供较强的普通读写语义,但 Cassandra 仍有时间戳冲突解决、最终一致性后台同步、跨数据中心选择、LWT 独立语义和 timeout 未知结果等边界。
“把 gc_grace_seconds 调小就能解决墓碑和磁盘问题”
错误。它可能减少墓碑保留时间,却增加删除复活风险。调整它必须建立在可验证的 Repair 计划、故障恢复时间边界和删除模式分析之上。
十八、如何把这些机制组合起来理解
对一个 Cassandra partition,可以用下面的链条理解:
Partition Key
↓
Token
↓
复制策略选择副本
↓
Coordinator 按 CL 发起读写
↓
副本写 Commit Log + Memtable
↓
flush 生成 SSTable
↓
Compaction 整理本地 SSTable 和墓碑
↓
Hint 处理短期副本不可达
↓
Repair 检查并同步长期差异
↓
故障检测、Streaming 和拓扑操作维持集群成员关系
其中每个机制解决不同问题:
- 复制:让数据有多个副本;
- Consistency Level:规定当前请求需要多少副本响应;
- Quorum:通过读写集合重叠降低读到旧值的可能性并提供相应保证;
- Compaction:整理单节点本地存储;
- Hinted Handoff:缓冲短期副本缺失;
- Repair:修复副本之间的长期差异;
- 故障处理:在节点、网络和拓扑变化中维持可用性与数据收敛;
- LWT:在单 partition 范围内提供条件更新的 Paxos 语义。
Cassandra 的可靠性不是由某一个参数单独决定的。复制因子、读写一致性级别、数据中心拓扑、时间戳、Compaction、Repair、墓碑保留时间和故障恢复流程必须一起设计。只有把“请求当下是否成功”和“所有副本最终是否收敛”区分开,才能正确分析 Cassandra 的一致性与运维行为。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:Cassandra 分布式数据模型:Partition Key、Clustering 与查询驱动设计
- 下一篇:Neo4j 图数据建模:节点、关系、属性、约束与 Cypher 基础
- 延伸:数据库复制、分片与高可用:一致性、路由、故障转移和扩容
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论