文章目录
Raft协议已经详细介绍过,不做赘述,在Crdb中使用了Multi-Raft协议,那么Multi-Raft协议是如何工作的呢?
Multi-Raft协议
下面是Crdb里对于Multi-Raft的介绍,说明了在分布式环境下,为什么要开始使用Multi-Raft协议。
In CockroachDB, we use the Raft consensus algorithm to ensure that your data remains consistent even when machines fail. In most systems that use Raft, such as etcd and Consul, the entire system is one Raft consensus group. In CockroachDB, however, the data is divided into ranges, each with its own consensus group. This means that each node may be participating in hundreds of thousands of consensus groups. This presents some unique challenges, which we have addressed by introducing a layer on top of Raft that we call MultiRaft.
简单来说,在分布式系统中由于数据被按照一定的方式做了切片,每一个切片的数据都有多个自己的副本,这些副本之间的数据使用Raft协议来保证数据的一致性。而在每一个存储的节点上,也会存储着多个切片的数据信息,所以对于节点和切片,存在着一个多对多的关系。而如果单独只使用Raft协议,虽然也是可以的,但是很显然效率是很慢的。
Multi-Raft需要解决的问题
单个Raft-Group在KV的场景下存在一些弊端:
- 系统的存储容量受制于单机的存储容量(使用分布式存储除外)
- 系统的性能受制于单机的性能(读写请求都由Leader节点处理)
Multi-Raft需要解决的一些核心问题:
- 数据何如分片。分片中的数据越来越大,需要分裂产生更多的分片,组成更多Raft-Group。分片的调度,让负载在系统中更平均(分片副本的迁移,补全,Leader切换等等)。
- 一个节点上,所有的Raft-Group复用链接(否则Raft副本之间两两建链,链接爆炸了)
- 如何处理stale的请求(例如Proposal和Apply的时候,当前的副本不是Leader、分裂了、被销毁了等等)
- Snapshot如何管理(限制Snapshot,避免带宽、CPU、IO资源被过度占用)
Multi-Raft实现细节
要使用Multi-Raft,首先就是需要对集群的数据进行切片,让每个 Raft 单独负责一部分数据。通常的数据分片算法就是 Hash 和 Range,一般会使用的 Range 来对数据进行数据分片。为什么使用 Range,主要原因是能更好的将相同前缀的 key 聚合在一起,便于 scan 等操作,另外,当某一块分片数据量较大时,在做分片分裂的时候也是非常方便的,这个 Hash 是没法支持的。
通过分片,一个单节点单RaftServer实例一把会存储下面三个数据,这些数据会存储在底层的存储层中,比如RocksDB等嵌入式数据库中(其中RaftLog和Snapshot的数据落在本地节点盘之上的)。
- StateMachine,内部状态机。
- RaftLog,持久化在本年底的transaction log。
- Snapshot数据
Single-Raft协议(也就是传统的Raft协议)工作示意图如下:
而Multi-Raft协议结构大致如下:
Multi-Raft会分离出RaftServerProxy和RaftServer服务,一个节点可以启动多个RaftServer实例,按照raft group id区别开,RaftServer有其独立的StateMachine以及其对应存储RaftLog的位置。这样,在RaftClient进行请求通信时需要带上raft group id,以此让server Proxy知道请求应定向到哪个具体的RaftServer中去。
Cockroach Multi-Raft
Raft - Consistency of Range Replicas
Raft 选举一个相对长寿的leader来提交指令 ,leader会与follower会保持周期性心跳,并让follower保存log的副本. 如果心跳消失,在经过随机的election timeouts时间,follower成为候选人(candidates) , 并且继续发起新的leader选举过程。Cockroach使用随机时间,这样通信往返时间短的会更易第一个发起选举。只有leader才能提交指令,followers只简单地传递命令到最后一个已知的领导者。
Cockroach的Raft实现在CoreOS的基础上,增加额外的优化层,因为考虑到一个节点可能有几百万的一致性组(每个range一个)。少部分优化主要是合并心跳(与数量巨大的range相反,节点数量决定了心跳的数量)和 请求批处理。将来的优化还包括二阶段选举和静态range。
Range Leadership(Leader Leases)
一个range的副本作为一个Raft组来执行共享commit log 里的指令。通过Raft实现一致性是一个昂贵的操作,且有些任务同一时刻只能在一个副本里进行处理。
所以,Cockroach提供了叫Range Leadership的概念,它是一个时间片租约,通过Raft算法提交的一个特殊Log来建立(Log包含了LeaderShip活跃时间间隔、Node:RaftID的组合)。
Node:RaftId的组合用来唯一的表示一个正在被请求的副本。
对于读写请求,需要寻址到持有这个租约的副本上;如果无法寻址到,就会被发送到任意其他副本上,并且被发送到请求的副本需要尝试同步的获取租约。
持有租约的副本将处理一些特殊range的维护任务,如:
- 通报(gossip)第一个range的信息
- 拆分(splitting),合并(merging),平衡(balancing) range
在单Raft保证了Leader持有最新的Log,而Cockroach的Multi-Raft也要保证持有租约的副本要确定它的缓存的时间戳(使用HLC)大于上一个持有租约的副本。Range Leader和Raft Leader是完全独立的,Raft 和 Range的Leader将不大可能位于同一个副本。
虽然语义上,Range LEADER和Raft LEADER是不同的两种角色,但是他们最好是位于同一个副本上,因为如果一个写请求打到Range Leader上的时候,如果Range Leader和Raft Leader不是同一个Leader的话,那么请求会被转发给Raft Leader,由Raft Leader完成同步(包括同步给Range Leader所在的副本),这样就多此一举了。
实现这一目标的一个相当简单的方法是: 规定每一次延长或开始新的租约期限时,必须由租约持有者来发起raft选举(除非它是已经leader),还要注意要保证Range leader稳定且长期存活,来避免大量Raft leader转换。
失效的时候(也就是请求寻址失败的时候),由于请求会发送到任意一个其他副本上,所以会由这个副本来尝试获取租约了。
Command Execution Flow
这一章详细介绍leader 副本是如何处理读写命令的。
每一个命令都指明一个要处理的key(或者一个key的范围)和包含该key的range的ID。当RoachNode收到一个命令时会根据rangID获取对应的range,并且检查 这个range是否包含相应的keys。如果不包含,RoachNode返回error,客户端收到error后继续重试直到找到正确的range。
当命令中的所有keys都包含在range中,RoachNode就开始执行命令。如果命令是一个非一致性的只读命令,会立即处理。如果是要求一致性的读写命令,只要满足以下两个条件,命令才被执行:
- 该range必须是leader。(如果不是Leader,副本尝试获取leader 租约(leader lease),如未获取租约则给客户端返回error)
- 当前要读写的keys与正在处理的命令的keys不能有重叠(保证对一个key的一致性读写命令被串行执行)。