Raft分布式强一致性方案
Raft 协议能够实现分布式系统的强一致性,但是使用Raft协议直接读取主节点并不能保证系统的强一致性。
这里举一个简单的例子:
Raft 集群中的一个主节点,他的所有操作连同它的时钟一起被挂起,例如运行在虚拟机中,虚拟机进行暂停操作,此时 Raft 节点监听不到主节点的心跳,就重新推选出新的主节点,并进行日志复制,保证集群节点的实时性和一致性。
由于只需要半数的节点就能够完成提交,在这种情况下,一些客户端可能会持续更新Raft的内置状态,此时如果有一个客户端访问了之前被挂起(宕机)的前主节点,且恰巧这个前主节点恢复并被唤醒,此时这个前主节点还未与其他节点进行心跳同步,导致自己仍认为是集群中的主节点,从而会导致该客户端访问到之前的老数据。
一句话阐明情况:旧主被 挂起(GC pause / 虚拟机 freeze / 内核调度饥饿) → 新主已选 → 旧主 在“得知新 Term 之前” 被唤醒,仍认为自己还是 Leader。
简单的解决方法:
每次读取也都视为一个新的写入操作,生成一条日志。
但是为了性能起见,我们可以让主节点在收到读请求之后,向从节点发送心跳同步状态,如果大多数从节点回复了心跳,则证明该主节点仍然一致、有效。
另一种方案:
旧的主节点等待其他的写操作返回,这样就知道当前主节点仍然保持自己的主节点身份,那么我们就可以返回之前的读请求了。
大佬是这样说的,但是GPT回复是:
旧主恢复后也必须先经过“一次多数派交互”(心跳或 noop)才能提供读服务;
“等新写返回” 并不是去 ‘修好’ 旧主节点,而是让 ‘唯一能接到请求的节点’永远先证明自己合法;
旧主一旦接请求就会被同一规则当场识破并拒绝,客户端随即重定向到新主节点,脏读自然消失。
另外的另外一种方案:
每次读取的时候,都向从节点进行读取(Zookeeper就这样干的),但这会引发新的问题,当写入成功后,主节点还没来的及同步给从节点(的日志),立即进行了一次读取操作,这个读取操作可能会读取到旧数据。
解决方法也很简单:
读取的时候等待从节点同步,将新数据同步写入进从节点就好了。因为共识算法的日志全局有序性特点,因此只需要保证客户端读取指令在相同客户端的写入指令之后执行即可。
多客户端场景
上面说的都是单个客户端的场景,我们来说说多个客户端。
多个客户端之间不会相互通信,那么使用 ”从节点读取“ 方案是可以的。这是因为网络调用也是需要时间的,请求发出时,网络读取到了3;如果网络堵塞了,那就会读取到4,每个客户端只需要保证自己操作时有序的就行,这个特性也在 Zookeeper 中称之为:A-linearizability。这也是 Zookeeper 读取时的默认级别。

简单来说就是别人的写入我可以晚一点读取到,但节点自己的写入必须立刻读取到。
但如果是B客户端告诉A客户端自己写入成功了,那么A客户端立刻发起一次读取请求,此时如果读取不到数据的话,显然会导致逻辑上的错误,因为B已经告诉A写入了数据,能够供给A读取。这时候 Zookeeper 默认的一致性隔离级别就会有问题了,Zookeeper 官方论文也给出了使用 sync 操作解决该问题。

当我们想进行一次强一致性的读取时,就发送给主节点一个 sync,当从节点同步到这个 sync,那么从节点此时就能确信,之前的日志都完整的被应用到了状态机上,从而来返回读取操作。


RoLingG | 博客
评论(0)