ZooKeeper
是一个开源的分布式协调服务框架,为分布式系统提供一致性服务。
可以理解为:ZooKeeper = 文件系统 + 监听通知机制
使用ZooKeeper的开源项目
许多著名的开源项目用到了 ZooKeeper,比如:
Kafka : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。
Hbase : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。
Hadoop : ZooKeeper 为 Namenode 提供高可用支持。
Dubbo:阿里巴巴集团开源的分布式服务框架,它使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表。
CAP和BASE理论
一个分布式系统必然会存在一个问题:因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡 。这就是著名的 CAP
定理。
CAP理论中,P
(分区容忍性)是必然要满足的,因为毕竟是分布式,不能把所有的应用全放到一个服务器里面,这样服务器是吃不消的。所以,只能从AP(可用性)和CP(一致性)中找平衡。
怎么个平衡法呢?在这种环境下出现了BASE理论:即使无法做到强一致性,但分布式系统可以根据自己的业务特点,采用适当的方式来使系统达到最终的一致性。BASE理论由:Basically Avaliable
基本可用、Soft state
软状态、Eventually consistent
最终一致性组成。
ZooKeeper使用的ZAB算法 见Untitled
Zookeeper的数据模型
ZooKeeper 数据模型(Data model)采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且,每个节点还可以拥有 N 个子节点,最上层是根节点以/
来代表。
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。由于ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。
和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。默认有四种类型的znode:
持久化目录节点 PERSISTENT:客户端与zookeeper断开连接后,该节点依旧存在。
持久化顺序编号目录节点 PERSISTENT_SEQUENTIAL:客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号。
临时目录节点 EPHEMERAL:客户端与zookeeper断开连接后,该节点被删除。
临时顺序编号目录节点 EPHEMERAL_SEQUENTIAL:客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号。
监听机制
Watcher 监听机制是 Zookeeper 中非常重要的特性,我们基于 Zookeeper上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 Zookeeper 实现分布式锁、集群管理等多种功能,它有点类似于订阅的方式,即客户端向服务端 注册 指定的 watcher
,当服务端符合了 watcher
的某些事件或要求则会 向客户端发送事件通知 ,客户端收到通知后找到自己定义的 Watcher
然后 执行相应的回调方法 。
当客户端在Zookeeper上某个节点绑定监听事件后,如果该事件被触发,Zookeeper会通过回调函数的方式通知客户端,但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 Watcher 的客户端不会再次收到消息(Watcher是一次性的操作),可以通过循环监听去达到永久监听效果。
ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:
客户端注册 Watcher,注册 watcher 有 3 种方式,getData、exists、getChildren。
服务器处理 Watcher 。
客户端回调 Watcher 客户端。
监听通知机制的流程如⇒:
首先要有一个main()线程
在main线程中创建zkClient,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
通过connect线程将注册的监听事件发送给Zookeeper。
在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
listener线程内部调用了process()方法。
会话session
Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接,客户端与服务端之间的任何交互操作都和Session 息息相关,其中包含zookeeper的临时节点的生命周期、客户端请求执行以及Watcher通知机制等。
client 端连接 server 端默认的 2181 端口,也就是 session 会话。
接下来,我们从全局的会话状态变化到创建会话再到会话管理三个方面来看看Zookeeper是如何处理会话相关的操作。
会话状态
session会话状态有:
connecting:连接中,session 一旦建立,状态就是 connecting 状态,时间很短。
connected:已连接,连接成功之后的状态。
closed:已关闭,发生在 session 过期,一般由于网络故障客户端重连失败,服务器宕机或者客户端主动断开。
客户端需要与服务端创建一个会话,这个时候客户端需要提供一个服务端地址列表,host1 : port,host2: port ,host3:port
,一般由地址管理器(HostProvider)管理,然后根据地址创建zookeeper对象。这个时候客户端的状态则变更为CONNECTING,同时客户端会根据上述的地址列表,按照顺序的方式获取IP来尝试建立网络连接,直到成功连接上服务器,这个时候客户端的状态就可以变更为CONNECTED。在Zookeeper服务端提供服务的过程中,有可能遇到网络波动等原因,导致客户端与服务端断开了连接,这个时候客户端会进行重新连接操作这个时候的状态为CONNECTING,当连接再次建立后,客户端的状态会再次更改为CONNECTED,也就是说只要在Zookeeper运行期间,客户端的状态总是能保持在CONNECTING或者是CONNECTED。当然在建立连接的过程中,如果出现了连接超时、权限检查失败或者是在建立连接的过程中,我们主动退出连接操作,这个时候客户端的状态都会变成CLOSE状态。
会话ID的生成
一个会话必须包含以下几个基本的属性:
SessionID : 会话的ID,用来唯一标识一个会话,每一次客户端建立连接的时候,Zookeeper服务端都会给其分配一个全局唯一的sessionID。在Zookeeper中,无论是哪台服务器为客户端分配的
sessionID
,都务必保证全局唯一。Timeout:一次会话的超时时间,客户端在构造Zookeeper实例的时候,会配置一个sessionTimeOut参数用于指定会话的超时的时间。Zookeeper服务端会按照连接的客户端发来的TimeOut参数来计算并确定超时的时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在超时规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
ExpirationTime:TimeOut是一个相对时间,而ExpirationTime则是在时间轴上的一个绝对过期时间。可能你也会想到,一个比较通用的计算方法就是:
ExpirationTime = CurrentTime + Timeout
。 这样算出来的时间最准确,但ZK可不是这么算的,下面会讲具体计算方式及这样做的原因。TickTime:下一次会话超时的时间点,为了便于Zookeeper对会话实行分桶策略管理,同时也是为了高效低耗地实现会话的超时检查与清理,Zookeeper会为每个会话标记一个下次会话超时时间点。TickTime是一个13位的Long类型的数值,一般情况下这个值接近TimeOut,但是并不完全相等。
isCloseing:用来标记当前会话是否已经处于被关闭的状态。如果服务端检测到当前会话的超时时间已经到了,就会将isCloseing属性标记为已经关闭,这样以后即使再有这个会话的请求访问也不会被处理。
SessionTracker与ClientCnxn
SessionTracker是Zookeeper中的会话管理器,负责整个zk生命周期中会话的创建、管理和清理操作,而每一个会话在Sessiontracker内部都保留了如下三个数据结构,大体如下:
protected final ConcurrentHashMap<Long, SessionImpl> sessionsById = new ConcurrentHashMap<Long, SessionImpl>();
private final ConcurrentMap<Long, Integer> sessionsWithTimeout;
sessionsWithTimeout这是一个ConcurrentHashMap类型的数据结构,用来管理会话的超时时间,这个参数会被持久化到快照文件中去
sessionsById是一个HashMap类型的数据结构,用于根据sessionId来管理session实体
sessionsSets同样也是一个HashMap类型的数据结构,用来会话超时的时候进行归档,便于进行会话恢复和管理
ClientCnxn是Zookeeper客户端的核心工作类,负责维护客户端与服务端之间的网络连接并进行一系列网络通信。
ClientCnxn内部又包含两个线程,SendThread是一个I/O线程,主要负责Zookeeper客户端和服务端之间的网络I/O通信,EventThread是一个事件线程,主要负责对服务端事件进行处理。
SendThread:SendThread维护了客户端与服务端之间的会话生命周期,其通过一定的周期频率向服务端发送一个PING包来实现心跳检测。此外,SendThread管理了客户端所有的请求发送和响应接受操作,其将上层客户端API操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调。同时,SendThread还负责将来自服务端的事件传递给EventThread去处理。
EventThread:EventThread负责客户端的事件处理,并触发客户端注册的Watcher监听。EventThread中有一个waitingEvents队列,用于临时存放那些需要被触发的Object,包括那些客户端注册的Watcher和异步接口中注册的回调器AsyncCallback。EventThread会不断地从waitingEvents队列中取出Object,识别出具体的类型,并分别调用process(Watcher)和processResult(AsyncCallback)接口方法来实现对事件的触发和回调。
ClientCnxn中有两个核心队列outgoingQueue和pendingQueue,分别代表客户端的请求发送队列和服务端响应的等待队列。
outgoing队列专门用于存储那些客户端需要发送到服务端的Packet集合
pending队列存储那些已经从客户端发送到服务端的,但是需要等待服务端响应的Packet结合
clientCnxnSocket是底层Socket通信层,定义了Socket通信的接口,为了便于对底层Socket层进行扩展,例如使用Netty来实现和使用过NIO来实现。在Zookeeper中默认的实现是ClientCnxnSocketNIO,主要负责对请求的发送和响应的接收过程。
会话创建
会话的创建的流程如下:
client随机选一个服务端地址列表提供的地址,委托给
ClientCnxnSocket
去创建与zk之间的TCP长连接。SendThread会负责根据当前客户端的设置,构造出一个ConnectRequest请求,该请求代表了客户端试图与服务器创建一个会话。同时,Zookeeper客户端还会进一步将请求包装成网络IO的Packet对象,放入请求发送队列——outgoingQueue中去。
当客户端请求准备完毕后,ClientCnxnSocket从outgoingQueue中取出Packet对象,将其序列化成ByteBuffer后,向服务器进行发送。
服务端的SessionTracker为该会话分配一个sessionId,并发送响应。
Client收到响应后,会首先判断当前的客户端状态是否是已初始化,如果尚未完成初始化,那么就认为该响应一定是会话创建请求的响应,直接交由readConnectResult方法来处理该请求。
ClientCnxnSocket会对接受到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到Zookeeper服务端分配的会话SessionId。
连接成功后,一方面需要通知SendThread线程,进一步对客户端进行会话参数设置,包括readTimeout和connectTimeout等,并更新客户端状态;另一方面,需要通知地址管理器HostProvider当前成功连接的服务器地址。
为了能够让上层应用感知到会话的成功创建,SendThread会生成一个事件SyncConnected-None,代表客户端与服务器会话创建成功,并将该事件传递给EventThread线程。
EventThread线程收到事件后,会从ClientWatchManager管理器中查询出对应的Watcher,针对SyncConnected-None事件,那么就直接找出存储的Watcher,然后将其放到EventThread的waitingEvents队列中。
EventThread不断地从waitingEvents队列中取出待处理的Watcher对象,然后直接调用该对象的process接口方法,以达到触发Watcher的目的。
至此,Zookeeper客户端完整的一次会话创建过程已经全部完成了。
会话超时管理
Session是由ZK服务端来进行管理的,一个服务端可以为多个客户端服务,也就是说,有多个Session,那这些Session是怎么样被管理的呢?而分桶机制可以说就是其管理的一个手段。ZK服务端会维护着一个个"桶",然后把Session们分配到一个个的桶里面。而这个区分的维度,就是ExpirationTime
每个Session的超时时间是一个很分散的值,假设有1000个Session,很可能就会有1000个不同的超时时间,进而有1000个桶,这样有啥意义吗?因此zk的ExpirationTime 用了下面的计算方式
ExpirationTime = CurrentTime + SessionTimeout;
ExpirationTime = (ExpirationTime / ExpirationInterval + 1) * ExpirationInterval;
Zookeeper分布式锁
ZooKeeper负载均衡和Nginx负载均衡区别:
ZooKeeper不存在单点问题,zab机制保证单点故障可重新选举一个leader只负责服务的注册与发现,不负责转发,减少一次数据交换(消费方与服务方直接通信),需要自己实现相应的负载均衡算法。
Nginx存在单点问题,单点负载高数据量大,需要通过 KeepAlived + LVS 备机实现高可用。每次负载,都充当一次中间人转发角色,增加网络负载量(消费方与服务方间接通信),自带负载均衡算法。