Raft是一种共识算法,在之前的文章里已经提到过。简而言之,每次集群处理一次请求,都需要经过集群中大部分节点协商。所以一个Raft集群的规模一般不会太大,否则协商的代价就会比较大。那么如果希望基于Raft实现一些规模比较大的服务该怎么扩展呢?
例如我们想做一个kv存储,那么一个简单的想法是把key分为多个range,然后不同的range由不同的Raft集群来控制。实际上,MultiRaft的思想就是这么简单…只是在实现上有一些细节需要考虑。如果希望更多理解MultiRaft的概念,可以读读这篇文章,还有这里。从中可以发现,MultiRaft解决的两个核心问题分别是:
- 共享物理节点的问题:多个Raft集群实际上是共享物理节点的,所以需要小心组织每个节点上的数据;
- Heartbeat过多的问题:每个Raft集群逻辑节点需要处理Heartbeat消息,如果每个物理节点上都有多个Raft逻辑节点,那么开销会比较大,所以希望Heartbeat以物理节点为单位而不是逻辑节点。
如果考虑跨Raft集群操作,实际上还有一个问题,就是如果一次操作跨不同的Raft集群怎么办?如果服务不需要提供事务那其实是没有问题的,但如果需要呢?现在使用MultiRaft的两个服务Cockroachdb和Tidb都有文档说明:
Cockroachdb的思路比较容易理解,也跟我想的差不多,而Tidb的则没有看明白,尤其是关于锁的问题。
下面按照我自己的理解来说明。首先,数据需要以MVCC方式存储,即每个kv保存多个版本,例如:
key | value | commit | state |
---|---|---|---|
a | 1 | 1 | stable |
a | 2 | 2 | unstable |
b | 1 | 1 | stable |
每个kv除了key和value额外保留两个字段,分别是commit和state。在这里stable代表一次事务已经完成,可以被外界读取的情况;反之,如果是unstable,表示事务没有完结,对外不可见。
在一次写入的时候,如果所涉及的数据都分布在一个Raft集群内,那么是不需要考虑事务的,因为这些变更可以记做一条Raft日志,从而达到事务的效果。只有跨多个Raft集群时才需要用Two-phase commit (2PC)来达到整体的事务效果。
在2PC的第一个阶段,每个Raft集群完成写入后,内部节点的状态(即一个kv map)对应的state都是unstable,表示这时候只是单个Raft完成写入,还需要等待2PC coordinator确定是否所有Raft集群都完成写入。数据里的commit是事务的编号,这可以由一个独立的服务来产生事务编号,保证commit单调递增。当所有Raft集群都写入成功,2PC进入第二个阶段,由coordinator向所有集群通告已经成功的commit号,接收到该信息后各个Raft集群将commit对应数据的state由unstable改变成stable,一次事务完成。
总的来说,MultiRaft是对Raft的一种扩展。但是,MultiRaft还不方便简单抽取出来作为一种可供其它应用直接使用的库,与业务逻辑的关联性比较强。不过,有了Cockroachdb和Tidb的实际应用,对其它类似的存储结构的扩展是很好的参考。