前言
往下我将尽量深入浅出地总结自己对于数据一致性的整体认识,如果读者觉得哪里 有问题的,欢迎讨论。
一致性的本质
一致,是指多个研究对象之间的某些性质是一样的,或逻辑关系是正确的。
例如读者a和读者b,在各自的设备上看到的本文内容如果是一样的,则两篇文章是一致的,也称掘金能保证一致性。
不一致是如何产生的
例如,我现在在杭州,修改了文章的题目后,再重新发布,然后立马通知我
海那边的朋友查看,那他看到的可能还是原来的题目。
如果数据总是“瞬间”(或者期间不会有任何事件打断)完成同步,那么仍然能保持一致,
但目前是不可能的。
注意,这只是一个例子,不表示它的实际实现,但道理还是这个道理,往下 也会这样举例。
总的来说,由于信息传播或者说状态通信需要一定的时间,所以 多个数据副本之间不可能同时完成状态切换(这里不讨论量子纠缠这些内容)。
那么,在某个时刻,副本之间的状态可能是不一致的。
如此时访问数据副本2是获取不到x的。
不一致会带来什么问题
想象一下,你明明已经通过自动柜员机把钱存到你的银行账户,但下一次 查询时,你账户里的余额并没有增加,这多恐怖。
在一些金融或者高安全要求的场景是要保证一致性的。
如何保证一致性
根据要求不同,会有多种一致性的模型。这里我们只谈论强 一致,即系统 保证 对数据的读取都是最新的写入值。
不管是为了提升系统性能,还是为了数据备份,在现在的计算机架构中(cpu多级缓存,分布式存储等),应该说多数据副本是不可能避免的,而副本之间的状态可能是不一致的。所以,我们要允许这种系统内部(多副本组成)不时刻保持一致,但系统能对外提供一致的读写服务。
那如何能实现呢?
首先副本之间需要进行通信,交换彼此的状态信息,这样每个副本才能知道
自己的状态和其他副本的状态(才能趋于一致),当外部请求访问自己的时候,才能判断自己是否处于最新状态,如果不是则拒绝请求,或者等待切换至最新状态。
而且,为了防止判断“过期”,我们还需要对其他副本进行“锁定”(也是一种状态切换),禁止系统接受新的写入。(或者说一次写入,只有全部副本都更新时才算完成,在这过程中系统不接受新的读写)
也就是说,我们要对系统建立一个读写同步区域(读读共享,读写互斥,写写互斥),这要求在系统至少实现一个原子的状态切换操作,一般是CAS(compare and swap)。这里不再展开单机中CAS的实现,关键的一点是冲突检测与仲裁。
在多核处理器中,各核心的cache的一致性,是通过总线使用MESI协议来将状态传播到所有副本,使用仲裁器来处理冲突,如下是一个简单的固定优先级的4主控总线仲裁器电路图(0优于1优于2优于3):
有时候,我们还要求系统具备持久性,如断电后重启,系统能恢复到停电前的状态,如数据库。单机数据库的一致性,通过事务(数据更新要么全成功,要么全失败)保证,如MySQL中两阶段锁定控制冲突,double-write来进行写前备份,避免数据半更新状态。
分布式环境下,道理也是一样的,数据副本分布在不同的计算机,它们使用网络来交换状态。我们仍然可以用同样的思路来建立读写同步区域,比如同样引入一个冲突检测与仲裁装置(主从模式,或者说引入单独的锁节点)。
主从复制
在主从复制中,我们要求只有在主节点成功建立同步区域才能进行一致性的读写。写入,状态的传播方向固定从主节点到从节点,完成后退出区域,接着才能进行下一轮。
主节点崩溃(如断电)后,客户端的一致性读写请求得不到响应(系统内部可以通过从节点得不到主节点的心跳应答来发现,所谓崩溃,对系统来说是通信故障),系统也就崩溃了。除非从当前从节点中挑选中一个来当新的主节点。为了系统的一致性,新的主节点的状态需要和旧的一致。
实际上,任意从节点崩溃也会导致复制不能顺利完成,系统需要等待从节点恢复(得到应答),或者舍弃掉它们。主节点如果在没有得到任何从节点应答之前,就继续提供服务的话。那么主节点崩溃后,数据可能会丢失,因为从节点上可能没有主节点最新的数据。
假设主节点在得到1个从节点应答后,即认为写入完成,那么在经过两轮写入后,可能出现以下情况:
此时,如果主节点崩溃,任一从节点都没有完整的数据。
当然,我们可以让剩下的所有副本进行信息交换,直至其中的一个副本得到了分散在各副本的数据,对于有冲突的数据,可设计一个逻辑时钟(物理时钟会有时钟漂移),让每一数据都有一个单调递增的逻辑时间,冲突时采用时间最大的。
可见,此时系统不能再容忍节点故障,因为我们不能确定只用其中的一部分是否能得到所有数据。
那如果我们把从1个节点应答改成x个节点应答,是否能提高系统对节点故障容忍度?
多数派读写
问题:
假设集群有m个节点,每次数据写到其中任意x个节点,问写入结束后,是否存在任意y个节点,其中节点数据的并集是全部的数据,如果存在,
求当max(x,y)取得最小值时(节点故障容忍度最高),x和y的值。
解:
根据鸽巢原理有,x+y>m时,y个节点数据并集是全集,为使max(x,y) 最小,x与y应该尽可能接近。
如m为5,写入任意3个节点,那么写入结束后,任意3个节点数据的并集是全集,系统最多允许2个节点发生故障。
只要半数以上的节点正常工作,系统就可提供一致性读写。
这里我们再明确一下主从复制的数据更新流程:
(在raft协议中,如果少数从节点回复写入成功但多数从节点回复写入失败,主节点会持续重试写入操作,直到多数节点成功写入为止。在这个过程中如果主节点崩溃,选举出新主后,新主会继续这个复制过程。)
这样的无限重试是简单有效的做法,当然,如果我们想系统做出失败的回复,我们可以设计一个“回滚”动作,对回复成功的从节点进行撤销写入。
总之,主节点在回复客户端请求前,必须确认集群中多数节点已达成统一。
另外,我们可以让从节点每次都按序执行更新动作,这样可以使得系统在每次完成写入后,总有多数个节点,它的数据是全集且最新的。(可以认为这是渐进式完成崩溃时的数据搜集、汇聚工作)
选举
主节点崩溃后,系统内部对超过半数的节点(如上3/5)进行一次读,选取有最新数据的从节点来成为新主。此时即使少数派中可能存在比新主更新的数据,也强制与新主保持一致。
在接受新的外部访问前,新主可通过一条检查命令,要求从节点与自己保持一致,对崩溃时的复制达成统一。
(在raft中,新主在当选后立即追加一条Noop并同步到多数节点,实现之前未提交的日志隐式提交)。
这种检查命令应该纳入到最新数据的统计当中,这是为了防止多数派对崩溃时的复制决定放弃时,再次发生选举,少数派已复制的节点当选新主,要继续复制,如此反复,造成浪费。
当然,在真正与客户端交互之前,系统都可以反复决定对上一次崩溃时的复制是继续还是放弃,而一旦系统对客户端做出其中的承诺,那么系统就要谨遵承诺,不能违反。
以上,系统便能保证写入的一致了,当要进行一致性读时,也是如上的流程,不能直接读主节点的数据,因为此时的主节点可能已经与其他从节点失联了,其他的从节点已经选举出了新主,新主已经在提供服务。
一致性读优化
强一致性要求能读到系统的最新写入,最新的数据存在于主节点中,上述问题在于一致性读时,此时的主节点可能已经是过时的了。
所以,一方面,可以在读的时候,主节点对多数派进行一次心跳,确认自己还是主节点后,再返回结果。
另一方面,可通过租约来限制选举,使其不会同时出现两个主节点。如主节点在每次与多数派从节点的心跳中,主从之间达成一个约定,在约定的时间内不会发生选举(或者选举出新主也不会向外提供服务)。
但这与时间挂钩,时间的大小是个问题,要求漂移误差有上界。
以上就是我对于数据一致性的认识,如有错漏,欢迎指出,欢迎讨论。
也欢迎关注我的公众号:Allen很自由
参考资料:
设计数据密集型应用
people.cs.pitt.edu/~melhem/cou…