ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Cassandra深度解析:分布式宽表数据库架构、数据模型与调优实践

Cassandra深度解析:分布式宽表数据库架构、数据模型与调优实践 先说个真实感受。很多刚接触运维和架构的朋友一听到 Cassandra第一反应就是“一个很能写的 NoSQL 数据库”但真正跑过生产环境、处理过节点宕机和跨机房容灾之后再回头看才会发现它最值钱的其实是那套去中心化的分布式设计。我最早维护 Cassandra 集群时踩过不少坑宽表设计不当导致分区过大、删除数据后墓碑堆积让读延迟飙升、误用 Leveled Compaction 把磁盘 IO 打满。这些经历让我意识到想搞懂 Cassandra真不是背几条 CQL 语句就行而是得先搞清楚它为什么这么设计然后在实际业务里顺着它的脾气来用。这篇文章写给正在做技术选型、入门分布式存储或者准备把 Cassandra 引入团队的开发者、运维和架构师。它是什么、解决了什么问题、数据模型怎么理解、部署和调优有哪些关键点我都会用自己的话讲清楚。最后一部分是我在实操中踩过的坑和排查思路直接拿来就能用能帮你少走很多弯路。1. 先搞清楚 Cassandra 到底是什么1.1 从 NoSQL 大家族里认识它的位置Cassandra 是一个开源的分布式 NoSQL 宽列数据库最初由 Facebook 开发用来解决 Inbox Search 的海量数据存储问题后来捐赠给 Apache 基金会现在是 Apache 顶级项目。它把数据自动分布到集群里的多个节点每个节点地位平等没有主从之分所以天生没有单点故障。它采用最终一致性模型在多数场景下读写表现都非常稳定尤其是写入吞吐线性扩展能力几乎可以说是它的招牌。很多人容易把它和 HBase 混在一起。两者确实都属于宽列模型但底层架构思路差别很大。HBase 依赖 HDFS 做持久化通过 ZooKeeper 协调节点状态是一种典型的主从架构RegionServer 需要依赖 Master 节点。而 Cassandra 走的是 P2P 去中心化路线没有外部依赖每个节点自己存放数据、自己参与路由节点之间通过 Gossip 协议通信。这个区别直接决定了部署体验Cassandra 装好一个节点就能跑起来再拉几个节点就能组集群不需要单独管理一套元数据服务。从实际使用场景看Cassandra 非常适合写入量大、数据规模横向扩展要求高的业务比如时序数据、消息存储、订单流水、用户行为日志、物联网数据。它的查询方式偏向按主键获取如果你想做任意字段的复杂 join它并不合适。这个定位决定了它是一个“场景型数据库”而不是“全能型数据库”。1.2 核心特征无主架构、线性扩展、可调一致性我把 Cassandra 最核心的几个特征拆开讲。首先是无主架构。集群里每个节点都能接受读写请求数据按照分区键的哈希值散列到不同节点上节点之间互为副本。某个节点挂了请求会自动路由到其他持有副本的节点不会出现“主节点一挂整个集群瘫痪”的情况。对于生产环境这种设计带来的信心是实打实的。其次是线性扩展。很多传统数据库加节点之后先要拆分数据、再迁移数据麻烦不说性能还不一定提升。Cassandra 加节点的操作简单很多新节点加进来后会从现有节点复制部分数据然后逐渐接管属于自己 token range 的数据。数据量越大节点越多写入吞吐基本上跟着节点数走直线上升。我见过很多团队在压测时发现 3 节点到 6 节点写入吞吐几乎翻倍这种扩展效率在其它 NoSQL 里很难复制。然后是可调一致性。Cassandra 允许你在同一个事务里指定到底要等多少个副本确认才算成功。写一条数据可以只用写一个节点就返回也可以等全部副本都写完才返回。这个参数用一个叫 Consistency Level 的东西控制后面我会专门讲。它最大的价值是让你根据业务场景去权衡延迟和数据安全而不是数据库替你拍板。还有一点容易被忽略它把容错和运维能力做进系统底层比如 Hinted Handoff节点恢复后补发写入、读修复读取时对比多个副本并自动修复不一致数据和 Gossip节点状态自动发现。这些机制让 Cassandra 在弱网络、多机房环境下依然能自我修复这一点对生产系统非常重要。1.3 和其他数据库的简单对比如果你是刚开始选型下面这张表可以帮助快速理清差异。我按实际使用中大家最关心的几个维度来对比维度CassandraHBaseMongoDBMySQL 分库分表数据模型宽列宽列文档关系行表集群架构P2P 无主主从 HDFS主从/分片中间件分片写入扩展极佳线性扩展较好依赖 Region 划分一般分片有复杂度需要业务配合拆分高可用自动副本、无单点依赖 HDFS 和 ZK副本集可自动切换需要外部方案查询方式CQL按主键为主API/SQL 插件丰富查询、索引完整 SQL强事务支持仅单分区/LWT 有限支持单行事务有限支持事务分片有局限完整 ACID适合场景海量写入、时间序列数据量巨大但依赖 Hadoop灵活多变的文档模型传统 OA/电商核心这张表不是要分个高下而是帮大家找到思考坐标。Cassandra 和 MongoDB 虽然都是 NoSQL但定位完全不同MongoDB 适合开发效率要高、查询条件灵活的场景而数据规模大到一定程度后写入和横向扩展能力才是 Cassandra 的舒适区。MySQL 分库分表在一致性要求极高的金融、交易系统里仍然是合理选择但需要业务代码配合维护复杂度并不低。2. 数据模型和 CQL最容易被搞晕的地方2.1 主键、分区键和聚类键的关系Cassandra 的外表很像关系表有 Keyspace类似数据库、Table、Row、Column查询语言也长得像 SQL但底层的逻辑跟 MySQL 完全不一样。如果你带着关系型数据库的惯性去设计 Cassandra 的表十有八九会踩坑。先说最关键的主键概念。Cassandra 里一张表的主键Primary Key由两部分组成分区键Partition Key和聚类键Clustering Column。分区键决定了这条数据会被保存在集群中的哪一个节点范围聚类键则决定了在同一个分区内数据按照什么顺序排列。我举个生活化的类比假设你要管理全国所有城市的人分区键就是“城市名”数据先按城市把人的档案分到不同仓库聚类键就是“门牌号”同在一个仓库里的人按门牌号排序这样管理既分散又有序。Cassandra 还支持组合分区键用括号把多个字段包起来表示它们一起组成一个分区键。这是容易栽跟头的地方PRIMARY KEY ((a, b), c)表示 a 和 b 共同决定一个分区c 是分区内的聚类键而PRIMARY KEY (a, b, c)则完全不同它只有 a 是分区键b 和 c 都是聚类键。这两个写法差一个字数据分布天差地别。我见过有人把多列主键理解错结果所有数据都挤到一个分区里直接造成热点问题。除了主键字段Cassandra 里其他普通列都是“可选的”每行并不需要所有列都有值。这与传统数据库的“表结构必须齐整”也有本质区别。不过在实际设计中还是建议尽量保持一个稳定、规范的 schema不然读取和排查都会很痛苦。2.2 使用 CQL 建表并理解物理布局下面我用一张电商订单表来演示 Cassandra 的 CQL 用法。先建一个 Keyspace相当于传统数据库里的逻辑命名空间CREATE KEYSPACE shop WITH replication { class: SimpleStrategy, replication_factor: 3 };这里的SimplexStrategy适合单机房测试环境生产多机房环境建议使用NetworkTopologyStrategy指定每个数据中心保留几个副本例如{class: NetworkTopologyStrategy, dc1: 3, dc2: 2}。简单说副本策略就是决定“一份数据在多少个节点上各存一份”。然后建订单表USE shop; CREATE TABLE orders ( customer_id text, order_ts timestamp, order_id text, amount double, status text, PRIMARY KEY (customer_id, order_ts, order_id) ) WITH CLUSTERING ORDER BY (order_ts DESC);我先解释一下这张表customer_id是分区键也就是所有属于同一个客户的订单都会存在同一个物理分区里这样可以保证按客户查询时只需要读一个分区order_ts是第一个聚类键同一个客户的分区内按订单时间倒序排列为什么还要加order_id作为第二个聚类键因为同一毫秒内客户下多笔订单时时间戳可能重复加上订单号可以保证行记录的唯一性。当你要查某个客户最近 20 笔订单时CQL 可以这样写SELECT * FROM orders WHERE customer_id user_1001 ORDER BY order_ts DESC LIMIT 20;这其实就是在单个分区内按顺序扫描前 20 行速度非常快。如果换到 MySQL 里你可能需要在(customer_id, order_ts)上建二级索引但在 Cassandra 里主键设计本身已经决定了这个查询只需要访问一个分区。2.3 宽行与反范式设计思维Cassandra 禁止 join同时也不建议使用二级索引直接使用效率不高且容易产生全集群扫描。在这个限制下应对复杂查询的思路只有一条用空间换时间提前把数据冗余成多张表。专业说法叫“反范式设计”听起来高大上本质上就是“数据写入时多写几份读取时按需取”。举个例子同样的订单业务如果经常需要展示某个用户的订单列表就建一张orders_by_customer如果运营经常要查某个商品在某个时间段的销量就建一张sales_by_product。两张表存的是不同视角的同一批数据写入时同时更新业务侧自己保证一致性。这个模式在 Cassandra 社区叫materialized view的思路但手动管理比依赖物化视图更可控尤其是涉及到多表事务的时候不要指望数据库帮你维护。反范式设计有一个核心原则先列出高频查询语句再为每条查询设计一张表。不要先设计表再反过来想怎么查询。很多实践者用 Cassandra 做时间序列数据天然就会为不同的时间粒度维护多张表比如minutes_data、hourly_data、daily_data每张表有自己的分区键和时间桶读取时按统计算效果很好。3. 架构与一致性分布式设计的成败关键3.1 节点、Token Range 与 Gossip 协议在正式理解 Cassandra 的架构之前需要先知道数据在集群里如何分布。Cassandra 把整个集群想象成一个环Ring每个节点负责一段 token 范围。每个分区键通过哈希函数计算出一个 token这个 token 落到哪个范围数据就被分配到对应节点上。默认的哈希分区方式是 Murmur3Partitioner可以把分布做得比较均匀。节点之间要维护这个环的信息靠的就是 Gossip 协议。这个协议类似于社交网络里的口耳相传每个节点每隔一秒随机和其他几个节点交换自己的状态信息几轮下来所有节点都能知道整个集群里有哪些节点在线、哪些节点离线、各自负责哪些 token。这个机制的好处是没有中心节点任何一个节点挂了都不影响全局认知坏处是状态同步有一点延迟不过在正常网络下有足够的生产价值。为了让数据分布更均匀Cassandra 还支持 Vnodes。每个物理节点默认拥有 256 个 token 范围这意味着一个大节点实际上参与了 256 个虚拟节点的数据分布。使用 Vnodes 之后集群扩容或调优数据平衡时移动的数据粒度和速度都更快而且节点间的数据倾斜问题明显减少。在节点数量少于 10 的集群中Vnodes 的优势尤其明显。3.2 副本与一致性等级怎么权衡读写延迟数据写入时Cassandra 不会只写一个节点而是会写入多个副本。副本数由建 Keyspace 时的replication_factor决定。比如replication_factor3意味着同一份数据会存在 3 个节点上。副本策略我们前面说过多机房要用NetworkTopologyStrategy因为副本会优先落在不同机架的节点上减少同机房故障带来的数据丢失风险。读一条数据时客户端会向持有副本的多个节点发起请求然后根据一致性等级判断是否需要等待多个节点的响应。一致性等级Consistency Level是 Cassandra 最灵活也最需要理解的参数之一。常见的有ONE只等一个副本返回成功延迟最低但数据可能不是最新的。QUORUM需要大多数副本复制数/2 1返回成功兼顾延迟与一致性。LOCAL_QUORUM只要求当前数据中心的大多数副本成功一般用于避免跨机房时延。EACH_QUORUM每个数据中心都要达到 Quorum适合强一致性需求代价是写入延迟随 DC 数增加。ALL要求所有副本都成功任何一台副本宕机时写入整体不可用不推荐生产使用。ANY只要有一个节点记录了这个写操作就算成功甚至可以保存在 Hint 里延迟最低但可能丢失数据。大多数生产环境我习惯用LOCAL_QUORUM写入LOCAL_QUORUM读取。这个组合既保证了写数据前大多数本地副本确认也避免了跨机房的额外延迟适合绝大多数多机房部署。如果业务能够接受偶尔读到旧数据可以进一步降为ONE延迟更漂亮但数据一致性风险一定要评估清楚。3.3 Hinted Handoff、读修复与轻量事务Cassandra 的高可用不只是靠副本还靠几个自我修复机制。最常用的就是 Hinted Handoff当一个节点临时宕机时另一个节点会替它保存它该收到的写操作等宕机节点恢复后再把这些写操作补发过去。这个机制让写入请求在单节点故障时依然可以快速返回不至于直接报错。读修复Read Repair则是在读取路径上做数据一致性修正。如果客户端读取时发现不同副本上的数据不一致比如某副本落后了读修复会同步最新数据并覆盖旧数据。这个机制是最终一致性模型的重要支撑它确保即使节点之间偶尔不同步只要持续有读请求数据也会逐渐恢复一致。Cassandra 还支持轻量事务Lightweight Transaction通过 Paxos 协议实现“比较并设置”Compare-and-SetCAS操作。比如用 CQL 执行INSERT INTO users (id, email) VALUES (1, newexample.com) IF NOT EXISTS;这个操作可以做到“只有当主键不存在时才写入”。虽然能实现真正意义上的原子操作但它的代价是四次网络往返吞吐量远低于普通写入。我做项目的经验是尽量只在极少数需要防重复插入的场景使用比如创建用户、分配唯一 ID普通数据写入不需要用它否则把 Cassandra 最擅长的写入性能白白浪费掉。4. 实操过程与核心步骤从部署到写入再看优化4.1 快速部署一个单节点 Cassandra如果你还没在本地玩过 Cassandra强烈建议先搭一个单节点环境把 CQL 和底层配置都过一遍。单节点部署很简单下载 Apache Cassandra 二进制包解压之后就能用。目前稳定版本主线是 4.0/4.1相比 3.x 在内存管理、读路径优化和运维接口上都有明显提升建议直接用 4.x。主要改conf/cassandra.yaml里的三个参数cluster_name: My Test Cluster listen_address: 127.0.0.1 rpc_address: 127.0.0.1listen_address是节点之间通信的 IPrpc_address是客户端访问的 IP单机测试都配为本机即可。如果是在云环境部署最好配置成内网 IP并确保 7000 端口、9042 端口和 9160 端口可访问。启动命令就是进入解压目录后cd apache-cassandra-4.1.3 bin/cassandra -R启动后可以用nodetool status检查状态bin/nodetool status看到UN表示节点正常。如果显示DN就要看日志确认是不是配置问题。这里有个易错点如果你在笔记本上启动默认的 JVM 堆内存设置会给你 1GB 左右跑着跑着容易内存溢出。开发环境建议在conf/jvm-server.options或cassandra-env.sh里调小堆内存比如设置为 512MB够跑测试就行。4.2 使用 CQLSH 完成建表、写入与查询启动好 Cassandra 后使用自带的 CQLSH 客户端进入交互环境bin/cqlsh 127.0.0.1 9042下面我完整走一遍订单表从创建到查询的过程。先建 KeyspaceCREATE KEYSPACE shop WITH replication { class: SimpleStrategy, replication_factor: 1 };单节点环境副本数只能设为 1多了也没有实际副本能放但生产上不要省这个数。接着建表USE shop; CREATE TABLE orders ( customer_id text, order_ts timestamp, order_id text, amount double, status text, PRIMARY KEY (customer_id, order_ts, order_id) ) WITH CLUSTERING ORDER BY (order_ts DESC);插入数据INSERT INTO orders (customer_id, order_ts, order_id, amount, status) VALUES (user_1001, 2024-06-01 10:00:00, order_001, 199.00, PAID);查询时注意Cassandra 查询必须包含分区键这是它语法上和 MySQL 差异最大的地方。下面这个查询可以正常运行SELECT * FROM orders WHERE customer_id user_1001;因为你只用了分区键Cassandra 能把请求定位到对应的分区然后扫描里面所有行。如果你写SELECT * FROM orders WHERE status PAID;就会报错或者需要加ALLOW FILTERING。ALLOW FILTERING会在集群全部节点上扫描数据量稍大一点就会导致明显的性能问题所以我一般看到刚入门的人写这个都会先帮他们改表设计。4.3 性能优化参数与经验心得Cassandra 有一堆配置参数看起来复杂但核心的其实没几个。我分享一下在压测和调优中真正影响体验的几项。首先是并发参数。concurrent_reads和concurrent_writes控制每个节点处理读写请求的线程数。默认值是 32如果你的机器是 16 核、SSD可以把写并发调到 64 或 128读并发调到 32 或 64。但不要盲目调大因为 Cassandra 的线程模型不是越多越好超过临界点后反而会因为上下文切换和内存竞争而掉性能。然后是Memtable 刷盘策略。Cassandra 的写入先写 Memtable内存表积累到一定大小后再刷成 SSTable。memtable_heap_space_in_mb控制堆内存用于 Memtable 的上限默认值偏保守建议结合 JVM 堆大小做调整。如果堆有 4GB可以给到 1GB 左右如果太小刷盘会很频繁IO 压力大。接着是Compaction 策略。这是影响磁盘和读性能的大头。默认的SizeTieredCompactionStrategySTCS适合写入量大、分区大小不太均匀的场景LeveledCompactionStrategyLCS适合读多写少、需要压缩读放大率的场景TimeWindowCompactionStrategyTWCS专门给时间序列数据使用能让过期数据进入严格的 time window 再统一压缩。我个人的经验是如果是纯粹的时序数据直接用 TWCS查询和删除效率都高如果是通用业务数据先用 STCS 跑一阵观察平均 SSTable 数和读取延迟再考虑换 LCS。在生产环境盲目切 LCS很可能写放大很严重SSD 寿命也受影响。还有一点是关闭不必要的缓存。key_cache和row_cache各有用途但 row_cache 在容量大、数据量更大的场景里几乎帮不上忙反而频繁产生 GC。我们团队在生产环境直接调小避免压测时内存被缓存吃满。5. 常见问题与排查技巧实录5.1 读写超时到底是节点堵了还是副本不够在 Cassandra 上跑业务最容易遇到的就是写超时和读超时。写超时的表象是WriteTimeoutException日志里有类似Client request timeout. Response not received before...的错误。出现这种问题我一般按下面的思路排查先看nodetool status确认集群里每个节点是否都处于UN状态。再看nodetool tpstats重点看MutationStage和ReadStage的线程池是否堆积大量 pending 请求。如果线程池 pending 很高说明节点处理不过来优先排查 GC 停顿和磁盘 IO而不是加节点。如果节点状态正常检查请求的一致性等级是不是设置得太高比如在跨机房场景用了EACH_QUORUM网络一波动就超时。读写超时要区分“节点能力不够”和“一致性等级设置太激进”两种情况。我见过一个业务测压测写吞吐一压就超时nodetool status显示节点状态正常后来把一致性等级从QUORUM降为LOCAL_QUORUM延迟和成功率瞬间就恢复了。所以说参数和业务期望要匹配不是越高越好。5.2 数据倾斜和热点问题Cassandra 设计得好时数据分布是均匀的但设计不好时某个节点会被打爆其他节点闲死。这种情况常常发生在分区键选择不当。比如你用用户 ID 作为分区键但少数超级用户的访问量巨大这些用户对应的分区就成了热点。我见过一个微博类应用明星账号下面的数据量巨大所有关注者读取同一个分区直接把节点 IO 打满。解决方案有几种如果是热点写入可以在分区键后加一个随机数后缀把数据分散到多个分区。但副作用是读的时候不知道该去哪个分区找需要额外的映射表来记录。如果是热点读取可以考虑对数据做缓存而不是把所有读请求都打到 Cassandra。如果分区键本身基数不高比如只有几十个枚举值就把组合分区键设计成多个字段增加基数。数据倾斜还有一个常见原因没有使用 Vnodes。旧版本每个节点只有一个 token扩容后新节点容纳的数据范围很小需要手动调整。4.x 默认开启 Vnodes但不代表完全免疫如果某个分区极其巨大仍然会出现“一个大分区压垮一个节点”。所以分区大小要控制在几百 MB 以内这个我在团队里是硬性要求。5.3 删除操作并不省事墓碑与压缩Cassandra 的删除和传统数据库完全不同。它不会立刻抹掉数据而是写入一条被称为 Tombstone 的标记信息告诉读路径“这条记录已经删了”。下次读到这个 tombstone 时就知道要跳过这条数据。这个设计支撑了 Cassandra 的最终一致性和多副本同步短期来看是合理的但长期堆积会带来两个问题读查询扫描时会跳过大量 tombstone导致读延迟升高。磁盘空间不会马上释放必须等 Compaction 把 tombstone 真正清理掉。我在项目里频繁遇到用户执行DELETE FROM table WHERE id xxx;后发现磁盘空间没有下降这是因为 tombstone 还没被压缩掉。想解决这个问题可以主动执行nodetool compact keyspace table但这操作在生产环境会带来很大的 IO 负担不要轻易用。更好的做法是在建表时设置合理的gc_grace_seconds。默认值是 864000 秒也就是 10 天这段时间内 tombstone 不会清理是为了给离线节点留出恢复时间。如果你确认数据不会存在长时间不可恢复的节点离线可以把该值调小到 1-2 天能明显减少 tombstone 堆积。另外如果业务上有“定期清掉旧数据”的需求建议不要一条条删除而是用时间桶进行分区级删除。比如按天分表某天过期后直接DROP TABLE daily_data_20240601这样避免海量 tombstoneCPU 和磁盘压力都很小。5.4 运维必读Nodetool 命令和避坑清单Cassandra 的运维没有太多花哨功夫最核心的就是把nodetool命令用熟。我平时用得最多的几个命令用途常用时机nodetool status查看节点状态和负载任何异常排查第一步nodetool tpstats查看线程池堆积情况读写超时、性能下降nodetool cfstats查看表级别的数据量和 tombstone 统计判断表设计是否合理nodetool compactionstats查看正在进行的压缩任务压缩卡住或 IO 高nodetool repair手动修复数据副本不一致数据不一致、节点长期离线后nodetool gossipinfo查看 Gossip 状态和节点心跳节点间网络异常排查关于repair我要特别提醒一句Cassandra 的副本一致性不会在后台自动完全修复读修复只能修复读到的那部分数据。所以生产环境一定要定时做全量 repair比如每周一次否则副本间的数据差异会慢慢变大等节点宕机切换后才暴露问题那时候就晚了。避坑清单最后记三条永远不要把gc_grace_seconds设成 0除非你非常清楚自己在做什么。不要在线上频繁执行nodetool compact它会触发严重的磁盘占用和 IO 抖动。查询必须带分区键。一次全集群扫描就可能把一台高性能机器打挂CQL 再怎么友好也要尊重底层分布式的限制。我自己的体会是Cassandra 不是一个拿来就能跑的“省心数据库”它需要你理解数据分布、副本策略、一致性等级这三件事才能真正把它的性能优势发挥出来。用好的时候它能在几百 TB 数据上保持流畅读写用不好分区热点和 tombstone 会把你折腾到崩溃。如果你正准备上手建议先拿时间序列数据练手建一张按天分桶的宽表压一压写入和按时间范围查询这会比任何文档都更直观地让你理解 Cassandra 的设计哲学。
返回列表