zookeeper如何保证数据一致性

​ 虽说zk是比较老的框架了,但是其一致性的保证放在今天的中间件中依然是很强力的存在。其数据持久化的流程也是非常标准的流程,兼具了性能和一致性的取舍,非常值得我们学习。


1 zk数据同步简述

重要知识点:首先ZK的数据分为两部分,磁盘数据和内存数据

  • 磁盘数据:存储在物理介质上的数据(持久化数据),可能与内存数据不一致。
  • 内存数据:zk在启动时,会从磁盘上加载数据到内存中,加载数据完成、选举完成、数据同步完成才能对外提供服务,运行期间内,客户端访问的数据都是内存中数据,不会访问磁盘数据。

ZK总体上来说,使用事务日志持久化的方式来保障被提交的数据不会丢失(也是很多数据库常用的操作),再通过内存数据与磁盘数据隔离的形式,来避免分布式环境下各种数据丢失、错乱的问题产生。

zk数据写入简要流程

image-20220329201630980

结合zk的数据存储以及上述数据写入流程可知,只有当数据被半数以上的zk节点持久化后,ZK的Leader节点才会把该次事务在内存中生效(内存生效才能被客户端访问)。

因为过半数的节点ACK数据就会写到内存中,所以会存在提交过程中,节点1能读到最新的数据,而节点2读不到,这也是唯一的数据不一致情况。而且针对于这种状况,ZK也做了自身的优化,在同一个session内,读写事务顺序是有保证的。总结为,ZK的数据一致性破坏条件为,只有是不同session的客户端,在数据提交过程中,才可能会读不到最新的数据。

2 顺序性保障

zk的leader节点在处理写事务请求时,会额外保留一些中间状态,比方说上次已提交的最大事务id、当前正在等待ack的事务id、当前已经被ack但是还没有写入内存的事务id。

通过以上三种信息,leader可以用来保障事务通过有序的顺序进行处理,其实本质上类似于单线程在执行二阶段提交的操作,与redis的做法基本类似。

举个例子,有两个写事务操作,事务A:zxid=1、事务B:zxid=2。

事务A在等待客户端的ack过半,事务B过来了,则事务B会被阻塞住等待事务A完成。

3 意外场景举例

在理论上,不同zk节点间,数据发生意外情况只有以下三种可能。

  • 场景1:有某个zk A节点与集群断联,导致A节点中的数据少于集群中数据(因为集群还在正常对外提供服务)

    image-20220405230258716

    A节点在重新连接集群时,会触发选举流程,A节点会收到当前集群已经有leader的选票,因为leader目前确实有效(有半数节点认同),所以A节点也认同leader,leader会把follower缺失的数据发送过去跟自己对齐。

  • 场景2:有某个zk-leader A节点刚刚发送完proposal后就与集群断联,而其他的节点选举出了新leader且新的集群没有提交新的事务

    image-20220405230328094

    当A节点又可以正常连接集群,会导致A节点中的数据多于集群中数据,这时新leader会把旧leader多出的数据删除掉。

  • 场景3:在场景2基础上的特例,当新的leader继续对外服务后,可能会造成之前的leader与现在leader之间的数据有多有少的局面,也可以说是最复杂的场景。
    image-20220405230328094
    zk对于这种场景用了很粗暴的方式,当发现数据有多有少后,会直接发送snap把新leader的所有数据覆盖掉A节点的数据,这么设计可能是为了避免出现某些问题,但是也埋下了一些坑,因为snap是比较耗费性能的操作(特别是有超时时间的限制)。

    以上三种场景每种场景我都debug过代码,可以保证描述正确性。
    如有更多的场景,欢迎找我沟通,互相学习,联系方式在个人主页。