初探ZooKeeper

307 阅读29分钟

ZooKeeper

Zk是什么?

image.png 上图是一个Zookeeper集群的简单架构罗列。集群中包含多个server,可以认为是多个服务端共同维持这个集群,底下的是一个个我们的客户端,可能会有很多很多。

所以它们可以用来干嘛呢?

ZooKeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现。

  • 数据发布/订阅(配置中心)
  • 负载均衡(后端机器节点轮询访问)
  • 命名服务(区分不同集群服务等)
  • 分布式协调/通知(Watch机制)
  • 集群管理(节点上下线)
  • Master选举(zk选主)
  • 分布式锁(Watch机制获取ZNode的锁)
  • 分布式队列(可以用作队列,但不推荐,机制不够)

zk典型应用场景

数据发布与订阅

将变更较为频繁的数据记录在ZNode中,各个client监听这个ZNode,使其在changeData的时候获取最近的配置,并更新到本地。 image.png

负载均衡

一个ZNode下不同path对应不同的服务,每个服务对应多台机器,通过简单轮寻访问达到负载均衡的效果。

image.png

利用ZK选主

image.png

分布式锁/分布式队列

  • 在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用 createNode方法在 locker下创建临时顺序节点,然后调用 getChildren(“locker”)来获取 locker下面的所有子节点,注意此时不用设置任何 Watcher。客户端获取到所有的子节点 path之后,如果发现自己创建的节点在所有创建的子节点序号最小,那么就认为该客户端获取到了锁。如果发现自己创建的节点并非 locker所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用 exist()方法,同时对其注册事件监听器。

代码的实现主要是基于互斥锁,获取分布式锁的重点逻辑在于 BaseDistributedLock,实现了基于 Zookeeper实现分布式锁的细节。

image.png

注册中心

image.png

客户端相关信息

客户端常见API

  • create node 创建节点(4种类型节点,根据type指定)
  • delete node 删除节点
  • setData(给某个节点设置数据信息)
  • getData (获取节点数据)
  • exists(判断节点是否存在)
  • getChildren(获取某个节点下的所有子节点)

客户端组件

客户端基本重要组件如下所示

image.png

  • ZooKeeper:客户端连接服务端的API操作类,包括连接、创建节点、删除节点及获取数据等操作都是由这个类发起的,提供的都是ZK的原生接口功能;

  • ClientCnxn:服务端有ServerCnxn,那么客户端自然就又ClientCnxn,每个对象代表服务端的一个连接。这个对象将负责把Packet包发送至服务端,并接受处理服务端的响应及触发的监听事件,实际和服务端的通信都是通过这个组件完成的;

  • Packet:字面意思,就是ZK中客户端和服务端交互的包对象,这里面包含了请求头、响应头、请求、相应及其它的一些对象,在ClientCnxn中,交互的数据元便是Packet类,在Client端是数据的载体;

  • WatchRegistration:从名字也可以看出来,这个组件是负责注册客户端定义的Watcher到ZK的,但实际上Watcher的注册并不是在Server端完成的,而是在Client端通过Packet组件调用这个类完成本地的注册;

  • ZKWatcherManager:客户端的Watcher管理器,客户端所有注册的Watcher都会在这个组件中,并且Watcher的注册是通过前面提到过的WatchRegistration类完成的,连接ZK时默认的Watcher监听器也是由这个类管理的。

ClientCnxn及内部类

  • ClientCnxn:SendThread、EventThread还有WatcherSetEventPair三个都是其内部类,并且SendThread和EventThread是通过ClientCnxn创建调用的;

  • SendThread:这个类是一个线程对象,在Client端运行时这个类也会一直运行直到Client关闭连接,其负责发送消息和接收Server端的回调通知,运行期间会一直监听NIO的通信以及监听一个Packet数组,当有可发送包时将会读取Packet数组解析包并发送,这个类和EventThread类一起合作完成了ZK客户端的通信和回调;

  • EventThread:同样是线程对象,这个类负责处理Client端的各类事件,运行期间会一直监听waitingEvents阻塞队列,当有事件被放到这个类中后将会被拿出来进行事件回调,这个是一个串性回调。需要注意的是Client端的Watcher并没有被发送到Server端,只是通知了Server端哪个路径被监听了,当触发监听事件时将会通知Client端可以执行本地的Watcher了;

  • WatcherSetEventPair:顾名思义,这个类的作用便是Watcher的Set集合和对应的Event事件组成的类,执行事件时如果判断是这种类型,将会依次调用这个类中的Watcher集合(也就是Client端本地被使用的Watcher)的process回调方法;

信息载体类Packet

  • RequestHeader:请求头,实现了Record接口,提供了相应的序列化和反序列化方法,其内部只有xid(可以理解为Packet的计数器,逐次加一和ReplyHeader的xid对应)和type(ZK的操作类型),所有的请求都有请求头。

  • ReplyHeader:响应头,实现了Record接口,有固定的序列化和反序列化方法,所有正常接收的响应都有响应头,这里面包含了和RequestHeader一样的xid(Packet的计数器,和请求和RequestHeader一样的xid对应)、zxid(响应本次操作的事务ID)和err错误码。

Watcher之监听组件

  • Watcher:ZK的Client端在实现监听功能的基本单位,所有的监听动作都是从Watcher的实现类进入的,并且Client端管理监听也是以该类为基本。这个类有两个内部枚举类为不同的监听事件,稍微介绍下几个值的意义:

    • KeeperState代表ZK客户端状态的枚举类

      • Disconnected:代表ZK客户端已经断开连接状态,当发生了异常或者关闭ZK客户端就会触发;
      • SyncConnected:代表异步连接成功事件,此时Server端已经生成了Session信息,并返回确认消息触发这个事件;
      • AuthFailed:在Server端认证失败后将会触发这个事件;
      • ConnectedReadOnly:连接成功但是只能读取ZK服务端的内容,而不能进行增删改操作;
      • SaslAuthenticated:在Server端的认证已经成功完成将会触发;
      • Expired:客户端的Session在服务端已经过期将会触发的事件,可能是连接超时、可能是太久没有进行ping通信也有可能是sessionTimeout事件配置不合理。
    • EventType:对于客户端来说可处理的几种事件类型,一起和KeeperState枚举类搭配组成了ZK的监听回调事件区分:

      • None:这个事件类型是由Client端来设置的,一般都是被用在连接ZK的Server端,当处于这个事件时,代表事件类型是由Client端发布的,其它的事件都是由Server端发布的,且Server端发布其它类型的EventType事件时,KeeperState状态必为SyncConnected
      • NodeCreated:代表监听的路径在Server端有节点被创建;
      • NodeDeleted:代表监听的路径在Server端有节点被删除;
      • NodeDataChanged:代表Server端监听路径的节点中节点数据被修改;
      • NodeChildrenChanged:代表在Server端监听路径的子节点发生了修改,包括增删改三种操作。

image.png

  • ZKwatchManager:ZooKeeper类的内部类,负责管理不同类型的Watcher以及ZooKeeper的默认Watcher,在这个类中Watcher被分成了四种:

    • defaultWatcher:ZK客户端连接的默认监听器,一般是用来处理KeeperState枚举类中的触发事件,如连接成功、连接超时或者认证失败这些;
    • dataWatches:用来处理和数据相关的事件,除了NodeChildrenChanged事件类型以外的四种事件类型都会触发,当使用**getData()**操作时将会添加此类型监听器;
    • existWatches:处理事件类型同dataWatches,使用**exists()**操作时将会添加此类型。
    • childWatches:处理None、NodeChildrenChanged和NodeDeleted三种事件类型,当使用**getChildren()**操作时将会添加此类型监听器。
  • WatchRegistration:从名字就可以看出来这个类的作用就是注册Watcher到ZKWatchManager中。三个实现子类也简单的说下:

    • DataWatchRegistration:注册Watcher到dataWatches集合;
    • ExistsWatchRegistration:注册Watcher到existWatches集合;
    • ChildWatchRegistration:注册Watcher到childWatches集合;

主要组件交互

image.png 这个这个主要交互图乍一看比较复杂,但实际上可以分为三个部分:发送消息、接收消息处理响应事件及轮询事件数组调用相应监听器。接下来再大致分析一下这三个部分的一些具体细节交互:

发送消息:这个部分的交互的核心便是ClientCnxn、ClientCnxnSocket、SendThread以及消息载体Packet(对应图的右下角部分),接下来分别聊下其交互:

  • ClientCnxn负责对接ZooKeeper API类,所有客户端的操作都需要从ZooKeeper类到ClientCnxn,然后在ClientCnxn中调用各个组件进行处理;
  • 除了建立连接是由ClientCnxn直接使用SendThread发送请求连接消息之外,其它的操作都是先拼装对应的Packet消息载体,随后由ClientCnxn保存到包数组中;
  • 当ClientCnxn发起相应的操作时,SendThread线程一直轮询将会获取到请求的数据,随后调用ClientCnxnSocket序列化Packet并发送到ZK的Server端。

接收消息处理响应事件:这个部分的交互核心便是SendThread、WatchRegistration以及监听器和相应事件的载体WatcherSetEventPair(左上角部分),接下来分别聊下其交互:

  • 当ZK的Server端收到Client的请求后会发送一个响应给对应监听的Client,而Client接收响应的类便是SendThread(没错,SendThread类负责接收和发送消息,承担着IO多路复用的作用),当接收到响应后,将会根据操作类型判断是通过Watcher监听器还是AsyncCallback异步回调处理响应事件;
  • 如果确认是Watcher来处理,将会使用WatchRegistration来注册对应的Watcher监听器,并生成对应的WatcherSetEventPair对象,把对应的事件和Watcher进行绑定放到事件数组中,以便后续EventThread获取使用;
  • 而如果确认是AsyncCallback来处理,将会直接把Packet放到事件数组中(因为AsyncCallback在对应的Packet中),以便后续直接使用。

轮询事件数组调用相应监听器:这个部分在一次操作流程中属于结尾部分,将会使用前面保存的轮询事件来判断进行相应的调用,这部分交互核心为EventThread、Watcher、AsyncCallback以及WatcherSetEventPair(左下角部分),接下来分别聊下其交互:

  • EventThread会一直轮询waitingEvents阻塞数组,当前面的SendThread收到响应后将会把对应的响应事件放到这里面,而EventThread便可以通过轮询的方式获取到前面获取的响应事件;
  • 当获取到响应事件后,会有两种处理,第一种是WatcherSetEventPair,这种响应一般对应普通的Watcher实现类监听器,如果判断是这种的类型时,将会从WatcherSetEventPair中获取对应的Watcher,并把响应的事件类型传进去;
  • 而如果是AsyncCallback类型,则会直接根据操作类型判断进行转换并调用具体实现类的processResult方法。

基本概念

事务ID

事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作, 一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID, 用 ZXID 来表示,通常是一个64位的数字。每一个ZXID 对应一次更新操作,从这些 ZXID 中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序,保证事务的顺序一致性

zk所描述的顺序一致性和我们常见的分布式理论中的顺序一致性还不太一样。

简单理解的话,zk的顺序一致性保证的是一个客户端连接上了一个服务端,发送的请求和命令都是按照时间顺序处理的,读到的数据也是按照ZXID一个个顺序读到的。如果zk客户端可以再连接到另外一个服务端上的话,可能会出现时间回溯,数据丢失的情况,为了防止这种情况,zk提供了单一视图,一个client只能连上其中一个server,且是一个TCP长链接。

而分布式理论上的顺序一致性,则是保证多个客户端在一个全序的时间轴上面,每个客户端写入和读取的数据都符合自身的顺序要求,而不一定是和读写事件的真实的时间戳要求一致。

详情可参考:lotabout.me/2019/QQA-Wh…

节点类型

  1. 临时节点(注册中心)
  2. 永久节点(配置中心)
  3. 临时顺序节点(分布式锁,选主,分布式任务)
  4. 永久顺序节点

image.png

节点角色

  • Leader
    • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
    • 集群内部各服务器的调度者。
  • Follower
    • 处理客户端非事务请求,转发事务请求给Leader 服务器。
    • 参与事务请求Proposal 的投票。
    • 参与Leader 选举投票。
  • Observer
    • 对于非事 务请求,都可以进行独立的处理,而对于事务请求,则会转发给Leader服务器进行处理。
    • 和Follower唯一的区别在于, Observer不参与任何形式的投票,包括事务请求Proposal 的投票和Leader选举投票。

WATCHER

  • 推模式:在推模式下,服务端主动将数据更新发送给所有订阅的客户端;
  • 拉模式:是由客户端主动发起请求来获取最新数据

客户端无法直接从服务端推送的信息中获取到对应数据节点的原始数据内容以及变更后的新数据内容,而是需要客户端再次主动去重新获取数据。

image.png

客户端处理 Watcher步骤

  1. 客户端调用getData()/getChildren()/exist()三个 API,传入 封装的Watcher(分别对应dataWatcher/childWatcher/existWatcher) 对象。
  2. 标记请求request,封装 Watcher 到 本地的WatchRegistration
  3. 封装成 Packet 对象,发服务端发送 request。
  4. 收到服务端响应后,将事件类型和 Watcher 组装注册到 ZKWatcherManager 中的WatcherSetEventPair进行管理。
  5. 请求返回,完成注册。

服务端处理 Watcher步骤

  1. 服务端接收 Watcher 并存储。接收到客户端请求,处理请求判断是否需要注册 Watcher,需要的话将数据节点的节点路径和 ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接,实现了 Watcher 的 process 接口,此时可以看成一个 Watcher 对象)存储在WatcherManager 的 WatchTable 和 watch2Paths(通过path找ServerCnxn以及通过ServerCnxn找path)中去。
  2. Watcher 触发。以服务端接收到 setData() 事务请求触发 NodeDataChanged 事件为例:
  • 封装 WatchedEvent。将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个 WatchedEvent 对象
  • 查询Watcher。从 WatchTable 中根据节点路径查找 所有的Watcher(ServerCnxn)。
  • 没找到说明没有客户端在该path的数据节点上注册过 Watcher
  • 找到提取并从 WatchTable 和 Watch2Paths 中删除对应 Watcher(从这里可以看出 Watcher 在服务端是一次性的,触发一次就失效了)
  1. 调用 process 方法来触发 Watcher。通过 ServerCnxn 对应的 TCP 连接发送 Watcher 事件通知。

客户端回调 Watcher

客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。 客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。

watcher使用细节

1.一次性的,无论客户端还是服务端,watch使用完都会remove

2.客户端Watcher 回调是串行同步的,保证了watcher的执行顺序。因此,千万不要因为一个Watcher的处理逻辑影响了整个客户端的Watcher回调 3.客户端无法直接从服务端推送的信息中获取到对应数据节点的原始数据内容以及变更后的新数据内容,而是需要客户端再次主动去重新获取数据 4.watcherEvent异步发送。 5.当一个客户端连接到一个新的服务器上时,watch将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到 watch的。

会话

Session

在ZooKeeper 客户端与服务端成功完成连接创建后,就建立了一个会话。 ZooKeeper 会 话在整个运行期间的生命周期中,会在不同的会话状态之间进行切换,这些状态一般可分为:CONNECTING、CONNECTED、RECONNECTING、RECONNECTED 和 CLOSE等。

Session是 ZooKeeper 中的会话实体,代表了一个客户端会话。其包含以下4个基本属性:

  • sessionID: 会话 ID, 用来唯一标识一个会话,每次客户端创建新会话的时候, ZooKeeper 都会为其分配一个全局唯一的sessionlD,sessionId通过当前时间戳和serverId经过位运算得到。
  • TimeOut: 会话超时时间。客户端在构造 ZooKeeper实例的时候,会配置一个sessionTimeout参数用于指定会话的超时时间(一般是2tickTime~20tickTime)。 ZooKeeper 客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定会话的超时时间。
  • TickTime(理解上更像是zk的最短时间单位): 下次会话超时时间点。为了便于ZooKeeper 对会话实行“分桶策略”管 理,同时也是为了高效低耗地实现会话的超时检查与清理, ZooKeeper 会为每个会话标记一个下次会话超时时间点(当前的时间戳+会话超时时间)。 TickTime 是一个13位的 long 型数据,其值接近于当前时间加上TimeOut, 但不完全相等。
  • isClosing: 该属性用于标记一个会话是否已经被关闭。通常当服务端检测到一个会话已经超时失效的时候,会将该会话的 isClosing 属性标记为“已关闭”,这样就能确保不再处理来自该会话的新请求了。

SessionTracker

SessionTracker是 ZooKeeper 服务端的会话管理器,负责会话的创建、管理和清理等工作。可以说,整个会话的生命周期都离不开 SessionTracker 的管理。每一个会话在 SessionTracker内部都保留了三份,具体如下。

  • sessionsById: 这是一个 HashMap<Long,SessionImpl> 类型的数据结构,用于根据 sessionlD来管理Session实体。key是sessionlD,value是对应的Session的封装,即为上面的几个Session的属性的封装。
  • sessionsWithTimeout: 这是一个 ConcurrentHashMap<Long,Integer>类型的数据结构,用于根据 sessionID 来管理会话的超时时间。该数据结构和 ZooKeeper 内存数据库相连通,会被定期持久化到快照文件中去。key是sessionId,value是依据当前时刻加上超时时间并向下取整(方便进行批量的分桶的数据超时处理)计算出来的时间戳。
  • sessionsets: 这是一个HashMap<Long,SessionSet>类型的数据结构,用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时检查。key是超时的时间戳,value是对应的Session的集合,每一次的特定会话激活会将当前这个key里面的特定会话移动到后续的某个桶中。

分桶策略

image.png ZooKeeper 的会话管理主要是由SessionTracker 负责的,其采用的会话管理方式为“分桶策略”。所谓分桶策略:将过期时间接近的会话放在同一个桶中进行批量管理处理。 即上面提及的第三个Map:sessionsets key为每个会话的“下次超时时间点”(ExpirationTime),value为对应的这个时间点超时的会话集合。

计算公式: // 计算出会话激活后这个会话的下次超时时间点 NextExpirationTime = currentTime + sessionTimeout // 相当于向下取整处理成离NextExpirationTime最近的上一个ExpirationInterval整数倍的时间戳 ExpirationTime = (NextExpirationTime / ExpirationInrerval + 1) *ExpirationInterval ExpirationInterval 是指 Zookeeper 会话超时检查时间间隔(心跳ping的时间间隔),默认tickTime。

会话激活

为了保持客户端会话的有效性,在ZooKeeper的运行过程中,客户端会在会话超时时间过期范围内向服务端发送 PING 请求来保持会话的有效性,我们俗称“心跳检测”。

  • 只要客户端向服务端发送请求,包括读或写请求,那么就会触发一次会话激活。(理解上更像是被动激活)
  • 如果客户端发现在 sessionTimeout / 3 时间内尚未和服务器进行过任何通信.那么就会主动发起一个PING 请求,服务端收到该请求后,就会触发上述第一种情况下的会话激活。(理解上更像是主动激活)

会话超时检查/会话清理

在ZooKeeper中,会话超时检查同样是由SessionTracker负责的。SessionTracker中有一个单独的线程(超时检查线程)专门进行会话超时检查。

如果一个会话被激活,那么ZooKeeper会将其从上一个会话桶迁移到后续的某个会话桶中。于是,expirationTime1 中留下的所有会话都是尚未被激活的。因此,超时检查线程的任务就是定时检查出这个会话桶中所有剩下的未被迁移的会话。

那么超时检查线程是如何做到定时检查的呢?这里就和ZooKeeper会话的分桶策略紧密联系起来了。在会话分桶策略中,我们将ExpirationInterval的倍数作为时间点来分布会话,因此,超时检查线程只要在这些指定的时间点上进行检查即可,这样既提高了会话检查的效率,而且由于是批量清理,因此性能非常好。

ps : client可能会和follower建立会话链接,这个会话的统一管理还是由Leader管理(包括sessionId的分配,超时和会话激活等),Leader和Follower之间会进行进行Ping(tickTime/2的时间间隔进行同步,当然也存在一个超时的次数syncLimit(默认5次))的心跳检测并同步了数据信息,其中就包括某个会话的会话激活时间点等等。

ZAB协议

ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子消息广播协议)的协议作为ZK数据一致性的核心算法。Zab协议的过程可以分成选举,发现,同步和广播,具体的阶段可以分成Leader选举,数据恢复阶段和广播阶段

ZAB 协议的核心是定义了对于那些会改变 ZooKeeper 服务器数据状态的事务请求的处理方式,即:

  • 所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为 Leader 服务器,而余下的其他服务器则成为 Follower服务器。
  • Leader服务器负责将一个客户端事务请求转换成一个事务 Proposal(提议),并将该 Proposal分发给集群中所有的 Follower服务器。
  • Leader服务器需要等待所有 Follower服务器的反馈, 一旦超过半数的 Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的 Follower 服务器分发Commit 消息,要求其将前一个Proposal进行提交(2PC二阶段提交)。

image.png

image.png

Leader选举

基本概念

SID: 服务器ID SID是一个数字,用来唯一标识一台ZooKeeper集群中的机器,每台机器不能重复,和 myid的值一致。 ZXID: 事务ID ZXID 是一个事务 ID, 用来唯一标识一次服务器状态的变更。在某一个时刻,集群中每台机器的ZXID值不一定全都一致,异步同步,每次写半数肯定有些慢一些啦。 Vote: 投票 Leader 选举,顾名思义必须通过投票来实现。当集群中的机器发现自己无法检测到 Leader机器的时候,就会开始尝试进行投票。 Quorum: 法定人数(过半机器数) 这是整个Leader选举算法中最重要的一个术语,我们可以把这个术语理解为是一个量词, 指的是ZooKeeper集群中过半的机器数,如果集群中总的机器数是n的话,那么可以通 过下面这个公式来计算quorum 的值:quorum=(n/2+1),通过过半机制能够避免脑裂

ps: 如果想搭建一个能够允许 N 台机器 down 掉的集群,那么就要部署一个由 2*N+1 台服务器构成的 ZooKeeper 集群。所以部署3个节点,那么就得至少有2个节点可用则该集群才可用。4个节点同样还是要2个以上。所以 Zookeeper集群部署的节点(非Observer)数一般为奇数。

投票流程

1.开始第一次投票(投自己) 2.变更选票(依据epcho(每届朝代id),zxid(事务id),sid(服务器ID)))

  • 根据投票中的epcho>当前自己的epcho;
  • 收到的投票zxid >当前自己的zxid;
  • 如果zxid相同,就看sid; 3.如果一台机器收到超过半数的相同的投票,那么这个vote中对应的sid就是leader
  • vote sid:接收到的投票中所推举 Leader 服务器的SID
  • vote zxid:接收到的投票中所推举Leader 服务器的ZXID
  • self sid:当前服务器自己的SID
  • self zxid:当前服务器自己的ZXID image.png Looking阶段的节点会不断地从recvqueue队列中取出接收到的选票vote,然后进行选票pk。

数据同步(发现与同步)

总结一句话就是Learner(Follower以及Observer)的zxid始终需要和准Leader的zxid保持一致。

同步流程

  • Learner会向Leader注册,然后Leader会从这个数据包中解析出该Learner的 currentEpoch和lastZxid。
  • Leader会从内存中取出最小和最大提交的zxid事务id,然后对learner采取不同的同步策略。
    • peerLastZxid: Learner 服务器最后处理的ZXID。
    • minCommittedLog:Leader 服务器提议缓存队列committedLog中的最小ZXID。
    • maxCommittedLog:Leader服务器提议缓存队列committedLog中的最大ZXID。
  • 通过learnerHandler对不同learner采取不同的同步策略。

同步策略

  • peerLastZxid介于minCommittedLog 和 maxCommitedLog 之间。处理方式:直接差异化同步 (Learner服务器发送DIFF指令进行同步)
  • peerLastZxid介于minCommittedLog 和 maxCommitedLog 之间,但是Learner存在Leader中不存在的事务日志。处理方式:先回滚再差异化同步 (TRUNC+DIFF同步)
  • peerLastZxid 大于maxCommittedLog。处理方式:仅回滚同步(TRUNC同步)
  • peerLastZxid 小于minCommittedLog或者leader服务器上面没有缓存队列且peerLastZxid!=lastProcessZxid。全量同步(SNAP同步)。Leader服务器将本机上的全量内存数据都同步给Learner。

消息广播

主要是针对于写请求,当任意节点收到写请求时,最终都会转发给leader节点。leader节点对生成对应的事务提案,并生成zxid,然后将提案广播给其他节点,其他节点收到请求以事务日志形式写入本地磁盘成功后反馈ack响应。当收到超过半数的ack响应后,则回复客户端写入成功,并向集群发送commit消息,提交事务。当各节点收到commit后,会完成事务的提交,数据写到数据副本中。

zxid是一个64位数字,高32位表示当前leader的epoch【每届朝代年号的类似东西】,低32位是一个单增的计算器【每个新的epoch从0重新开始计数】

在实际中,leader会收到多个写请求,为了统计每个写请求来自于follower的ack反馈数,在leader服务器中会为每个follower维护一个消息队列,然后将需要广播的提案依次放入到队列中,并基于FIFO的规则发送消息。leader只需要统计每个zxid对应的提案超过半数有ack反馈就行,无需等待全部的follower节点。因此可以说ZK是CAP中比较典型的CP的分布式模型,注重数据一致性以及网络分区。这和以AP为主的例如redis不同,redis是在主节点把数据成功写入后,直接响应客户端写入成功。是会异步同步其他从节点,但是不会等待从节点返回之后再响应客户端。

image.png

数据与存储

内存数据存储 DataNode 是数据存储的最小单元。 DataNode 内部除了保存了节点的数据内容 (data[])、ACL列 表 (acl) 和节点状态 (stat) 之外,正如最基本的数据结构中对树的描述,还记录了父节点 (parent) 的引用和子节点列表 (children) 两个属性 image.png

事务日志

每个更新请求,必须先将事务日志写到文件中,然后才把数据同步到内存数据库。 日志文件特点:

  • 文件大小都是64MB。剩余空间不足4KB时,则会再预分配64MB创建新的事务日志文件(重启也会和上一个可写的日志断开,重新从一个新的事务日志开始写)。
  • 文件名后缀名是zxid(并且是写入日志文件的第一条事务zxid)的十六进制表示的数字,依次递增。

image.png

数据快照

数据快照是ZooKeeper数据存储中另一个非常核心的运行机制。顾名思义,数据快照用来记录ZooKeeper服务器上某一个时刻的全量内存数据内容,并将其写入到指定的磁盘文件中。 总结就是: 依据这个频次:(再进行了snapCount/2~snapCount之间的随机事务操作后进行快照同步),切换一个新的事务日志文件,重新创建一个新的事务文件(相当于当前的快照记录的是基于最后一个旧的日志文件以及之前所有日志文件的结果?),开启一个新的数据快照异步线程将内存数据写到磁盘。

image.png

image.png tips:

  1. 一般建议给事务日志和快照数据单独搞个磁盘 提高读写效率
  2. 能通过读取事务日志和快照数据快速启动服务

配置文件参数

  • dataDir 该参数无默认值,必须配置,不支持系统属性方式配置。 参数dataDir用于配置ZooKeeper服务器存储快照文件的目录。默认情 况下,如果没有配置参数dataLogDir,那么事务日志也会存储在这个目录 中。考虑到事务日志的写性能直接影响ZooKeeper整体的服务能力,因此建议同时通过参数dataLogDir来配置ZooKeeper事务日志的存储目录。

  • dataLogDir 参数dataLogDir用于配置ZooKeeper服务器存储事务日志文件的目录。默认情况下,ZooKeeper会将事务日志文件和快照数据存储在同一个目录中,读者应尽量将这两者的目录区分开来。 另外,如果条件允许,可以将事务日志的存储配置在一个单独的磁盘上。事务日志记录对于磁盘的性能要求非常高,为了保证数据的一致性,ZooKeeper在返回客户端事务请求响应之前,必须将本次请求对应的事务日志写入到磁盘中。因此,事务日志写入的性能直接决定了ZooKeeper在处理事务请求时的吞吐。

  • Snapcount(达到这个事务日志个数左右(随机)进行快照同步) 该参数有默认值:100000,可以不配置,仅支持系统属性方式 配置:zookeeper.snapCount。

  • preAllocsize 事务日志在磁盘上预分配的内存空间 为了减少开辟磁盘空间的时间(空余分配磁盘小于4KB的时候会触发)该参数有默认值:65536KB,即64MB,可以不配置。

  • 会话超时时间 minSessionTimeout,maxSessionTimeout 这两个参数有默认值,分别是参数tickTime值的2倍和20倍, 即默认的会话超时时间在2tickTime~20tickTime范围内,单 位毫秒,可以不配置,不支持系统属性方式配置。

  • Ticktime 该参数有默认值:3000,单位是毫秒(ms),可以不配置,不支持系统属性方式配置。 参数tickTime用于配置ZooKeeper中最小时间单元的长度

服务启动

image.png

ZK缺点

  • 服务缺乏可扩展性无法直接横向扩容(需要重启整个集群或者逐台机器重启,zk 3.5之后支持动态扩容 www.usenix.org/system/file…) 两种方式进行水平扩展:
    【1】全部重启:关闭所有 Zookeeper服务,修改配置之后启动。不影响之前客户端的会话。 【2】逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。
  • 存储缺乏可扩展性,每个znode节点最多存储1M(系统定义)
  • 写有瓶颈并且随着扩容写的qps会越来越低,因为需要和更多的follower进行通信 image.png
  • 选主期间对外不可用

参考链接: juejin.cn/post/709999…