CRAQ一致性协议是对链式复制一致性协议的改进,增加对读操作的吞吐,设计非常有意思,记录一下。
1. Chain Replication (链式复制)
如上图所示,链式复制的思想是在一个分布式集群中,节点之间形成一个链路:
- 对于任何写请求,都从HEAD节点开始写,直到写到TAIL,这个写请求被认为是被整个集群提交。
- 对于任何读请求,都从tail中读取,因为tail中保存了所有已经提交的写请求。
可以看出,通过这种机制,链式复制实现了强一致性,在同一时间任意请求集群,总能得到相同的结果。链式复制一致性协议的特点在于: - 写请求吞吐更高。这个很好理解,传统的主从结构中,master需要将写请求发送给多个replica,这样写的小号很高。但是链式复制中,每个节点只需要将写请求发给自己的下一个节点。
- 读请求成为了瓶颈。因为所有的读请求都是tail节点处理的,因此tail不可避免地会承载非常大的压力。
CRAQ
CRAQ对于传统的链式复制做的修改在于:其允许读请求由集群中的任意节点请求,而保持集群的强一致性。CRAQ的精髓思想在于:这个节点之间的链表是双向的,写请求从head→tail实现整个集群的写,然后tail→head通知所有节点集群最近的一致性点。当读请求达到任意节点时,节点首先check自己对于这个请求的对象是不是在上个一致性点之后没有修改,如果没有修改,说明节点存储的内容和tail存储的内容是一致的,可以直接返回,否则就向tail中再请求一次。
CRAQ的工作流程为:
- 每个节点存储多个版本的对象,一个clean版本代表着最近一次一致性点的对象版本,对于从上个一致性点之后来的每个请求,节点都为对象保存一个dirty版本号和对应的值。
- 对于写请求:
2.1 客户端还是将请求发给head。
2.2 当链表中的一个节点收到写请求时,其修改对应的对象值并创建一个对应的dirty版本号
2.3 当tail节点收到写请求之后,代表着这个写请求已经在集群中被提交,为这个对象创建clean版本,并沿着链表tail→head的方向发送确认消息,确认这个clean版本。
2.4 当链路上的节点收到tail的消息后,其确定这个clean版本为最近的一致性版本,并删除再这个版本之前的所有版本。 - 对于读请求:
3.1 读请求可以发送给链路上的任何节点
3.2 节点收到对对象的读请求后,检查这个对象的版本,如果发现对象在我这只有一个clean版本,说明这个对象在上次被tail确认之后没有任何修改,此时我保存的对象和tail保存的对象肯定是一样的,此时就直接给client返回对象的值。
3.3 如果节点收到对某个对象读请求之后,发现我这里由对象的一个clean版本和多个dirty版本,说明对象在上次被tail确认之后又被修改了多次。此时节点不能随便返回,因为他不知道集群对这个对象的提交到了哪个版本(可不一定是这个clean版本,因为tail可能已经提交了更新的版本,只是更新版本的确认消息害没传到这个节点),因此此时节点需要向tail询问一个版本号,然后给client返回这个版本号对应的对象的值。
我们在两种不同的场景来讨论CRAQ读请求的吞吐量:- 集群中大多数请求是读而不是写:在这种情况下,从non-tail节点中读取的值很有可能就是clean版本,可以直接返回;我们已经说了链式复制的瓶颈是读,因此,在这种场景下集群的吞吐可以随着集群的数量线性扩展。
- 集群中大多数请求是写而不是读:这种情况下,当读请求到了non-tail节点时,需要频繁向tail节点请求。看起来好像没啥改善,但是请注意,我们只是向tail节点请求版本号,然后根据对象的版本号给client返回自己存储的对象的值。这比传统的链式复制(直接向tail请求对象的值)要轻量很多。
3. 根据CRAQ实现的一致性协议
- 强一致性:这个就跟我们讨论的一样
- 最终一致性:在non-tail节点处理读请求时,直接返回自己clean版本的值,虽然可能在不同节点上读取的值不一样,但最终总会一样的
- 介于强一致性和最终一致性:我们保证系统在固定的时间内实现最终一致性。想法是为系统设置时钟,固定的时间内non-tail节点向tail节点请求最新版本号。
总体来说,CRAQ还是很有意思的,但是相较于raft和paxos,还是没办法处理脑裂等问题,它只关心数据复制,也是提供了一种很好理解的强一致性协议了。