打开你的Cassandra集群的nodetool tablestatus,看到某张表SSTable数量冲到几十甚至上百,后台compaction永远跟不上写入,查询慢得像在拖泥巴——这类问题十有八九不是硬件不行,而是当初建表时随手选错了compaction策略。
Cassandra的compaction策略,简单说就是决定SSTable文件“什么时候合并、怎么合并、跟谁合并”的一套后台规则。它直接影响磁盘占用、写入放大、读取效率,甚至决定你在半夜会不会被告警电话吵醒。今天只聊常规生产环境里最常见的三种策略:SizeTieredCompactionStrategy(STCS)、LeveledCompactionStrategy(LCS)和TimeWindowCompactionStrategy(TWCS)。我会把它们的原理、适用场景、具体配置写法,以及我在实际维护中踩过的坑一次性讲清楚。无论你是刚接手Cassandra集群的运维,还是在设计数据模型的后端开发,这篇都适合你翻一翻。
1. 内容整体设计与思路拆解
1.1 为什么compaction策略值得单独抠细节
Cassandra底层用的是LSM-Tree结构,所有写入先到内存中的Memtable,然后按顺序落盘成不可变的SSTable。SSTable一旦生成就不会再改写,所以同一张表的多个副本数据可能散落在不同SSTable里。查询时需要把多个SSTable的数据合并读取,你会发现文件越多,读路径越慢。
Compaction就是后台线程挑出一些SSTable,读出来重新排序、合并,剔除掉过期的数据和墓碑(tombstone),写成一两个新的SSTable。这个过程听起来简单,但不同策略对“挑哪些文件、什么时候挑、合并成多大”有完全不同的决策路径,也就带来了完全不同的IO特征和空间需求。
选了不合适的策略,最常见的现象就是:SSTable越堆越多,磁盘空间被重复数据吃光,读放大高得离谱,或者compaction本身把IO打满导致正常读写被拖慢。这就是为什么在建表或者调整表结构之前,必须先想清楚这张表的写入模式和读取模式。
1.2 三种策略的核心差异一览
下面这张对比表是我自己习惯用的速查表,不涉及具体版本细节,但在选型时足够用了。
| 策略 | 合并逻辑 | 空间放大 | 读放大 | 写放大 | 典型场景 |
|---|---|---|---|---|---|
| STCS | 按SSTable大小分层,把差不多大小的合并成更大的 | 高 | 高 | 低 | 写多读少、吞吐优先、不追求读延迟 |
| LCS | 把SSTable分成固定大小的level,逐层向下压实 | 低 | 低 | 高 | 读多写少、追求稳定读延迟、热数据表 |
| TWCS | 按时间窗口分组,窗口内合并,窗口间不再合并 | 低 | 低 | 低 | 时序数据、日志数据、按时间过期清理 |
这张表只是前菜。真正决定你能不能用好它们的是背后的触发条件、参数细节和切换时机。
2. 核心细节解析与实操要点
2.1 SizeTiered:适合写多读少的“老实人”
STCS是Cassandra默认的策略,它的思想非常朴素:维护几个大小相近的SSTable组,当一个组里的文件数量达到阈值,就把整组合并成一个更大的SSTable。这个“大小相近”由bucket_high和bucket_low定义,比如默认bucket_high=1.5、bucket_low=0.5,意思是两个SSTable大小差异在0.5倍到1.5倍之间时,认为是同一层。
默认情况下,表里有4个同样量级的SSTable(min_threshold=4)就会触发合并,最多可以合并32个(max_threshold=32)。合并后文件变大,进入新的层级,等待下一次和同规模文件合并。所以STCS的SSTable体积会一路涨,最终形成“大文件堆叠”的局面。
STCS的优点是写入路径基本不额外折腾,落盘后就是后台慢慢合并,写入延迟低。缺点也很致命:空间放大严重。因为要留出相当于原始数据一段比例的临时空间,并且大合并时会把几十个文件读出来再写回去,瞬间IO和磁盘占用都拉满。
我给一个实际体验:某日志分析系统使用默认配置,数据量大概每天200GB,跑了两个月后磁盘占用比实际数据多出了将近一倍。排查询时发现大量大合并正在同时进行,单个节点临时SSTable峰值达到30GB,磁盘被打满。后来改成LCS才缓解。
如果你确定要用STCS,建表时可以这样显式声明:
CREATE TABLE user_events ( user_id uuid, event_time timestamp, event_type text, payload text, PRIMARY KEY ((user_id), event_time) ) WITH compaction = { 'class': 'SizeTieredCompactionStrategy', 'min_threshold': 4, 'max_threshold': 32, 'min_sstable_size': 50 };min_sstable_size的默认单位是MB,小于这个值的SSTable不会参与分层合并。实际上,官方文档里STCS还有sstable_size_in_mb参数,但某些版本没启用。我建议不要纠结这些细节,把握住“文件越多、合并越猛”这个本质就行。
2.2 Leveled:用写放大换读性能的“强迫症”
LCS的设计目标很直接:让任意一条数据在读的时候最多只接触少数几个SSTable。它把SSTable固定成level,每个level里又分成固定大小(默认160MB)的小文件。Level 0是刚落盘的文件,L1大小上限是一个文件大小,L2是10个文件大小,L3是100个文件大小,以此类推,每下一层容量放大10倍。
LCS保证每个level内不重叠,除非Level 0有重叠。这样查询时最多读一个L0的SSTable加上每个level各一个SSTable,读放大是“1 + level数量”,非常可控。读性能和延迟稳定性比STCS好得多。
但代价是写放大会显著提高。数据可能被反复搬运:先写到L0,再合并进L1,如果L1满了还要把里面的数据拆出来推到L2,一次写入可能被放大好几倍。高压写入场景下,后台compaction容易跟上不写入,表现为pending compaction长期不为零,CPU和磁盘都在燃。
所以LCS适合读多写少、对读延迟有要求的业务表,比如用户资料、订单状态、会话信息。对于日志流水类高写入场景,用LCS就是给自己找事。
配置示例:
CREATE TABLE order_status ( order_id text, updated_at timestamp, status int, PRIMARY KEY (order_id) ) WITH compaction = { 'class': 'LeveledCompactionStrategy', 'sstable_size_in_mb': 160, 'tombstone_threshold': 0.2 };这里sstable_size_in_mb决定每层内SSTable多“宽”,默认160MB。调小这个值会让文件更碎、合并更频繁,调大则会减少合并次数但增加单次合并开销。一般保持默认即可,除非你有明确的性能测试结论。
2.3 TimeWindow:时序数据的“时间切片机”
TWCS是我个人最爱,也是目前处理时间序列数据的最优解,前提是你的数据真的符合时间序列特征。它的核心思路是:按照一个固定时间窗口(比如一天或一小时)来组织SSTable。同一窗口内的数据允许合并,跨窗口的SSTable互不干扰。
这样做的好处非常明显。首先,因为跨窗口不合并,老窗口的SSTable会一直保持原始形态,不会被反复读写,删数据的成本极低——直接把整个窗口的SSTable扔掉就行。同时,读放大也很低,查询某条时间点的数据只需要读取对应窗口内的文件。写放大也很低,因为窗口内合并只涉及当天的数据,不会拉扯历史数据。
TWCS对模型有硬性要求:表必须有一个时间戳列作为clustering key的一部分,且写入时该时间戳必须基本跟随系统当前时间。如果补写历史数据,那些数据会被归入旧窗口,导致旧窗口出现新的SSTable,破坏了“窗口封闭”的假设。严重时甚至会把大量跨窗口SSTable合并在一起,退化成STCS的恐怖场景。
我维护过一个设备上报表,数据会延迟几小时入库,用TWCS后经常看到旧窗口出现新合并,后来强制客户端把上报时间作为时间戳而不是入库时间,问题才解决。
TWCS建表示例:
CREATE TABLE device_metrics ( device_id uuid, recorded_at timestamp, temperature double, humidity double, PRIMARY KEY ((device_id), recorded_at) ) WITH compaction = { 'class': 'TimeWindowCompactionStrategy', 'compaction_window_unit': 'DAYS', 'compaction_window_size': 1, 'tombstone_threshold': 0.2 };compaction_window_unit可以是MINUTES、HOURS、DAYS。compaction_window_size是窗口大小,比如1 DAY就是一天一个窗口。对于需要24小时以内清掉数据的短期缓表,甚至可以用HOURS级窗口,过期文件直接删除,非常干净。
3. 实操过程与核心环节实现
3.1 建表时如何声明compaction策略
上面已经给了三种策略的建表写法,这里我再补充两个容易忽略的细节。
第一,compaction参数是一个Map,键和值都必须是字符串。如果写数字不要加引号会直接报语法错误。比如写'min_threshold': 4正常,但'min_threshold': 4.0在某些版本也会被接受,不过统一写整数最稳。
第二,除了class之外,其他参数在Map里的顺序无所谓。Cassandra会忽略未知的参数,但不会报错。这意味着如果你写错了参数名,可能不会立刻发现,而是悄悄使用默认值。我曾经把compaction_window_size拼成compaction_window_sizes,建表成功了,但实际窗口大小一直用默认的1天,直到数据模型设计者发现窗口不对才排查出来。
如果你不确定当前生效的参数,直接查表定义:
DESCRIBE TABLE device_metrics;返回结果里会带出完整的compaction配置,一目了然。
3.2 运行时如何平滑切换compaction策略
生产环境很多时候建表时没想清楚,跑了一段时间发现策略不合适。Cassandra支持用ALTER TABLE动态修改compaction策略,不需要重启节点。
比如想把某张日志表从默认的STCS改成TWCS:
ALTER TABLE raw_logs WITH compaction = { 'class': 'TimeWindowCompactionStrategy', 'compaction_window_unit': 'HOURS', 'compaction_window_size': 6 };执行后,策略立即生效,后续新生成的SSTable都会按照新的逻辑管理。但要特别提醒,老SSTable不会被立刻重构。它们会继续存在,直到某个compaction触发时才会按新策略合并。所以切换后你可能会看到磁盘占用短暂不降反升,这是正常的过渡期。
另外,从STCS或LCS切换到TWCS前,务必确认表结构里有合适的时间戳列,并且数据写入模式确实是随当前时间追加的。否则切换后旧窗口会一直积累不合并的数据,历史文件越堆越多,最终比原来还糟。
3.3 参数配置建议与踩坑笔记
这里整理一份我在不同场景下实际采用的参数参考。注意这不是黄金标准,只是给你一个起点,最终要结合集群磁盘IO能力和读延迟目标调优。
| 场景 | 策略 | 推荐配置 |
|---|---|---|
| 高吞吐日志、流水、事件表,不追求点查 | STCS | min_threshold=4, max_threshold=32, min_sstable_size=50 |
| 用户信息、订单状态、点查延迟敏感 | LCS | sstable_size_in_mb=160, tombstone_threshold=0.2 |
| 时序指标、设备上报、按时间过期 | TWCS | compaction_window_unit=DAYS, compaction_window_size=1, tombstone_threshold=0.1 |
参数选择有几个原则:
- STCS的
min_threshold调高(比如6)能减少合并频率,但会让SSTable数量涨得更高,读放大更大。调低则合并更频繁,写放大上升。追求写入稳定的场景建议保持默认4。 - LCS的
tombstone_threshold表示当SSTable里墓碑占比超过这个值就触发合并清理。对于频繁更新删除的表,设到0.2能更主动清理,避免读放大被墓碑拖垮。 - TWCS的
tombstone_threshold不要设太低,否则窗口内稍微有点删除就会频繁合并,相当于自废TWCS武功。建议0.1到0.2之间。
还有一个经常被忽略的参数是compaction_throughput_mb_per_sec,在cassandra.yaml里,默认是16或64,建议根据磁盘能力调大。否则在高写入场景下,compaction吞吐被限制,pending任务越积越多。这个参数是整个节点级的,不是表级。
4. 常见问题与排查技巧实录
4.1 问题速查表
下面几个问题是我被问得最多的,也是我自己真实踩过的,整理成表方便对照。
| 现象 | 可能原因 | 快速处理 |
|---|---|---|
| 磁盘空间突然暴涨,SSTable数量翻倍 | STCS大合并产生临时文件 | 等合并结束或改用LCS |
nodetool compactionstats显示pending任务长期高 | 写入速度超过compaction吞吐 | 调大compaction_throughput_mb_per_sec,或临时增加节点 |
| 使用TWCS后旧窗口仍然出现合并 | 写入时间戳严重偏离当前时间 | 检查客户端时间戳生成逻辑,强制使用业务时间 |
| 查询越来越慢,但SSTable数量不多 | 墓碑过多或跨分区宽行 | 检查tombstone_threshold配置,手动触发nodetool compact |
| 切换策略后磁盘占用变大 | 旧SSTable需要时间重新合并 | 观察一段时间,不要误判为策略错误 |
4.2 我遇到的三个真实观察
第一个观察是STCS在混合负载下的“大合并风暴”。某统计表每天凌晨都有大批数据处理任务,触发了几十个SSTable同时合并,现场IO延迟从3毫秒飙到200毫秒,线上读写全部卡顿。后来改成LCS,虽然写放大高了一些,但读延迟稳定多了,也没有再出现瞬间IO峰值。
第二个观察是LCS在纯写入压力下的“过度工作”。某项目直接把默认的STCS改成LCS,结果日志表写入速率高,后台compaction线程忙到起飞,CPU占用持续90%以上,写入还是跟不上。最后切成TWCS才解脱。
第三个观察是TWCS的“时间戳陷阱”。有一批IoT数据会延迟数天入库,业务方一直没意识到时间戳用的是入库时间还是采集时间。忽略这个区别的后果是,旧时间窗口不断有新SSTable生成,整个窗口的“不可变性”失效,空间放大逐渐逼近STCS。后来我们统一采集时间为时间戳,才恢复正常。
4.3 监控compaction是否健康的实用命令
日常巡检我建议至少盯这几个信息:
nodetool compactionstats:实时查看正在运行的compaction数量、任务类型、完成百分比。nodetool tablestats <keyspace.table>:查看每个表的SSTable数量和压缩比率。SSTable数量持续增长说明compaction跟不上。nodetool tpstats -- CompactionExecutor:如果CompactionExecutor线程池的Pending任务持续不为0,说明compaction积压了。nodetool netstats:查看节点间流传输状态,有时候compaction带来的磁盘压力会影响跨机房传输。
我习惯在监控大盘上放三个指标:每张表的SSTable数量变化、CompactionExecutor的Pending数、磁盘IO util。这三个指标组合起来基本能提前暴露compaction问题。
如果发现SSTable数量异常上升,先不要急着改策略。先看是不是墓碑过多、写入峰值、或者冷热数据不均衡。只有确认当前策略确实不适配负载模型,才值得做切换。
写在最后的一点体会
我见过太多同学在Cassandra建表时随手粘贴一段模板配置,然后半年后和compaction问题搏斗。其实选一个好策略不需要多复杂,只要先问自己三个问题:这张表的写入是持续追加还是频繁更新?读路径是否对延迟特别敏感?数据是否有明确的时间过期语义?
如果让我说一条最实用的经验,那就是:能复用默认配置就别自己发明,能用TWCS的别用STCS,能用LCS的别让它承担高压写入。但最重要的是,一定要在测试环境用真实的SSTable增长速率模拟几天,再看nodetool tablestats里的SSTable数量曲线是否稳定。你以为合理的配置,跑几天后往往会暴露真实问题。我后来养成的习惯是:每张新表上线前,都会专门跑到生产前置环境写一版压力数据,盯着compaction跑一个完整周期。这个习惯帮我提前拦下了好几次磁盘危机,也推荐你试试。