
1. 从一个排队的案例说起第一次接触 Cassandra是在一个用户行为埋点系统里。当时 MySQL 单表已经上亿写流量高峰时主从延迟经常飙到十几秒半夜告警响个不停。团队试过加缓存、分库分表但数据量还在涨运维成本越来越高。那次引入 Cassandra 之后整个写入链路变得非常轻松——几十台普通机器组成的集群每天稳定吞下几十亿条记录单次写入延迟稳定在个位数毫秒。很多年以后我依然觉得对于时序型、海量写入、多副本高可用的场景Cassandra 是最值得优先考虑的存储方案之一。这篇文章不是官方文档的搬运而是以一个实际使用者的视角把 Cassandra 的核心概念、数据模型、部署实操、建表调优和常见坑一次讲清楚。不管你是刚听说这个名字还是已经用它跑过一段时间只要按着文章的思路走一遍至少能解决“这玩意到底怎么用”“为什么我写入这么慢”“我的表该怎么设计”这几个最要命的问题。2. 基础概念与核心设计思路2.1 它不是 MySQL也不是 MongoDB很多人第一次看到 Cassandra会下意识把它归类到“NoSQL 数据库”然后拿它和 MongoDB、Redis 做对比。这个方向不算错但远不够准确。Cassandra 最早是 Facebook 为了收件箱搜索功能开发的后来捐给了 Apache 基金会。它最大的特点是分布式优先。也就是说它天生就不是给你单机用的而是假设你会拿一堆普通服务器组成集群。数据自动分片、自动多副本复制、无单点故障这些是它的基本盘不是加装的功能。这决定了你思考问题的方式必须转变。用 MySQL 时第一反应是“我该怎么设计表、建索引”用 Cassandra 时第一反应是“我的数据该怎么分布、怎么查询”。如果你还是按关系型数据库那套思路去设计 Cassandra 的表结构基本会撞得头破血流。2.2 数据怎么分布一致性哈希与虚拟节点Cassandra 的数据分布机制是它一切特性的地基。它采用了一致性哈希的思想把整个哈希环分成若干个 token 范围每个节点负责环上的一段。具体来说写入一条数据时Cassandra 根据分区键的哈希值计算出一个 token这个 token 落在哪个范围数据就存储到哪个节点上。早期版本的 Cassandra 需要你手动指定节点负责的 token 区间到了 1.2 以后引入虚拟节点vnode概念每个物理节点默认分配 256 个虚拟节点系统自动管理 token 分配。你不需要关心节点具体负责哪个区间添加节点时数据会自动平衡这比早期版本省心太多了。注意vnode 虽然方便但如果你对数据局部性有严格要求比如想把同一租户的数据尽量路由到同一批机器默认配置不一定适合你。这种情况需要你去研究 NetworkTopologyStrategy 和 token 范围的精细化设计但绝大多数场景直接默认就行。2.3 副本与容错为什么机房挂一台机器没感觉副本策略决定了同一个数据在集群里存几份。生产环境最常用的配置是 NetworkTopologyStrategy它允许按数据中心DC设置副本数。比如一个集群有两个机房每个机房 3 副本那么每个机房内会存 3 份数据这样即使整个机房不可用另一个机房的数据依然完整。这里必然聊到 CAP 理论。Cassandra 属于 AP 系统但它并不是舍弃了一致性而是让你在每次读写时自己选择一致性级别。比如写入时指定 QUORUM即多数派确认读取时也指定 QUORUM那么读取到的数据基本是所有节点都认可的强一致数据。如果你指定 ONE那就只是某一个节点的数据可能落后一些但延迟最低。我个人的实践是核心账户类数据用 QUORUM 级别监控日志类数据用 ONE 级别。这样既有高可用又不会让性能被一致性拖垮。2.4 整体架构里还有几个关键角色除了数据节点本身Cassandra 的集群里还有一些逻辑角色需要了解Seed Node新节点接入集群时的“联络员”负责引导新节点认识集群里的其他节点。它不承担特殊的数据职责但配置时建议选稳定的节点不要动态变化。Gossip 协议节点之间通过每秒钟一次的消息交换来感知彼此的状态这是集群故障检测的基础。默认的 gossip 间隔已经够用不建议随便调。Hinted Handoff当一个节点短暂不可用时协调节点会把写给它的数据临时存起来等它恢复后再补送过去避免数据丢失。这个机制在节点重启、滚动升级时特别有用。3. 部署与基础实操3.1 环境准备与安装Cassandra 是基于 Java 的项目所以需要 JDK 8 或 JDK 11不同版本要求不一样建议查一下官方对应关系。我自己在 Ubuntu 20.04 上部署时用的 Java 11 Cassandra 4.0整体非常稳定。安装方式可以直接用 apt 源也可以下载 tarball。生产环境我推荐 tarball因为可控性强目录结构清晰也方便多版本共存。解压后的目录结构如下conf/存放配置文件核心是 cassandra.yamldata/数据文件目录logs/日志目录里面最有用的文件是 system.logbin/命令行工具比如 cqlsh、nodetool启动前需要修改 cassandra.yaml 里最核心的几个参数cluster_name: MyCassandraCluster listen_address: 10.0.1.11 rpc_address: 10.0.1.11 seed_provider: - class_name: org.apache.cassandra.locator.SimpleSeedProvider parameters: - seeds: 10.0.1.11,10.0.1.12cluster_name同一集群的所有节点必须一致否则节点会互相拒绝加入。listen_address节点之间内部通信绑定的 IP不写对会导致节点无法互相发现。rpc_address客户端连接地址如果客户端和节点不在同一网段要确保这个 IP 可达。注意cassandra.yaml 里被注释掉的默认参数非常多新手容易迷路。我的建议是别在默认配置上大改只调整必要项即可。Cassandra 的默认参数经历了大量生产环境的验证很多参数你看着别扭实际上在多数场景下已经很合理。3.2 启动、验证与常用命令启动很简单——进入 bin 目录执行./cassandra -R # 前台运行方便看日志 ./cassandra # 默认后台运行第一次启动会比较慢因为要初始化系统表。观察启动是否成功最直接的方法是看日志最后有没有 Starting listening for CQL clients 字样然后可以用nodetool status看集群状态nodetool status Datacenter: dc1 StatusUp/Down |/ StateNormal/Leaving/Joining/Moving -- Address Load Tokens Owns Host ID Rack UN 10.0.1.11 573.86 KB 256 ? 7f1b8b3e-3f94-46e8-9b6a-4a5b15a12345 rack1 UN 10.0.1.12 573.86 KB 256 ? 9b2a3c4f-4a5b-4c6d-8e9f-0a1b2c3d4e5f rack1状态为 UN表示节点 Up在线且 Normal正常。如果看到 DN说明节点 Down需要去查日志。3.3 添加新节点与集群扩容扩容是分布式数据库的常见操作。Cassandra 的扩容比很多系统都简单把新的机器装好、配置 seed 地址和 cluster_name启动后它会自动和种子节点通信完成入环和数据迁移。这里有一个关键点扩容会触发数据流式传输传输过程对网络带宽有一定占用。如果集群正在承载高流量业务建议把 stream_throughput_outbound_megabits_per_second 参数调低比如设置成 200避免因扩容拖垮业务流量。这个参数在 cassandra.yaml 里注释掉的需要自己加进去。扩完之后再用nodetool status看如果新节点状态变成 UN且全体节点的数据分布重新平衡扩容就完成了。数据均衡可能持续一段时间期间不要急着撤掉旧节点。3.4 客户端怎么连官方推荐的语言驱动有很多Java、Python、Go、Node.js 等都有。连接方式和 MySQL 区别不大只是连接串里写集群的多个地址。以 Python 驱动为例from cassandra.cluster import Cluster from cassandra.auth import PlainTextAuthProvider auth_provider PlainTextAuthProvider(cassandra, your_password) cluster Cluster([10.0.1.11, 10.0.1.12], port9042, auth_providerauth_provider) session cluster.connect()连接成功后执行 CQL 查询和普通 SQL 差不多的感觉但底层路由机制完全不同——驱动会自动计算 token把请求发送到对应节点或者协调节点。注意建议在驱动层面开启 TokenAwarePolicy让写请求直接路由到数据所在的节点省掉一次转发尤其是在写密集型场景下性能提升非常明显。Python 驱动默认就带这个策略其他语言的驱动通常也有配置项但默认不启用需要手动开。4. 分布式架构与关键技术原理4.1 写入路径从客户端到磁盘的一次旅行一条写请求进入集群后发生的事情比想象中多但整体思路很清晰协调节点先根据分区键计算 token定位到负责该 token 范围的副本节点集合向所有副本节点发送写请求。副本节点收到请求后把数据同时写入内存中的 memtable 和磁盘提交日志commit log然后返回确认信息给协调节点。协调节点根据一致性级别比如 QUORUM判断是否满足要求满足则将成功响应返回客户端。这里有个重要的细节数据不是直接写盘文件的而是先写 memtable。memtable 是一个按 key 排序的内存结构当它达到一定大小或时间阈值后会被刷入磁盘生成一个不可变的 Sorted String TableSSTable文件。这种“先写内存、批量刷盘”的设计让 Cassandra 的写入性能非常恐怖因为磁盘顺序写而非随机写。你可能担心数据丢失如果 memtable 还没刷盘就断电怎么办这就是 commit log 存在的意义。每次写入都会先顺序追加到 commit log真正落盘后再返回成功。所以即使 memtable 丢了重启后也能从 commit log 恢复。4.2 读路径与 Compaction读请求的要比写请求复杂一些因为需要从 memtable 和多个 SSTable 中找到目标数据覆盖的可能不止一个数据版本。为了控制读放大Cassandra 引入了一个名为 Bloom Filter 的结构它是一个大概率快速判断“这个 SSTable 里有没有这个 key”的过滤器有效跳过那些明显不包含目标数据的 SSTable 文件。除了 Bloom Filter还有两个层次的一级索引Partition Index定位 SSTable 中 partition 的大致偏移量。Column Index定位分区内部具体行的偏移量。三者的配合让 Cassandra 在数据量巨大的情况下依然能维持可接受的读延迟。当然数据持续增多后SSTable 的数目也越来越多必须有另一个人来处理——Compaction。Compaction 是后台任务负责把多个小 SSTable 合并成大文件清除已删除的数据提高读取效率。和 compation 相关的配置项很多比如 compaction_strategy、tombstone_threshold、gc_grace_seconds 等等。其中 tombstone墓碑和 gc_grace_seconds删除数据保留时长这两个概念是新手最容易踩坑的地方。4.3 GC 与墓碑机制Cassandra 的删除并不是立刻物理删除而是写入一个 tombstone 标记。这样做的目的是让“删除”在所有副本节点上保持一致。当 tombstone 的年龄超过 gc_grace_seconds默认 10 天后才会在压缩时被清理掉。如果你频繁插入和删除同一个 key会导致 tombstone 大量堆积查询时 Cassandra 要扫描这些墓碑严重时会出现“超出墓碑限制”的异常甚至让查询失败。这个坑非常隐蔽报错信息大概是 Unable to complete the query. Exceeded tombstone limit。我的建议高频删除的场景可以适当调高tombstone_failure_threshold但根治办法是优化操作模式比如用 TTL 代替手动删除。大批量删除后手动执行nodetool compact可以加速清理但要注意这会带来一定性能开销最好在低峰期做。4.4 存储引擎LSM 与 SSTableSSTable 的底子是 LSM-TreeLog-Structured Merge Tree一种专门优化写入性能的磁盘数据结构。和 MySQL 的 B 树不同LSM 理念是“我先把写请求全部顺序写入磁盘文件中代价是读取时可能需要查多个文件再通过合并compaction优化读取”。**这种设计天然适配“高写入、大吞吐、相对简单查询”的场景。你可以在留言板、交易流水、日志存储里大量写入偶尔按主键查出某条记录——这恰好是 Cassandra 最擅长的领域。如果业务需求是几百个维度的即席分析、复杂的 JOIN 查询、事务一致性要求非常高——那 Cassandra 不是最佳选择应该考虑其他方案。5. 数据建模与查询设计5.1 核心概念对照Cassandra 的数据模型用术语比较多我对照关系型数据库做一个通俗解释Cassandra对照 MySQL说明KeyspaceSchema库顶层命名空间定义复制策略Table表定义在 keyspace 中Partition Key主键索引键决定数据分布位置必须有Clustering Columns普通索引/排序键分区内部数据的排序方式Row / Column行 / 列基础数据单元Consistency Level事务级别自定义读/写一致性要求下面是一个典型的事务日志表的建表语句CREATE KEYSPACE IF NOT EXISTS app_log WITH replication {class: NetworkTopologyStrategy, dc1: 3}; CREATE TABLE app_log.event_record ( user_id text, event_time timestamp, event_type text, detail text, PRIMARY KEY (user_id, event_time) ) WITH CLUSTERING ORDER BY (event_time DESC);上面的表里分区键是 user_id数据按用户分散到不同节点聚合列是 event_time同一个用户的数据在分区内部按时间倒序排列。一次查询某个用户最近的事件会是这样的SELECT * FROM app_log.event_record WHERE user_id u_1001 ORDER BY event_time DESC LIMIT 20;5.2 建模心法查询驱动Cassandra 的建模思维和 MySQL 完全不同。在 MySQL 里你按实体建模然后用 JOIN 弥补数据分布的不足在 Cassandra 里JOIN 成本高且不支持所以要做的是按查询建表一张表回答一个查询模式。怎么理解这一点举个例子。你需要查询某个用户最近的10条订单。那你的核心信息应涵盖用户 ID 和订单时间并且按 user_id 分区、按 order_time 倒序排列。同样还是这一份订单数据如果你想查“最近7天下单量最多的商品”那需要另建一张以商品为分区键的统计表——两张表的数据可以来自同一个数据管道但结构完全不同。这是逆向建模先问“我要怎么查”再设计表结构。很多人刚上手时特别不习惯但一旦接受了这种设定你会发现 Cassandra 的查询性能非常稳定查询计划永远简单、索引简单、没有复杂的执行计划。5.3 排序、分页与 COUNT 的误区排序默认排序由聚合列决定建表时通过 CLUSTERING ORDER BY 设定。要注意跨分区全局排序是不可能的只能在分区内部排序。所以想“全表按时间排序”这种需求在 Cassandra 里做不了需要另想办法比如用预聚合表或者外接分析引擎。分页不要用 OFFSETCassandra 没有针对大偏移量的优化。正确做法是用基于主键或令牌的游标分页。驱动里有 paging state 机制直接可用。COUNTSELECT count(*) FROM table在数据量大时会非常慢因为它要全面扫描全表。生产环境不要用这个做统计应该维护计数器或者定期聚合。5.4 设计一个实际的表结构以我们团队做过的一个“用户登录日志系统”为例原始需求是查某个用户最近 50 条登录记录查某个用户某天的登录次数查全体用户当天登录总数针对需求 1建表如下CREATE TABLE user_login_log ( user_id text, login_time timestamp, ip text, device text, PRIMARY KEY (user_id, login_time) ) WITH CLUSTERING ORDER BY (login_time DESC) AND gc_grace_seconds 86400;针对需求 2建一张按“用户 天”聚合的统计表CREATE TABLE user_daily_login_count ( user_id text, day text, login_count counter, PRIMARY KEY (user_id, day) );更新时用将其作为计数器的UPDATE ... INCREMENT BY 1UPDATE user_daily_login_count SET login_count login_count 1 WHERE user_id u_1001 AND day 2025-04-01;针对需求 3再建一张小时/天粒度的全局聚合表用批处理任务扫描日志并刷新或者直接用 Kafka 流处理把计数写进 Cassandra。这样三张表各自服务一类查询每张表的访问路径都简单高效这就是 Cassandra 建模的核心思路。6. 运维经验与问题排查Cassandra 的运维门槛不算低但把常用命令和典型问题掌握好日常维护并不会吓人。下面是我踩坑最多的几个地方。6.1 监控指标到底该看什么用nodetool简单查看集群状态只是第一层。生产环境更得关注 JVM、存储与 Compaction 这几个维度。我常用的指标包括Pending Tasks 和 Compaction 速率若积压任务持续上升说明 Compaction 跟不上写入速度需要考虑调低写入速率或者扩容。JVM 堆使用长时间在高位运行容易触发 Full GCCassandra 的停顿越少越好一旦发现 Full GC 频繁优先排查缓存配置与墓碑堆积问题。磁盘 IO 使用率当 Compaction、协调流量与 Flush 同时发生时高峰期 IO 使用率特别容易飙升。这时适度降低compaction_throughput_mb_per_sec可以缓解压力但换取的是更长的 Compaction 时间。6.2 节点挂了怎么办节点出故障时状态会显示为 DN。先不要急着把它踢出集群。很多情况下只是暂时的网络抖动或长时间 Full GC 导致的“假死”。先观察几分钟然后通过nodetool status看是否自动恢复。如果确认节点彻底起不来了且数据副本数足够可以直接执行nodetool removenode 通过nodetool status看最后面显示的host_id在移除节点前确认剩余节点的副本数仍然满足数据安全。移除后调度重新平衡数据过程中的流量与 IO 压力需要留意。6.3 查询变慢多半采集业不注意查询变慢一般不是 Cassandra “跑不动”而是表结构或查询方式有问题。最常见的几个原因明明按主键查询但是漏了分区键全分区扫描慢是自然的事。没有建适合查询的聚合列使用了 ALLOW FILTERING结果在后台全表扫描。墓碑堆积过多上一节提到的 tombstone 问题查询时需要处理大量墓碑标记。使用了跨分区 ORDER BYCassandra 不支持全局排序这种请求要么被拒绝要么默认忽略排序需要靠应用层处理。解决思路回到数据建模这一层按照查询需求重新拆分表或者引入专门的分析系统。6.4 经典错误速查表错误情况原因处理方式节点加入失败出现“Could not reach any seed”种子节点 IP 配置错误或需要开启监听地址检查 seeds 配置、防火墙、端口 7000节点加入后没有数据还没有开始流式传输或者集群通信异常观察 stream 日志与 gossip 信息使用nodetool status查看大量超时副本过多、磁盘慢、负载过高检查 Compaction 队列调小 QUORUM考虑扩容读大量超时墓碑太多、查询全分区扫描用trace命令分析查询时长调整查询方式磁盘爆满数据增长、快照未清理定期清理旧快照nodetool clearsnapshot删除大片数据后查询依然很慢墓碑还在必要时手动nodetool compact7. 常见问题与调试技巧实录7.1 我用 trace 命令排查慢查询Cassandra 提供了tracing功能可以直接看某条 CQL 到底在哪一步耗了多久。在 CQL 客户端执行TRACING ON; SELECT * FROM user_login_log WHERE user_idu_1001;结果里会显示每一步操作及耗时。如果发现很多时间花在SSTABLE_READ阶段大概率是 SSTable 数量太多或 tombstone 太多如果时间花在Queued阶段则集群本身负载已经很高。这种定位思路比看系统监控更直接也更容易定位到你自己的表结构设计问题。7.2 Hinted Handoff 到底救不了多大规模Hinted Handoff 是一个很好的功能但它有上限。当节点宕机超过max_hint_window_in_ms默认 3 小时时新的写请求不会再排队等它这些期间的数据会缺失。所以长时间宕机后重新加入节点它不只是缺失数据还需要通过日常修复去补数据nodetool repair -pr我建议将 repair 纳入定期任务比如每周执行一次。如果不做长期运行后各副本的数据差距会被悄无声息放大即使使用 QUORUM也可能读到旧数据。7.3 大分区、热分区和反序列化如果某个分区键的值特别集中比如明星用户、热点商品会导致大量数据写入同一个节点形成热分区拖垮性能。常见的解决办法有加后缀将 user_id 拆成多个子分区查询时并行读取。比如user_id_00、user_id_01。用随机数作为聚合列的开头部分在时间粒度数据场景下将写入分散开但读取时也可以按时间合并。应用层缓存对热点数据将部分读请求分流到缓存减少对集群的压力。7.4 我常备的排查命令清单分享一个自己常用的小抄遇到 Cassandra 异常时快速定位nodetool status # 集群状态 nodetool info # 当前节点资源消耗 nodetool compactionstats # 压缩状态 nodetool tpstats # 线程池状态看是否有堆积 nodetool gossipinfo # gossip信息 cqlsh -e TRACING ON; SELECT ... # 追踪CQL耗时 du -sh /data/cassandra/data/* # 看哪些表占用空间最大 gcutil 或者 jstat -gcutil pid # 看JVM的GC情况这些都是运维里最基础、也最实用的工具熟悉它们比背各种配置参数重要得多。7.5 我最想提醒初学者的一句话一定不要用关系型数据库的习惯去设计 Cassandra 的表。把查询需求写在纸上然后一步一步反推分区键和聚类列把读和写分开设计把“一份数据多种查询”拆成“多张表各管一个查询”。我觉得这是所有 Cassandra 实践经验里最有价值的一条。很多人在技术细节上踩坑说到底是因为思维模式还没转换过来。Cassandra 的路径相对陡峭但一旦上手你会发现它在大规模写入和高可用场景下确实能让你省太多心。我从一个完全不知道“分区键和聚类列到底有什么区别”的状态到现在维护着几十个节点的集群过程中最大的感悟就是先搞清楚它为什么这么设计再动手用遇到问题才不至于慌。