开源数据库CockroachDB(二)

上一篇:开源数据库CockroachDB(一)

转载自:CockroachDB中国社区

七、逻辑Map内容

逻辑上,Logical Map在用户实际数据前包含了一系列系统保留key-value对(由SQL子系统管理):

  • \x02<key1>: Range metadata for range ending \x03<key1>. This a “meta1” key.
  • \x02<keyN>: Range metadata for range ending \x03<keyN>. This a “meta1” key.
  • \x03<key1>: Range metadata for range ending <key1>. This a “meta2” key.
  • \x03<keyN>: Range metadata for range ending <keyN>. This a “meta2” key.
  • \x04{desc,node,range,store}-idegen: ID generation oracles for various component types.
  • \x04status-node-<varint encoded Store ID>: Store runtime metadata.
  • \x04tsd<key>: Time-series data key.
  • <key>: A user key. In practice, these keys are managed by the SQL subsystem, which employs its own key anatomy.

八、Store和存储

CockroachDB节点包含一个或多个Store,每个Store独享一块磁盘,每个Store对应一个RocksDB实例。该实例与节点中其它Store共享缓存。这些Store包含一批Range的副本。同一个Range的多个副本不会位于相同节点上。

当集群第一个节点初始化时,Range只有一个副本,当其它节点加入,这些Range将被复制到其它节点,直到达到指定副本数(默认为3)。使用zone config控制Range复制策略,通过Constraint控制Range副本位置。当Range的zone config改变时,Range相应调整其副本数,或根据Constaint把副本移动到合适Store上。

九、自修复

如果一段时间内(默认5分钟),其它Store都没有接收到某个Store的心跳消息,集群则认为该Store故障。此时,该Store上的所有Range副本都被认为不可用且已被删除。这些被认为已删除的Range副本将会通过其他可用Store中的副本进行复制,直到达到预定的副本数。如果在同一时间内有超过50%或更多的副本不可用,那么整个集群将被认为是不可用的,直到至少超过一半的副本恢复可用为止。

十、副本均衡

随着数据量增长,Store之间的数据分布可能会变得相对不均匀。为了均衡整个集群负载,副本将根据配置的复制系数在Store之间动态调整以维持均衡。Rebalance的考量因素包括:

  • 每个Store的副本数量
  • 每个Store的数据总量
  • 每个Store剩余空间大小

将来可能会考虑更多因素,包括:

  • 每个Store的CPU和网络负载
  • 在查询中经常被同时使用的Range
  • 每个Store中活跃的Range数量
  • 每个Store持有的Range租约数量

十二、Range元数据

一个Range默认大小是64M (2^26 B), 要支持1PB(2^50 B)的数据大概需要2^(50 – 26) = 2^24 = 1600万个Range。一个Range元数据最大256字节是比较合理的(其中3*12字节用来保存3个节点的位置,余下220字节保存此Range自已的key)。 上述1600万个Range,如果每个Range元数据大小按256字节计算,则总共需要占用约4G (2^32 B)字节,在各个节点间进行复制开销太大。所以,对于数据量很大的集群,Range元数据的存储也必须是分布式的。

因为元数据是分布式存储的,为了保证key的查找效率,我们把所有顶层的元数据保存在一个Range里(第一个Range). 顶层元数据的key以 meta1为前缀 ,使得它们位于Key空间的前面。 一条元数据占用256字节,一个64M的Range可以保存 64M/256B = 2^18 = 256K个Range的元数据,总共可以提供64M * 2^18 =16T的存储空间。为了提供更大的存储空间,我们采用两级寻址(两级索引 ,第一级用来保存第二级的地址,第二级用来保存数据)。 通过两级寻址,可以支持2^(18 + 18) = 64G个Range, 每个Range大小为2^26 B = 64M ,则总共可寻址范围2^(36+26) B = 2^62 B = 4E 的数据空间.

对于一个用户给定的key1 ,在meta1寻址空间中,可以通过key1的successor key定位到对应的 meta1 记录。successor key指大于key1的第一条meta1记录的key。 meta1 record指向meta2 record所在的Range。meta2 record则指向包含了key1的Range。

具体而言:元数据键以x02(meta1)和x03(meta2)为前缀; 前缀x02和x03保证了meta1与meta2数据有序。因此,key1的meta1 record位于\x02<key1>的 successor key之前。

注意: 我们在meta1和meta2上追加保存每个Range的最后一个key ,是因为 RocksDB的迭代器只提供一个Seek()接口,功能类似Ceil()。如果只使用Range的第一个key调用Seek()查询meta1和meta2上的key,会导致回退查找,这样做效率不高,并且部分场景不适用。

下面展示一个有三个Range的元数据索引结构。省略号表示其他key/value对。为了简单起见,下面的例子使用meta1和meta2分别指代前缀\x02和\x03。Range分裂时会将Range相关的信息更新至Range元数据中,这些元数据不需要特殊对待。

Range 0 (located on servers dcrama1:8000, dcrama2:8000, dcrama3:8000)

  • meta1\xff: dcrama1:8000, dcrama2:8000, dcrama3:8000
  • meta2<lastkey0>: dcrama1:8000, dcrama2:8000, dcrama3:8000
  • meta2<lastkey1>: dcrama4:8000, dcrama5:8000, dcrama6:8000
  • meta2\xff: dcrama7:8000, dcrama8:8000, dcrama9:8000
  • <lastkey0>: <lastvalue0>

Range 1 (located on servers dcrama4:8000, dcrama5:8000, dcrama6:8000)

  • <lastkey1>: <lastvalue1>

Range 2 (located on servers dcrama7:8000, dcrama8:8000, dcrama9:8000)

  • <lastkey2>: <lastvalue2>

如果集群的数据量极小,未达到一个Range的容量上限,则所有的元数据和数据都会存放于同一个Range中。

Range 0 (located on servers dcrama1:8000, dcrama2:8000, dcrama3:8000)*

  • meta1\xff: dcrama1:8000, dcrama2:8000, dcrama3:8000
  • meta2\xff: dcrama1:8000, dcrama2:8000, dcrama3:8000
  • <key0>: <value0>

最终,如果数据足够多,那么数据存储的层次结构将如下所示( 本示例没有体现Range副本,只简单用Range编号标识Range) :

Range 0

  • meta1<lastkeyN-1>: Range 0
  • meta1\xff: Range 1
  • meta2<lastkey1>: Range 1
  • meta2<lastkey2>: Range 2
  • meta2<lastkey3>: Range 3
  • meta2<lastkeyN-1>: Range 262143

Range 1

  • meta2<lastkeyN>: Range 262144
  • meta2<lastkeyN+1>: Range 262145
  • meta2\xff: Range 500,000
  • <lastkey1>: <lastvalue1>

Range 2

  • <lastkey2>: <lastvalue2>

Range 3

  • <lastkey3>: <lastvalue3>

Range 262144

  • <lastkeyN>: <lastvalueN>

Range 262145

  • <lastkeyN+1>: <lastvalueN+1>

上面Range 262144 只是一个近似值。一个元数据Range 真正可寻址的范围取决于key的长度。如果key长度足够小,可寻址范围会增加,反之亦然。

如上所示,对一个key的寻址最多需要3次read就可获取<key>对应的value.

  1. lower bound of meta1<key>
  2. lower bound of meta2<key>,
  3. <key>.

对于很小的map, 可以在Range 0上通过一次RPC调用完成查询。 16T以下的Map需要两次。节点可以缓存所有Range元数据, 我们希望节点的数据局部性越高越好。同时,节点缓存的元数据可能失效。 在一次查询中,如果通过缓存查找Range没有命中,节点就会删除缓存中对应的失效元数据,并重新发起一个新的查询。

十三、Raft – Range副本一致性

每一个Range副本数由ZoneConfig配置,可以包含三个或更多的副本。Range中的每个副本都会维护自己的一致性算法状态机。我们采用《Raft consensus algorithm 》作为一致性算法, 因为它实现简单,容易理解,并且论文中包含了实现的全部重要细节。ePaxos 对于WAN分布式副本有不错的性能,但是并不保证副本间顺序一致性。

Raft会选举一个leader来提交议案 ,leader与follower保持周期性心跳,并让follower保存raft log的副本。如果follower未收到leader的心跳包,在等待一个随机的选举时间后,follower会变成leader候选人(candidates) , 并发起新的leader选举投票。CockroachDB使用随机选举时间,这样通信往返时间短的follower会最先发起选举(还没实现)。只有leader才能提交议案;follower只能转发议案到最后一个已知的leader。

CockroachDB的Raft是基于CoreOS实现的,但考虑到一个节点可能有几百万个Range,因此做了优化,主要包括心跳合并(心跳数量取决于节点数量,而不是Range的数量)和批量处理请求。将来的优化还包括两阶段选举和静态Range(例如静态Range停止心跳交互)。

十四、Range Leases

如上所述,Range的副本被组织成一个Raft组,执行共享commit log中的命令。但是,执行Raft一致性操作开销较大,且有些任务同一时刻应该只在一个副本里执行。特别是,期望通过单一副本(理想情况是多个副本都可提供权威性读取服务,但难度很大)提供权威性读取服务的情况。

因此,CockroachDB引入了Range Leases的概念:一个包含时间片的租约。一个副本通过Raft提交一条特殊日志来获取Range租约。这条日志包含了从Liveness Table(一张包含每个节点存活时间和过期时间的系统表)获取的副本所在节点的epoch。每个节点必须持续更新Liveness Table中的节点存活记录。一旦有新的租约通过Raft协议被提交,只有成功应用了lease acquisition log,该副本才能成为新的租约持有者,且使用租约前,副本必须确保所有先遣写日志已经被应用。

为了防止两个节点同时获得租约,租约请求者本身也拥有着一份仍然有效的租约副本。如果新租约被应用时,原租约仍然有效,新租约将被授予或者忽略。只有节点A的存活记录过期并且Epoch被增大,则租约才可以从节点A转移至节点B。

注意:Liveness Table所在的Range和其Key范围之前的所有Range(例如meta1和meta2)其租约均不使用上述机制,以免循环依赖。

只要节点的epoch不变,并且其租约未过期,副本的租约仍然可用。持有租约的副本可以满足本地读取的需求,而不需要使用Raft引入额外的开销,并且负责处理诸如分裂、合并和均衡等针对Range的维护任务。

所有读写请求都被寻址到拥有此租约的副本上。如果没有副本持有租约,则可能寻址到任意副本上,接受请求的副本将尝试先获取租约再处理请求。如果非租约持有者接收到请求,将返回“当前副本非租约持有者”的错误信息,错误信息中指明最后有效的租约持有者。这些请求将通过网关节点重新发至新的租约持有者,客户端对此无感知。

如果读操作不经过Raft协议,新租约持有者会确保它的Timestamp Cache中全部时间戳大于上一个租约持有者的Timestamp Cache中的全部时间戳(因为这样才能兼容发生在上一个租约持有者上的读操作)。实现方法是:租约过期前,先进入stasis period (停滞期,过期时间减去最大时钟偏移),新的租约持有者必须将Timestamp Cache的低水位线设置成新租约的起始时间。

当一个租约进入停滞期,将不再提供读写服务,这不是我们所期望的。然而,这种情况只发生在节点失效时。在实际场景中,几乎不怎么发生,因为租约通常是长期可用(或者临时延期,这可以避免进入停滞期)并且可以主动转移,保证下一次租约生效前不提供任何读服务(仍可提供写服务)避免进入停滞期。

十五、Raft leadership托管

Range租约与Raft leadership是两种相互独立的角色,两者可能被不同的副本所持有。两种角色的分离会给系统带来一定的开销(租约持有者必须将每个议案转发给leader,增加了昂贵的RPC往返开销),所以每次重新续订或者转移租约时会试图合并这两种角色。实际应用中,Raft leadership和Range租约分离的情况会很少出现并且会被快速自修正。

十六、命令执行流程

本节将更详细地描述一个持有租约的副本如何处理一条读写命令。每条命令给出:1)该命令访问的一个Key(或者一个Key范围) ,2)被访问的Key所属的Range的 ID。当一个节点收到一条命令时,它根据所给定的Range ID检索对应的Range并检查该Range是否仍然管理这些Key。如果存在Key不属于此Range,则该节点向协调器返回错误,协调器重新将请求发送到正确的Range。

如果命令中所有的Key都属于此Range,此节点将尝试处理该命令。如果该命令是非一致性读,那么它将被立即处理。如果该命令是一致性读或者更新操作,那么只有满足如下所有条件时才可被执行:

  • 该Range副本持有Range租约
  • 该命令的Key与其他运行中命令的Key没有重叠,并且不存在读/写冲突

当不满足第一个条件时,该副本将尝试获取租约,或者返回错误以使得客户端将命令重新发送到真正的租约持有者。第二个条件确保对给定Key的一致性读/写命令是顺序执行的。

当如上条件都满足时,持有租约的副本将处理该命令。一致性读会由租约持有者立即处理。写命令则被写入Raft Log以使得每个副本都执行相同的操作。每条命令产生的结果是确定的,因此Range副本之间总是保持一致。

当写命令完成后,所有的副本更新自己的应答缓存来保证幂等性。当读命令完成后,Lease Holder更新本地Read Timestamp Cache,记录该Key最后一次被读取的时间戳。

当命令被执行时Range 租约可能已经过期。每个副本执行命令前会检查提交当前命令的副本是否仍然持有租约。如果租约已经过期,命令会被其它副本拒绝。

十七、Range分裂与合并

Range的分裂或合并取决于Range是否超过设定的最大或最小容量/负载的阈值。超过容量或负载任一最大值的Range会被拆分;容量和负载都低于设定的最小值的Range会被合并。

每个Range维护着Key前缀相同数据的统计信息,形成一组具有分钟级粒度的时序数据。统计信息来自于每次对Range的读写操作。 对这些统计信息进行分析可以作为分裂/合并Range的依据。Range 的大小和IOps这两个敏感指标被用来评估是否需要分裂或合并。读写队列的总体等待时间可做为副本迁移的依据。 这些指标通过GOSSIP协议在节点之间传播。

Range大小或容量一旦超过设定阈值就会分裂,为此,租约持有者会计算一个适当的候选Key作为分裂点,并通过Raft协议发起拆分。与分裂对比,合并则要求Range的容量和负载都小于设定的阈值,选取前后Range中较小的一个与之合并。

分裂、合并、均衡和恢复操作都通过相同的算法在节点间移动数据。新副本被创建并添加到源Range的副本集合中,然后每个新副本通过以下方式保持和Leader数据一致:回放全量的Raft Log,或者从源副本获取数据快照并从快照的时间戳开始重放日志直到完全同步。一旦新副本完成同步,则更新Range元数据,并且删除源副本(如果有)。

协调者(持有租约的副本)

当Range内的总数据量超过设定的最大阀值时,节点会拆分这些Range。类似地,当总数据量低于设定的最小阀值时,节点会合并这些Range。

待定:特别是对于合并(均衡也是如此),会有一个Range从本地节点消亡;要保证对该Range的操作优雅,平滑地切换到新的Range上。

根据gossip的负载统计信息,如果某个节点被判定为集群中负载和容量最高的节点,则该节点上的Range将被迁移到其他节点上。会优先选择同一数据中心内负载较低的节点按照1:1进行数据复制并重置Range元数据。

十八、节点分配

当Range分裂时,必须指派新节点。CockroachDB通过Gossip协议在集群中高效地交换节点“感兴趣”的信息,而不是和所有节点通信后获取集群的状态或者指定中心节点维护集群全局信息。那么什么是节点“感兴趣”的信息呢?例如,某个节点是否持有大量空闲空间。节点加入Gossip网络后,会把从Gossip网络中收到的每个主题和节点持有的信息比对。如果节点发现持有的信息比最近一次从Gossip网络获取到的数据“更有意思”(也就是较新的信息),则该节点将在下一次与对等节点的Gossip会话中把持有的信息扩散给对等节点。通过此方法,整个集群可以迅速地发现存储空间充裕的节点。为了避免负载堆积到同一个节点上,CockroachDB会从负载较低的节点集合中随机选取节点来分担负载。

Gossip协议本身包含两个主要组件:

  • 对等节点选择:每个节点维护N个与其定期通信的对等节点。CockroachDB节点会选择扇出系数最大的节点作为对等节点。即选择新的对等节点时,优先选择与自身对等节点集合交集最小的节点。每次Gossip网络被初始化时,每个节点都会与对等节点相互交换双方持有的对等节点集合。节点可以从其他节点的对等节点集合中选取合适的节点作为自己的对等节点。节点为了避免请求过载,可以拒绝交换对等节点集合的请求。每个节点倾向于应答和自己对等节点集合交集最小的节点的交换请求,反之拒绝请求。

对等节点可以使用《Agarwal & Trachtenberg (2006)》中提到的高效的启发式选择方法来选择。

待定:如何避免网络分区?需要做协议仿真来模拟网络运行,并从中找出可行方案。

  • Gossip选择:通信内容是什么?Gossip内容划分为如下主题:负载特性(磁盘容量、CPU负载、节点状态[DRAINING,OK,FAILURE])被用于副本迁移时的节点选择;Range统计信息(Range读/写负载、丢失副本、无效Range)和网络拓扑(机架间带宽/延时、数据中心间带宽/延时、子网故障时间)用于确定Range分裂的时机,或者决定修复副本还是等待网络恢复、调试、人工介入。上述统计信息的最小值集合和最大值集合在所有场景中都会被传播;每个节点都会把自己所能看到的全局视图与这些信息叠加,同时附加上汇报节点的信息和其他上下文信息。Gossip的主题都通过protobuf组织成结构化数据。每个主题中包含的Gossip条目数可配。

为了提高效率,节点为Gossip每个新条目分配一个序列号,并且记录每个节点感知的最大序列号。每一轮Gossip通信只同步增量条目。

十九、节点和集群度量信息

系统的每个组件都会输出与自身相关的度量信息,例如柱状图、吞吐量计数器或计量器。

CockroachDB不仅能把度量信息通过HTTP协议输出到外部监控系统(如Prometheus),而且内部实现了一个时序数据库(存储在多副本的Key-Value map中)。

这些时间序列数据以Store粒度存储,通过Admin UI可以高效地获取集群、节点或Store级别的全局信息。后台进程会周期性对旧数据进行降采样,并最终删除旧数据。

二十、Zones

通过Zone配置项,可以将keyspace分割成多个区间,并分别设置其复制策略。Zone配置项可以设置和数据中心相关的信息,划分至该Zone的Range的所有副本必须落在配置项中指定的数据中心。

相关的最新数据结构可参考pkg/config/config.proto  ,建议从message ZoneConfig入手。

如果Zone配置被在线修改,那么每个节点会查找与新的Zone配置不匹配的Range。如果发现不匹配的Range,则节点会用与负载均衡类似的方式 (1:1分裂复制Range)重新调整Range。

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值