CockroachDB架构-分布式层

本文知识点来源于官网地址https://www.cockroachlabs.com/docs/v22.1/architecture/distribution-layer.html

概览

CockroachDB(以下简称CRDB)将数据存储在key-value对组成的巨大的有序map结构。这个map里面描述了集群中的所有数据,以及数据的位置。数据及数据的位置信息(我们称之为meta数据)均以range为单位存储。所有的key都能在某一个range中找到具体的数据。

通过这个巨大的有序的map,可以实现:

  1. 简单查找:由于我们确定哪些节点负责存储哪些数据,查询可以快速定位到要查找的数据。
  2. 高效扫描:由于数据是顺序存放,很容易在扫描过程中找到特定的数据。

这个巨大的有序的map主要包括两部分内容:

  1. meta数据:描述集群中数据位置的元数据
  2. 用户数据:存储表的数据

Meta数据

集群中所有range的位置都存储在key空间开头的一个两级索引中,称为meta range。meta range中包含一级meta(meta1)及二级meta(meta2),meta1指向meta2,meta2指向集群中的数据。
可以用以下树型结构来理解两级索引和用户数据的关系:
CockroachDB架构-分布式层_第1张图片
当数据量不是特别大的时候,此时meta1和meta2存放在一个range当中,meta1只有一条元数据,指向meta2,meta2中每个元数据对应每个用户数据所在的每个range的信息。
每个range的最大容量是512MB。每个range的元数据信息大约占用256Byte空间,因此一个meta range可以容纳1PB左右的用户数据。
如果集群的实际数据确实比较大,如超过1PB,这时候meta range的512MB无法存放所有的range元数据,而meta range又是不可分割的,此时会用新的range来存储新产生的meta2数据。
meta range可以像普通range一样被处理,像集群中KV数据一样被访问和复制。
另外,每个节点都会缓存相关的meta数据,以提升对数据访问的效率。如果缓存中没有相关的meta数据,就通过常规读取来更新缓存数据。

用户数据

meta range后面的key空间就是集群中KV数据,也是以range为单位进行存储。
每个表及二级索引最开始映射到一个range,range中的每个key-value表示表中的一行数据。当一个range大小达到512MB时,它就分裂为两个range。当数据量持续增大时,后续会继续分裂。数据以range为单位进行复制(通过复制层),并将每个副本的地址存储在meta range中。

使用巨大的有序的map

当一个节点接收到请求时,它以自底向上的方式查询包含请求的key的range位置。其工作原理如下:

  1. 对于每个key,节点首先从本地缓存中查找包含指定key的range的位置。如果在缓存中找到range的位置,则立即返回它。
  2. 如果在本地缓存中没有找到以上数据,则需要进一步从缓存中去查找meta2所在的range的位置。如果在缓存中找到meta2所在的range的位置,节点向meta2的range发送一个RPC,以获取包含指定key的range的位置。
  3. 如果在本地缓存中没有找到meta2的range位置,节点将再进一步从缓存中查找meta1所在的range的位置。由于在实际情况下meta1的range位置一定是通过Gossip协议分布在集群各节点的缓存中,因此正常情况下都可以在缓存中找到此数据。关于Gossip协议,可以参考另外一篇文章介绍分布式一致性协议之Gossip
  4. 在从缓存中找到了meta1的range位置后,通过meta1找到指向的meta2的位置,最后找到要包含指定key的range的位置。

上面描述的过程是递归的,每次递归查找时,要么从缓存中获取一个位置,要么对树上“上一层”的值执行另一个查找。因为meta range是缓存的,通常情况下可以执行查找,而不需要向另一个节点发送RPC。

range描述符

CRDB每个range都有元数据,称为“range描述符”。range描述符由以下内容组成:

  1. 一个连续的RangeID。
  2. range包含的key集合,通常是表数据KV的第一个和最后一个索引列值。这个就是meta2 range的key。
  3. range及副本的地址。这个就是meta2 range的value。

由于range描述符包含meta2 range的key-value数据,所以每个节点的meta2缓存也缓存了range描述符。

range描述符会在以下几种情况下进行更新:

  1. range Raft组成员变更(具体见复制层)
  2. range分割
  3. range合并

所有range描述符的更新都在range所在节点的本地发生,然后传播到meta2 range。

Range拆分

默认情况下,CRDB尝试保持范围/副本为默认范围大小(当前为512 MiB)。一旦一个范围达到了这个限制,我们就把它分成两个更小的范围(由连续的键空间组成)。
在分割范围期间,节点创建一个新的Raft组,其中包含与分割范围相同的所有成员。现在有两个范围这一事实也意味着存在一个事务,它使用新的键空间边界更新meta2,以及使用范围描述符的节点地址。

基于负载的拆分

为了优化集群的性能,CRDB可以将频繁访问的键分割为更小的范围。与基于负载的再平衡一起,基于负载的拆分在集群中平均分配负载。

启动/禁用基于负载的拆分
可以通过以下参数设置来打开/关闭基于负载的拆分功能,默认为打开。

SET CLUSTER SETTING kv.range_split.by_load_enabled = false;

另外,可以通过以下参数来控制什么时候对range进行拆分,默认为2500。

SET CLUSTER SETTING kv.range_split.load_qps_threshold = 2000;

基于负载的拆分如何工作
当一个范围超出了设置的kv.range_split.load_qps_threshold,该范围适合基于负载的拆分。
在这一点上,开始收集每个关键指标,以确定拆分是否会基于以下启发式来提高集群的性能:

  • 平衡因素:如果您执行拆分,拆分的两边会有负载平衡吗?例如,如果99%的流量用于单个键,那么将其与范围内的其他键分离不会对性能产生实质性影响。但是,如果60%的查询是针对单个键的,那么将键移动到它自己的范围可能会使集群受益。
  • 分割交叉:如果执行分割,有多少查询需要跨越这个新的范围边界?因为在查询中涉及多个范围会比单个范围产生更大的开销,因此分割范围实际上会降低性能。例如,如果范围涉及多个SELECT COUNT(*)…如果将范围一分为二,可能会对性能产生负面影响。

Range合并

默认情况下,CRDB自动将小范围的数据合并到一起,形成更少、更大的范围(直到默认的范围大小)。这可以提高查询延迟和集群生存能力。

Range合并如何工作

CRDB将您的集群数据划分为许多范围。例如,您的集群可能有一个id在[1000,2000)之间的客户的范围。如果该范围超过默认的范围大小,则该范围将被分割为两个更小的范围。
但是,当您从集群中删除数据时,范围包含的数据可能远远小于默认范围大小。在集群的生命周期中,这可能导致许多小范围。
为了减少小范围的数量,您的集群可以有任何低于一定大小阈值的范围,尝试与它的“右邻居”合并,即从当前范围结束的地方开始的范围。使用上面的例子,这个范围的右邻居可能是id在[2000,3000)之间的客户的范围。
如果小范围和它的邻居的组合大小小于最大范围大小,范围合并为一个单一的范围。在我们的示例中,这将创建一个新的键范围[1000,3000)。

在CRDB中的查询必须联系查询中涉及的每个范围的副本。这对于有很多小范围的集群会产生以下问题:

  • 查询对于它们必须协调的每个范围的处理时间会产生固定的开销。
  • 拥有许多小范围可以增加您的查询必须与之协调的机器数量。这将使您的查询更有可能遇到网络延迟或节点过载等问题。

通过合并小范围,CRDB可以大大减少查询中涉及的范围的数量,从而减少查询延迟。

每当节点联机或脱机时,CRDB自动重新平衡集群中的范围分布。

  • 在重新平衡过程中,最好在节点间复制几个较大的范围,而不是许多较小的范围。复制更大的范围需要更少的协调,通常完成得更快。
  • 通过合并较小的范围,集群需要重新平衡的总范围更少。这最终会提高集群的性能,特别是在面对节点中断等可用性事件时。

你可能感兴趣的:(CockroachDB,分布式,架构,数据库)