DDIA读书笔记-第六章

概要

分区的主要目的是提高可扩展性。不同的分区可以放在一个无共享集群的不同节点上,这样一个大数据集可以分散在更多的磁盘上。可以应对海量数据和更高的查询负载。

单分区查询时,每个节点对自己所在分区可以独立执行查询操作,更多的节点可以提高查询的吞吐量。

分区和复制

分区和复制通常结合使用,即每个分区在多个节点都有副本。某条记录属于特定分区,而同样的内容会保存在不同的节点上以提高容错。

  • 分区的角度看,主副本在一个节点上,从副本在其他节点上
  • 节点的角度看,一个节点既有主副本的分区,也有从副本的分区

键-值数据的分区

键值对是数据的一种最通用、泛化的表示,其他种类数据库都可以转化为键值对表示:

  1. 关系型数据库,primary key → row
  2. 文档型数据库,document id → document
  3. 图数据库,vertex id → vertex props, edge id → edge props

如果海量数据做切分,如何确定哪些记录放在哪些节点上呢?

分区的主要目标是数据和查询负载均匀分布在所有节点上。如果节点平均分担负载,那么理论上10个节点就可以处理10倍的数据量和10倍于单机的吞吐量。

但如果分区不均匀,产生倾斜 ,分区效率严重下降。比如10个节点,9个空闲,造成热点问题。

避免热点最简单就是随机分配,但是读取时,不知道数据保存在哪个节点,需要全量查询。可以改进为键-值数据模型,这样可以通过关键字来访问。

关键字区间分区

为每个分区分配一段连续的关键字或关键字区间范围(最小值和最大值表示)

关键字的区间段不一定要分布均匀,因为数据本身可能就不均匀,有的区间数据多,有的少,应结合具体数据本身的分布特征。

分区边界的确定可以手动,也可以自动(Rebalance)。

区间分区好处在于可以进行快速的范围查询。如,某个应用是保存传感器数据,并将时间戳作为键进行分区,则可轻松获取一段时间内(如某年,某月)的数据。

但坏处在于,数据分散不均匀,且容易造成热点。可能需要动态的调整的分区边界,以维护分片的相对均匀。比如以时间戳为 Key,按天的粒度进行分区,所有最新写入都被路由到当天的分区节点,造成严重的写入倾斜。一个解决办法是分级 或者混合,使用拼接主键,如使用传感器名称 + 时间戳作为主键,则可以将同时写入的多个传感器的数据分散到多机上去。

基于关键字hash值分区

关键字hash的方式可以很好避免倾斜和热点问题。

好的hash函数可以处理数据倾斜并使其分布均匀。并不需要加密很强。但是注意java内置的hashcode并不适合分区,同一个键在不同进程中可能返回不同的hash值。

这种根据hash值进行范围分区可以很好的将关键字均匀分配到多个分区。

还有一种常提的哈希方法叫做一致性哈希 。其特点是,会考虑逻辑分片和物理拓扑,将数据和物理节点按同样的哈希函数进行哈希,来决定如何将哈希分片路由到不同机器上。它可以避免在内存中维护逻辑分片到物理节点的映射,而是每次计算出来。即用一套算法同时解决了逻辑分片和物理路由的两个问题。

如果不使用一致性哈希,我们需要在元数据节点中,维护逻辑分片到物理节点的映射。则在某些物理节点宕机后,需要调整该映射并手动进行数据迁移,而不能像一致性哈希一样,半自动的增量式迁移。

哈希分片在获取均匀散列能力的同时,也丧失了基于键高效的范围查询能力。如书中说,MongoDB 中选择基于哈希的分区方式,范围查询就要发送到所有分区节点;Riak、Couchbase 或 Voldmort 干脆不支持主键的上的范围查询。

一种折中方式,和上小节一样,使用组合的方式,先散列,再顺序。如使用主键进行散列得到分区,在每个分区内使用其他列顺序存储。如在社交网络上,首先按 user_id 进行散列分区,再使用 update_time 对用户事件进行顺序排序,则可以通过 (user_id, update_timestamp) 高效查询某个用户一段事件的事件。

两种分区方式区别在于,一个使用应用相关值( Key )分区,一个使用应用无关值(Hash(key))分区,前者支持高效范围查询,后者可以均摊负载。但可使用多个字段,组合使用两种方式,使用一个字段进行分区,使用另一个字段在分区内进行排序,兼取两者优点

负载倾斜和热点

基于hash的分区可以减轻热点,但无法完全避免。极端情况下,大量读写请求都是针对同一个关键字,则所有请求被路由到同一个分区。

比如,大V的社交网站发布内容,天然引起同一个键的大量写入,造成热点问题,名人的用户id或者人们正在评论的事件id。此时数据层的hash无法解决热点问题。因为同一个id的hash一定相同。

这种负载的倾斜,一般需要应用层进行解决。如可以用拼接主键,对这些大 V 用户主键进行"分身",即在用户主键开始或者结尾添加一个随机数,两个十进制后缀就可以增加 100 中拆分可能。但这无疑需要应用层做额外的工作,请求时需要进行拆分,返回时需要进行合并。并且需要额外的元数据记录哪些关键字进行了这种特殊处理。

可能之后能开发出检测热点,自动拆分合并分区,以消除倾斜和热点。

分区和二级索引

二级索引:主键以外的列的索引。二级索引通常不能唯一标识一条记录,而是加速特定的查询。如查找所有颜色为红色的汽车。

KV 存储中,为了降低实现复杂度,一般不支持二级索引。但大部分场景,因为我们不可能只按单一维度对数据进行检索,因此二级索引很有用。尤其对于搜索场景,比如 Solr 和 Elasticsearch,二级索引(在搜索领域称为倒排索引)更是其实现基石。

二级索引的主要问题是很难规整的映射到分区中,有两种主要方法支持二级索引的分区:

  1. 基于文档的分区
  2. 基于词条的分区

基于文档的二级索引分区

书中举例二手车网站,使用文档id进行分区,现在用户按颜色和厂商搜索汽车,在颜色和厂商字段上建立二级索引。比如,每当一辆红色汽车添加到数据库时,数据库分区会将其添加到二级索引条目为"color:red"的文档id列表中。(es倒排索引呗)

这种索引方法也被称为本地索引,每个分区完全独立,各自维护自己的二级索引,且不关心其他分区中的数据。

  • 优点:变更文档时,只需要处理包含目标文档id的那个分区
  • 缺点:查询效率低,可能需要查询多个分区,并合并结果。

这种查询分区数据库的方法也被称为分散/聚集,代价昂贵,即便并行查询,也容易导致读延迟过大(某些分区可能处理较慢造成长尾)

基于词条的二级索引分区

另一种思路,构建全局索引,而不是每个分区维护本地索引。并且为避免单点称为瓶颈,也需要进行分区,放在多个节点上,但分区策略可以和主键不同。

如图,可以采用从a到r的颜色放在分区0,从s到z的颜色放在分区1。

这种索引方案称为词条分区。它以待查找的关键字本身作为索引,比如颜色color:red。

同样可以区间(范围)分区,或者hash分区,区别和之前提到的一样。高效的区间查询和更均匀的划分分区的权衡。

  • 优点:查询高效,无需扫描多个分区再聚合。
  • 缺点:写入速度慢,因为单个文档可能涉及多个二级索引,分布在多个分区,造成写放大。并且为避免多分区导致的写入延迟,多为异步更新,这又会带来不一致性的问题。如果保证强一致性,有需要引入跨分区的分布式事务(性能有损耗且复杂度高)。并且不是所有数据库都支持。

分区再均衡

数据库在运行过程中,数据和机器都会发生一些变化:

  1. 查询吞吐增加,需要增加机器以应对增加的负载。
  2. 数据集变大,需要增加磁盘和 RAM 来存储增加数据。
  3. 有机器故障,需要其他机器来转移故障机器数据。

所有这些问题都会引起数据分片在节点间的迁移,我们将之称为:再均衡(rebalancing)。对于 rebalancing 我们期望:

  1. 均衡后负载(存储、读写)在节点间均匀分布
  2. 均衡时不能禁止读写,并且尽量减小影响
  3. 尽量减少不必要的数据移动,尽量降低网络和磁盘 IO

动态再均衡的策略

不要使用取模

假设集群有 N 个节点,编号 0 ~ N-1,一条键为 key 的数据到来后,通过 hash(key) mod N 得到一个编号 n,然后将该数据发送到编号为 n 的机器上去。

为什么说这种策略不好呢?因为他不能应对机器数量的变化,如果要增删节点,就会有大量的数据需要发生迁移,否则,就不能保证数据在 hash(key) mod N 标号的机器上。在大规模集群中,机器节点增删比较频繁,这种策略更是不可接受

静态分区

分区数量是固定的,并且最好让分区数量大于(比如高一个数量级)机器节点,易于实现和维护。

静态分区中,让分区数量远大于机器节点的好处在于:

  1. 应对将来可能的扩容。加入分区数量等于机器数量,则将来增加机器,仅就单个数据集来说,并不能增加其存储容量和吞吐。
  2. 调度粒度更细,数据更容易均衡。举个例子,假设只有 20 个分区,然后有 9 个机器,假设每个分区数据量大致相同,则最均衡的情况,也会有两个机器数的数据量比其他机器多 50%;
  3. 应对集群中的异构性。比如集群中某些节点磁盘容量比其他机器大,则可以多分配几个分区到该机器上。

redis的集群方案咯?

新节点加入时,只需要改变分区和节点的对应关系,并且只需要迁移部分分区数据,这个过程可以逐步完成,在此期间,旧的分区数据并不受影响。

对于分区数量也不能太大,因为每个分区信息也是有管理成本的:比如元信息开销、均衡调度开销等。一般来说,可以取一个你将来集群可能扩展到的最多节点数量作为初始分区数量。

对于数据量会超预期增长的数据集,静态分区策略就会让用户进退两难,已经有很多数据,重新分区代价很大,不重新分区又难以应对数据量的进一步增长。

动态分区

对于按键区间进行分区的策略来说,由于数据在定义域内并不均匀分布 ,如果固定分区数量,则天然地难以均衡,可能出现某些分区倾斜严重。因此,按范围分区策略下,都会支持动态分区。设定单分区的下界上界。按生命周期来说:

  1. 开始,数据量很少,只有一个分区。
  2. 随着数据量不断增长,单个分区超过一定上界,则按尺寸一分为二,变成两个新的分区。
  3. 如果某个分区,数据删除过多,少于某个下界,则会和相邻分区合并

动态分区好处在于,小数据量使用少量分区,减少开销;大数据量增加分区,以均摊负载。

但同时,小数据量时,如果只有一个分区,会限制写入并发。因此,工程中有些数据库支持预分区(pre-splitting),如 HBase 和 MongoDB,即允许在空数据库中,配置最少量的初始分区,并确定每个分区的起止键。

另外,hash分区策略也可以支持动态分区,即,在哈希空间中对相邻数据集进行合并和分裂。

节点比例分区

前文所述,

  1. 静态均衡的分区数量一开始就固定的,但是单分区尺寸会随着总数量增大而增大。
  2. 动态均衡会按着数据量多少进行动态切合,单分区尺寸相对保持不变,一直于某个设定的上下界。

他们的分区数量都和数据集的大小有关,和集群节点数量没有直接关系。

另一种分区策略,保持总分区数量节点数量成正比,也即,保持每个节点分区数量不变。

假设集群有 m 个节点,每个节点有 n 个分区,在此种均衡策略下,当有新节点加入时,会从 m*n 个分区中随机选择 n 个分区,将其一分为二,一半由新节点分走,另一半留在原机器上。

随机选择,很容易产生有倾斜的分割。但如果 n 比较大,如 Cassandra 默认是 256,则新节点会比较容易均摊负载。

  • 为什么? 是因为可以从每个节点选同样数量的分区吗?比如说 n = 256,m = 16,则可以从每个节点选 16 分区吗?

随机选择分区,要求使用基于哈希的分区策略,这也是最接近原始一致性哈希的定义的方法。

自动和手动再均衡

再均衡应该自动还是手动完成呢?或者还有一个半自动的过渡阶段?

实践中,均衡是自动进行还是手动进行需要慎重考虑。

  1. 自动进行。系统自动检测是否均衡,然后自动决策搬迁策略以及搬迁时间。
  2. 手动进行。管理员指定迁移策略和迁移时间。

数据均衡是一项非常昂贵且易出错的操作,会给网络带来很大压力,甚至影正常负载。自动均衡诚然可以减少运维,但在实践中,如何有效甄别是否真的需要均衡(比如网络抖动了一段时间、节点宕机又重启、故障但能修复)是一个很复杂的事情,如果做出错误决策,就会带来大量无用的数据搬迁。

因此,数据均衡通常会半自动的进行,如系统通过负载情况给出搬迁策略,由管理员审核没问题后,决定某个时间段运行(避开正常流量高峰),Couchbase、Riak 和 Voldemort 便采用了类似做法。

请求路由

现在数据集分布到多个节点了,但是客户端请求时,如何知道应该连接哪个节点呢?并且再均衡的情况下,又如何路由请求呢?

本质上类似于服务发现,通常有几种处理策略:

  1. 每个节点都有全局路由表。客户端可以连接集群中任意一个节点,如该节点恰有该分区,则处理后返回;否则,根据路由表的路由信息,将其路由合适节点。
  2. 由一个专门的路由层来记录。客户端所有请求都打到路由层,路由层依据分区路由信息,将请求转发给相关节点。路由层只负责请求路由,并不处理具体逻辑。
  3. 让客户端感知分区到节点映射。客户端可以直接根据该映射,向某个节点发送请求。

不管哪种,核心问题都是做出路由决策的组件(节点,路由层或者客户端),如何知道分区和节点的对应关系并感知变化?也即,如何让所有节点就路由信息快速达成一致,业界有很多做法。
依赖外部协调组件。如 Zookeeper、Etcd,他们各自使用某种共识协议保持高可用,可以维护轻量的路由表,并提供发布订阅接口,在有路由信息更新时,让外部所有节点快速达成一致。

某种协议点对点同步。如 Dynamo、Cassandra 和 Riak 使用流言蜚语协议(Gossip Protocol)(redis的分片也是嘞),在集群内所有机器节点间就路由信息进行传播,并最终达成一致。

内部元数据服务器。如三节点的服务器,每个节点都存储一份路由数据,使用某种共识协议达成一致。

更简单一些,如 Couchbase 不支持自动的负载均衡,因此只需要使用一个路由层通过心跳从集群节点收集到所有路由信息即可。

当使用路由层(或者 Proxy 层,通常由多个实例构成),或者客户端将请求随机发动到某个集群节点时,客户端需要确定一个具体 IP 地址,但这些信息变化相对较少,因此直接使用 DNS 或者反向代理进行轮询即可。

并行查询执行

大部分 NoSQL 存储,所支持的查询都不太负载,如基于主键的查询、基于次级索引的 scatter/gather 查询。如前所述,都是针对单个键值非常简单的查询路由。

但对于关系型数据库产品,尤其是支持 大规模并行处理(MPP, Massively parallel processing) 数仓,一个查询语句在执行层要复杂的多,可能会:

  1. Stage:由多个阶段组成。
  2. Partition:每个阶段包含多个针对每个分区的并行的子查询计划。

数仓的大规模的快速并行执行是另一个需要专门讨论的话题,后面第十章会讲。

相关推荐
Yvemil71 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
2401_854391081 小时前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端
虽千万人 吾往矣1 小时前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
这孩子叫逆2 小时前
Spring Boot项目的创建与使用
java·spring boot·后端
coderWangbuer3 小时前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql
攸攸太上3 小时前
JMeter学习
java·后端·学习·jmeter·微服务
Kenny.志3 小时前
2、Spring Boot 3.x 集成 Feign
java·spring boot·后端
sky丶Mamba4 小时前
Spring Boot中获取application.yml中属性的几种方式
java·spring boot·后端
feng_xiaoshi4 小时前
【云原生】云原生架构的反模式
云原生·架构
千里码aicood5 小时前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统