提起Cassandra,不少后端同学的第一反应可能是“听说过,好像是个分布式数据库”“排名挺靠前的,但周围好像也没几个团队真在用”。这种印象特别真实。我在分布式存储、中间件这块折腾了十年出头,先后维护过几套Cassandra集群,也亲眼见过一个高并发写入场景从“MySQL半死不活”迁移到Cassandra之后,扩容只需要加节点、改配置那种畅快感。
这篇文章不从官方文档给你念定义,而是站在一个实际用过的从业者角度,把这套系统最关键的几件事讲透:它为什么会出现、解决什么问题、数据模型背后那套思维为什么和传统数据库完全不同、读写路径里哪些机制决定性能上限,以及你最关心的——什么业务适合上它、上手时最容易踩哪些坑。如果你正在做技术选型,或者已经决定用Cassandra但还停留在“会用CQL”的阶段,这篇文章能帮你少走不少弯路。
1. 为什么会有Cassandra:单机扩展不动了才去想别的路
1.1 关系型数据库的写入瓶颈是结结实实存在的
先说个很多人都经历过的场景。业务早期,MySQL单库单表完全够用,读写都顺畅。等到用户量上来,写入量一天比一天夸张,主从架构开始撑不住:主库写入压力大、从库同步延迟、分库分表之后跨库关联查询变复杂,运维同学半夜三更被报警电话叫醒。更深一层的问题是,关系型数据库在后端是“单点写入”的模型——无论你怎么分片,一个数据项总得有一个唯一的归属节点负责写入,扩展性天然受限。
不是MySQL不好,而是它出生年代的硬件和业务模型决定了它的设计方向。到了互联网时代,高并发写入和数据规模成为常态,单机的CPU、内存、磁盘总有上限。这时候,横向扩展就不是“加分项”而是“必须项”了。于是有团队开始想:能不能做一套没有单一主节点的数据库,所有节点都能写入,数据自动打散分布,负载均衡靠加机器而不是换更大的机器?
1.2 两篇论文催生出的设计路线
Cassandra的技术路线非常清晰:它的存储引擎借鉴了Bigtable的LSM树思路,集群模型则融合了Dynamo的分区、复制与去中心化设计。这俩都是公开的论文思路。简单说,Dynamo那边的核心贡献是:用一致性哈希把数据均匀分散到节点上,节点间无主从之分,写入请求发给任意节点都可以,由节点之间的副本复制机制去完成数据同步。Bigtable那边的核心贡献是:不用传统B+树原地更新磁盘数据,而是先在内存里攒批、再顺序写盘、形成不可变的SSTable文件,配合后台合并操作来整理数据。
Cassandra把这两套思想合在了一起,同时在工程上做了大量简化:既没有复杂的依赖调度,也降低了实现分布式系统的门槛。它提供类SQL的CQL查询语言,数据通过一致性哈希自动分布在环上,每个节点保存数据副本,节点挂了由周围节点共同承担读写。你作为使用者,不需要操心某个key到底落在哪个节点,也不需要担心主节点挂了怎么办。
1.3 它到底是什么,不是什么
很多人拿Cassandra和HBase对比,因为都是LSM树存储引擎、都是海量写入场景。但它俩定位差别很大:HBase依赖HDFS和ZooKeeper,是主从架构中的一种,数据强一致性能力更好,但写入链路更长,运维组件更多;Cassandra是无主架构,节点之间地位对等,写路径更短,可用性更高,更适合多数据中心、跨地域部署和几乎不能接受停机的时间。
它也不是“强事务数据库”。Cassandra没有完整的ACID事务,它提供的是一致性等级制度。你写数据时指定一致性级别是ONE、QUORUM还是ALL,系统会按照对应数量的副本完成写确认或读取协调。这套设计放弃了传统数据库的串行化事务,换来了海量吞吐和高可用。你非要拿它存订单流水、做金融账务系统,我只能劝你再想想。
2. 不必先背API,先把设计哲学揉明白
2.1 去中心化:没有主节点,意味着没有单点
Cassandra集群里每个节点都是对等的,这是它最反直觉的地方。你随便连哪台机器执行CQL都行,这个节点就是本次请求的协调者。协调者根据分区键算出数据应该落在哪些节点上,然后联系对应的副本节点完成操作。节点之间会互相维护一份集群拓扑图,新加入的节点会被自动发现并纳入数据分布。
去中心化带来一个直接好处:没有主节点就意味着没有“主挂全挂”的单点故障。任何一个节点宕机,只要副本数足够,读写请求会由其他节点继续服务。坏处是,去中心化让一致性控制变复杂了,所以它必须用别人那套一致性等级机制来管控。你想牺牲多少一致性、换多少可用性,全看业务需求。
2.2 Gossip协议:节点之间怎么互相认识
Cassandra节点间传递状态信息,靠的是Gossip协议。直白点说就是“散布消息”:每个节点周期性随机挑几个其他节点,互相交换自己知道的集群状态,包括谁活着、谁死了、谁的负载高、谁的Token范围是什么。信息不像主从架构那样在一个中心点汇总,而是像传染病一样在整个集群里扩散。
第一次看这套协议的人容易关心一个问题:如果一个节点状态变化了,多久整个集群都知道?答案是通常非常快,但有个传播过程。实际使用中,如果你改配置、加节点、删除节点,心里要记得“不是马上生效”,而是需要一点时间让集群状态收敛。我见过有人刚给集群加完节点,立刻跑压力测试,发现数据分布不均衡,然后开始怀疑自己操作错了。其实等几分钟,等新节点拉起数据流,再观察负载均衡调度,通常就正常了。
2.3 一致性等级:最终一致不等于不靠谱
这是新手最容易误解的地方。有人一听“最终一致”就联想到丢数据、报表对不上。其实Cassandra的一致性是用一系列等级来精确控制的:
| 一致性等级 | 写入确认要求 | 读取结果要求 | 适用场景 |
|---|---|---|---|
| ONE | 任意一个副本写入成功 | 从任意一个副本读取 | 对一致性要求最低,性能最好,适合大量写入日志 |
| QUORUM | 过半副本写入成功 | 合并过半副本数据后返回 | 默认推荐,读写都过半可保证读到最新 |
| ALL | 所有副本写入成功 | 所有副本返回后合并 | 一致性最强,但节点故障会影响读写可用性 |
这里的QUORUM值得细讲:如果写入时要求过半副本确认,读取时也要求过半副本返回,那么读写集合必有交集,也就是说读请求至少会覆盖到一个包含最新数据的副本,这样就能保证读到最新值。这个思想叫“多数派相交”,不理解它,你就很难明白为什么最终一致的系统可以在多数派条件下对外表现得像“强一致”。
所以在实际项目里,关键业务数据把读写都设成QUORUM,是可以应付大多数一致性需求的。要注意的是,一致性等级越高,需要的副本交互就越多,延迟和失败概率也会上升。设置ALL之前先想清楚:你确定愿意为了强一致牺牲可用性吗?
3. 数据模型:决定你能否用得起来的是分区键
3.1 CQL长得像SQL,但关系最好早点断了
刚接触Cassandra的人会特别亲切,因为CQL写着像SQL:CREATE TABLE、SELECT、INSERT、UPDATE、DELETE,感觉差不多。但这种亲切是假象。CQL只选择了类SQL的语法外壳,底层是完全不同的数据组织方式。你在MySQL里可以随便写where条件、做join、搞子查询和group by,在Cassandra里如果查询不走主键路径,基本就是全表扫描,集群再多也扛不住。
我见过一个典型反面案例。有人把MySQL业务表逻辑直接翻译成CQL建表,读写倒是能跑,等到查询要求多了,发现想按业务字段查时只能建一堆二级索引,最后二级索引膨胀到比正经数据还占磁盘,查询性能惨不忍睹。总结一句话:SQL的语法是服务你的,但数据模型按查询设计,不是按实体设计。
3.2 partition key和clustering key是生命线
Cassandra的表结构里有两类键:分区键(partition key)和聚簇键(clustering key)。分区键决定了一条数据落在哪个节点、以及数据在文件里按什么顺序总体分布。聚簇键决定同一分区内部的数据排序方式。
比如一张订单表,如果主键设计为(uid, order_time),那么每一条订单数据会按用户uid分区,同一个用户的所有订单在聚簇键order_time排序下有顺序地存放。查询条件必须是“uid等于什么”“uid等于什么且order_time在哪个范围”这样带前缀的路径,才能直接定位到分区并顺序扫数据。你要是问“按商品ID查订单”,那么要么你另外建一张以商品ID为分区结构的表,要么接受全集群扫描。这个约束决定了建模时必须把业务查询当成第一优先级来设计主键。
3.3 建模的黄金法则:查询先行、反范式
Cassandra官方社区有一句话常被提及:先想好你的查询模式,再设计表。传统关系型建模是先分析实体关系,再通过查询把表关联起来。Cassandra恰恰反过来——你先列出所有需要支持的查询,针对每一个高频查询设计一张表。同一个业务实体可能出现好几张不同的表,比如订单主表、按用户查订单的表、按商品查销量汇总的表,这是非常正常的。数据冗余不是设计失误,而是分布式系统的必然代价。
配合查询先行,还要接受反范式。把需要展示的字段直接冗余存进去,避免跨节点join。我习惯把要展示的字段都塞到同一张表里,最多预留几个“宽行”字段做增量累积。这样查询路径最简单,性能也最可预期。
3.4 二级索引和物化视图:能不用就不用
Cassandra提供了二级索引和物化视图,听上去很贴心,但我实际用下来的体会是,它们只适合轻量级场景或者管理后台低并发查询。二级索引本质上是维护一份本地索引,查询时如果没命中分区,需要向集群内的多个节点广播查询,差不多就是一次微型全表扫描。物化视图背后也依赖额外的主键逻辑,调整起来比直接冗余表更复杂。
刚上手的团队往往喜欢用二级索引“凑合”支持一下非主键查询。我的建议是,如果这个非主键查询会被用户直接高频命中,在项目初期老老实实建反范式表;如果只是内部运营后台偶尔罢了,二级索引还能接受。
4. 读写路径:理解内部机制才能压榨性能
4.1 写路径:为什么Cassandra写得快
Cassandra的写性能好,不是硬件堆出来的,而是路径设计非常短。任何一个协调节点收到写入请求后,它先定位副本节点,把数据发给对应节点。每个节点收到写入后,先把操作追加到CommitLog(顺序写盘),然后把数据写入内存中的MemTable,只要这一步完成就给客户端返回成功。整个过程不需要查找旧数据、不需要更新随机磁盘位置、不需要维护二级索引。
这个设计好比记账:每天先在小本子上记流水,晚上再统一誊写账本。小本子顺序写特别快,誊写账本才涉及到整理和排序。对应到Cassandra就是CommitLog顺序写保证崩溃可恢复,MemTable在内存里攒数据,等到门限触发后,一次性把数据dump成不可变的SSTable文件。这种“先顺序写日志、再批量刷盘”的思路,天然对机械硬盘和固态硬盘都友好。
4.2 读路径:不是简单按主键找一条记录
Cassandra读取一条数据时,可能要同时查MemTable和多个SSTable文件,找出所有版本再按时间戳合并。如果每次读都把所有SSTable扫一遍,性能就崩了。于是它先查内存里的MemTable,然后借助每层SSTable的布隆过滤器(Bloom Filter)快速判断“这个文件里有没有我要的数据”。布隆过滤器是基于概率的判断,它只会漏报不会误报:说“没有”就一定没有,说“可能有”才去真正读文件。这个机制大幅减少了无效磁盘读取。
另外还有Row Cache、Key Cache等缓存层,能缓存热数据的关键位置。所以Cassandra读性能的上限,跟你的数据分布、SSTable层级、缓存命中率非常相关。写多读多的场景下,建议把SSTable合并频率调高一点,减少文件数量,因为在文件多、层级杂乱的时候,读放大是会实实在在拖慢查询的。
4.3 Compaction和Tombstone:看不见的幕后工程
随着数据不断写入,SSTable文件越积越多,重复键和旧版本数据散落在不同文件里。后台的Compaction(合并压缩)机制会定时挑选SSTable文件,把它们合并成一个更大的文件,顺便丢弃过期数据和已删除标记。这个过程很像整理房间:不整理房间也能住,但东西越堆越多,找东西越来越慢;定期整理速度会变快,但整理本身消耗资源。
删除操作在Cassandra里尤其隐蔽。它不是立刻物理擦除数据,而是写入一个Tombstone(墓碑)标记。等到Compaction真正执行时才把带墓碑的数据清理掉。如果你的业务模型高频删除旧数据,却没有控制好Compaction策略和墓碑清理时机,那么整个集群会出现“数据量看着不大、但是查询越来越慢、磁盘只增不减”的诡异现象。我处理过好几个类似问题,最终根因都是Tombstone堆积把读路径拖垮了。
4.4 LSM和B+树的取舍:为什么不用原地更新
有人问过:MySQL的B+树也很成熟,Cassandra为什么非要用LSM树那套“先内存后落盘”的设计?核心原因是随机IO和顺序IO的悬殊差距。B+树对单条记录做更新时,需要定位到对应的数据页做随机读写,磁盘的随机IO延迟比顺序IO高一个数量级。LSM树的做法是放弃原地更新,把所有写操作变成顺序追加,最终通过后台合并达到整理数据的目的。代价是读放大、写放大和合并开销,但这恰恰换来的是大量写入场景下的稳定吞吐。
这套取舍决定了Cassandra注定不是“全能选手”。你让它像MySQL那样处理千奇百怪的即时查询,它会很难受;你让它几百上千个并发同时灌数据,再按分区快速读出,它如鱼得水。
5. 场景判断:哪些活儿适合它干,哪些不适合
5.1 适合:写多读少的海量数据处理
我见到Cassandra用得最顺的场景,集中在这么几类:物联网设备上报数据的时序存储、社交应用的Feeds流、消息系统中海量聊天记录的冷热分离、电商购物车和Session管理。这些场景共性是:写入量巨大,单个客户端查询通常都带明确的用户ID,读取模式相对固定,不需要跨实体join和复杂事务。Cassandra的自动数据打散能力,让集群可以随时加机器吸收新增流量,很适合业务预测不准、经常突然涨量的项目。
以IoT传感器上报为例,每个传感器每分钟上报一条状态,一天可能上亿条。传统关系库30天存储就得做分区裁剪和归档,每次扩容都要停窗口。换成Cassandra,按传感器ID做分区,把时间作为聚簇键,查询一条传感器某时间范围的数据很快,写入压力被整个集群分担,运维只需要规划好时间维度的过期策略,配一个TTL自动清理老数据,非常省心。
5.2 不适合:强事务、复杂关联、多变查询
需要强事务原子性的账务系统、需要大量join和子查询的传统业务系统、查询条件完全不固定的数据仓库,这些场景不要勉强让Cassandra上。它在事务上只支持单分区的原子操作,跨分区的多个操作无法保证一致的原子性。尽管有LWT,但那只能覆盖单分区条件更新,性能和复杂度都有代价。
另外,如果你没有固定的访问模式,领导今天让你按手机号查用户,明天让你按注册时间查用户,后天让你按地域汇总下单量,关系型数据库和真正的大数据查询引擎都比Cassandra合适。用它干活的前提是,你得先确人业务查询稳定、可提前建模。
5.3 一句话评估法
遇到一个业务场景,我习惯用几句话做快速评估:数据写入量是不是单机扛不住的级别?查询是否都能用“某个固定实体ID+范围条件”表达?能不能接受数据最终一致带来的短暂不同步窗口?业务是否完全不能容忍主节点故障导致的停机?如果前两个是肯定、后两个能接受,Cassandra大概率合适。如果有任何一个不满足,我建议你重新考虑。
6. 上手实操:从单机到三节点集群的经验
6.1 环境准备和关键配置项
先别急着在生产环境上集群,我建议本地先用Docker起一个单节点,把CQL和基本操作跑通,再去配置真实集群。下载安装包后,最核心的配置文件是cassandra.yaml,里面有几个参数新手必须搞明白:
- cluster_name:同一个集群的节点必须一致,否则节点会被拒绝加入。
- seeds:种子节点列表,新节点通过它来学习集群拓扑。建议每个数据中心至少配置两个种子节点。
- listen_address、rpc_address:前者是节点间内部通信地址,后者是客户端连接地址,别混在一起。
- num_tokens:虚拟节点数,默认值在大多数场景够用,不建议一上来就乱调。
- data_file_directories:数据目录,有条件的话放到独立的NVMe固态盘上。
配置完后,用cassandra服务启动,执行nodetool status就能看到节点状态。出现UN(Up Normal)就说明节点健康。三个节点组集群时,可以先把第一个节点启动,等它变UN了,再依次添加第二、第三个节点。整个过程不需要手动分配Token,虚拟节点机制会自动划分配置。只要能正常互相发现,你的集群就已经形成。
6.2 节点规划和容量估算的土办法
容量估算很容易被忽视。Cassandra为了让出合并空间,磁盘占用往往是实际数据的数倍。我做容量规划时,习惯按“原始数据量×副本数×崩溃余量×合并余量”来估算。举个例子,原始数据100GB,三机房副本数3,为了应对节点替换和Compaction,我通常预留30%~50%的空闲空间,那么单个节点至少得准备“100GB×3×1.5/节点数”的空间。如果节点数少,单机磁盘压力会很大,宁可多上几台机器,也不要让单节点磁盘长期超过85%,否则Compaction容易把磁盘写满,直接触发节点故障。
内存配置方面,Cassandra默认使用堆内缓存和堆外存储。通常给JVM堆配置16GB以下就足够,太多内存会导致GC停顿变长,反而影响吞吐。系统内存的剩余部分会用于页缓存。保持“JVM堆适中、页缓存充裕”是常见的调优方向。
6.3 用cassandra-stress完成第一轮压测
配置好集群,别急着写业务代码,先用官方自带的cassandra-stress工具测试吞吐能力。它内置了多种压力模型,可以直接模拟写入、混合读写。我常用的做法是先用简单的默认写模型跑一轮,观察集群的QPS、延迟和CPU、磁盘IO。这里的经验是,Cassandra集群瓶颈通常先出现在磁盘IO和GC上,而不是CPU。如果压测中看到平均写延迟飙升,多半是磁盘排到瓶颈或者GC停顿太频繁。
压测还应该验证一致性问题。把读写一致性设成QUORUM,用cassandra-stress模拟持续高并发写入,过一段时间去查是否存在心跳超时节点。多轮压测下来,如果某个节点的GC停顿不断突破阈值,建议从JVM参数和堆内存开始排查,而不是盲目加机器。
6.4 我踩过的三个坑
第一,把数据目录放在系统盘。某次生产环境磁盘告警,后来发现数据目录和系统日志共用同一块云盘,Compaction时系统盘被打满,整个节点挂掉。后来数据文件一律独立挂盘,调度任务也做了限流。
第二,忽视Tombstone堆积。一个消息队列场景里,业务方高频写入又高频删除过期消息,建表时没设置合适的TTL和下压缩策略,集群膨胀到磁盘紧张后的查询经常超时。那次清理数据花了两个周末,血泪教训。现在我会在压测初期就加入带TTL的写模型,观察删除操作对SSTable数量的影响。
第三,一致性等级设置和业务预期不匹配。有次上线前把读写都设成ALL,自认为最安全,结果正好赶上机器网络抖动,客户端连续报错,用户体验一落千丈。后来改成QUORUM,同时在业务层做了缓存兜底,问题解决。对于大多数业务,QUORUM已经是性价比很高的选择,ALL适合那些“宁可不可用也不读旧值”的极少数场景。
这些坑其实都指向同一件事:Cassandra的分布式理念非常好,但用起来的很多问题源于对内部机制的理解不够。它不会像传统数据库那样由DBA帮你兜底一切,你作为使用者必须理解分区、副本、一致性、Compaction之间的关系,才能把这头大象驯服。我现在的习惯是,在任何Cassandra项目开工前,先把所有业务的查询模式列成一张表,针对每个查询验证主键设计,再谈集群规模和性能指标,这套流程下来基本没有再出过结构性问题。