
打开你的Cassandra集群的nodetool tablestatus看到某张表SSTable数量冲到几十甚至上百后台compaction永远跟不上写入查询慢得像在拖泥巴——这类问题十有八九不是硬件不行而是当初建表时随手选错了compaction策略。Cassandra的compaction策略简单说就是决定SSTable文件“什么时候合并、怎么合并、跟谁合并”的一套后台规则。它直接影响磁盘占用、写入放大、读取效率甚至决定你在半夜会不会被告警电话吵醒。今天只聊常规生产环境里最常见的三种策略SizeTieredCompactionStrategySTCS、LeveledCompactionStrategyLCS和TimeWindowCompactionStrategyTWCS。我会把它们的原理、适用场景、具体配置写法以及我在实际维护中踩过的坑一次性讲清楚。无论你是刚接手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_high1.5、bucket_low0.5意思是两个SSTable大小差异在0.5倍到1.5倍之间时认为是同一层。默认情况下表里有4个同样量级的SSTablemin_threshold4就会触发合并最多可以合并32个max_threshold32。合并后文件变大进入新的层级等待下一次和同规模文件合并。所以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改成TWCSALTER TABLE raw_logs WITH compaction { class: TimeWindowCompactionStrategy, compaction_window_unit: HOURS, compaction_window_size: 6 };执行后策略立即生效后续新生成的SSTable都会按照新的逻辑管理。但要特别提醒老SSTable不会被立刻重构。它们会继续存在直到某个compaction触发时才会按新策略合并。所以切换后你可能会看到磁盘占用短暂不降反升这是正常的过渡期。另外从STCS或LCS切换到TWCS前务必确认表结构里有合适的时间戳列并且数据写入模式确实是随当前时间追加的。否则切换后旧窗口会一直积累不合并的数据历史文件越堆越多最终比原来还糟。3.3 参数配置建议与踩坑笔记这里整理一份我在不同场景下实际采用的参数参考。注意这不是黄金标准只是给你一个起点最终要结合集群磁盘IO能力和读延迟目标调优。场景策略推荐配置高吞吐日志、流水、事件表不追求点查STCSmin_threshold4, max_threshold32, min_sstable_size50用户信息、订单状态、点查延迟敏感LCSsstable_size_in_mb160, tombstone_threshold0.2时序指标、设备上报、按时间过期TWCScompaction_window_unitDAYS, compaction_window_size1, tombstone_threshold0.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大合并产生临时文件等合并结束或改用LCSnodetool 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跑一个完整周期。这个习惯帮我提前拦下了好几次磁盘危机也推荐你试试。