分区:分而治之

42 阅读 1505 字 · 约 6 分钟

复制解决的是同一份数据放多份的问题,但数据量大到单机磁盘装不下时,复制帮不上忙——你总不能把 10TB 的数据完整塞进三台机器里,每台还是 10TB。这时候只能把数据切开,每台存一部分。这就是分区。

为什么需要分区

分区的动力来自容量。单机磁盘有上限,读请求可以靠复制分摊,但存储本身必须分散。分区把数据按某种规则切成片段,分布到不同节点上,每台机器只负责一部分。

分区和复制不矛盾——实际上它们经常一起出现。一个分区可能用主从复制保证高可用,一个集群里既有分区的拓扑,又有复制的关系。

怎么切:Range vs Hash

切法决定了一切。分区策略只有两类基础思路:按范围切,或者按哈希切。

按范围切(Range Partitioning)。 最直觉的做法——类似图书馆按字母顺序分架。key 在 A-M 的去第一个分区,N-Z 的去第二个分区。好处是范围查询很高效,SELECT * FROM orders WHERE date BETWEEN '2024-01' AND '2024-06' 只碰相关分区。坏处是热点——如果写入集中在最新时间戳,所有写请求都往最后一个分区灌。

按哈希切(Hash Partitioning)。 把 key 过一遍哈希函数,用哈希值决定落在哪个分区。好处是分布均匀——只要哈希函数合理,数据基本打散。坏处是范围查询废了——相邻 key 被哈希打到了不同分区,跨区查询要扫全表。

现实系统很少只用一种。Cassandra 用一致性哈希(哈希环),DynamoDB 也是类似思路。HBase 用范围分区。Kafka 的 partition 本质是按 key 哈希选 partition。

分区切法对比:按范围切(Range)范围查询高效但写入热点集中;按哈希切(Hash)分布均匀但范围查询废了

怎么路由

切完之后的第二个问题:客户端发出请求时,怎么知道去哪个节点找数据?

三种路线:

客户端自己算。 客户端持有分区元数据,自己算出 key 在哪个分片。最直接,但元数据变了得通知所有客户端。

中间层代理。 前面放一个路由层,请求先到路由层,它负责转发。MongoDB 的 mongos 就是代理模式。客户端简单,但多一跳。

节点转发。 客户端随便发到一个节点,节点内部转发到正确的分区。Cassandra 任意节点都能当协调者。弹性好,但多了一跳。

三者没有绝对的好坏——是复杂度放在哪里的选择。

动态再平衡

分区不是一成不变的。数据增长、机器增减,分区拓扑要跟着变。

固定分区数。 提前定好分区数(比如 100 个),节点多时每台多分几个,加节点时搬几个分区过去。Elasticsearch 的分片就是这个思路。简单,但分区数定下来就不能改。

动态分区。 分区达到阈值自动拆。HBase 的 region 拆分会自动触发。但拆分有窗口,拆分期间部分数据不可用。

一致性哈希。 加节点只影响相邻节点数据,不需要全量搬家。Cassandra 用 vnode 把每个物理节点映射成多个虚拟节点,数据迁移量均匀。

再平衡的核心痛点是:数据要搬家,搬家的开销和一致性怎么权衡。

分区与复制的交叉

分区和复制叠加时,复杂度不是加算,是乘算。每个分区有自己的复制拓扑(主从几副本),分区的 leader 和复制拓扑的 leader 要协调。加一个节点既是新分区的持有者,可能也是某个分区的从副本。Cassandra 和 Kafka 里这是日常——每个 partition 有自己的复制因子,每个 partition 的 leader 分布在不同的物理节点上。

小结

分区解决的是「数据太多」的问题。按范围切简单但容易热点,按哈希切均匀但丢了范围查询。路由选择本质上是把复杂度放在客户端、中间层还是服务端。再平衡是运维侧最头疼的事——数据搬家永远是贵的。

分区和复制解决的都是数据分散的问题,但角度不同:复制是同一份数据放多份,分区是不同数据放不同地方。两者叠加,才是多点存储的真实复杂度。