zookeeper原理与应用

2,081 阅读12分钟

zookeeper(简称zk),顾名思义,为动物园管理员的意思,动物对应服务节点,zk是这些节点的管理者。
在分布式场景中,zk的应用非常广泛。 zk的一致性,是指数据在多个副本之间保持一致的特性
CAP定理告诉我们,在分布式系统设计中,P(分区容错性)是不可缺少的,因此只能在A(可用性)与C(一致性)间做取舍。 zookeeper是CP

基本概念:

  • 数据节点(dataNode):zk数据模型中的最小数据单元,数据模型是一棵树,由斜杠(/)分割的路径名唯一标识,数据节点可以存储数据内容及一系列属性信息,同时还可以挂载子节点,构成一个层次化的命名空间。

  • 会话(Session):指zk客户端与zk服务器之间的会话,在zk中,会话是通过客户端和服务器之间的一个TCP长连接来实现的。通过这个长连接,客户端能够使用心跳检测与服务器保持有效的会话,也能向服务器发送请求并接收响应,还可接收服务器的Watcher事件通知。Session的sessionTimeout,是会话超时时间,如果这段时间内,客户端未与服务器发生任何沟通(心跳或请求),服务器端会清除该session数据,客户端的TCP长连接将不可用,这种情况下,客户端需要重新实例化一个Zookeeper对象。

  • 事务及ZXID:事务是指能够改变Zookeeper服务器状态的操作,一般包括数据节点的创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每个事务请求,zk都会为其分配一个全局唯一的事务ID,即ZXID,是一个64位的数字,高32位表示该事务发生的集群选举周期(集群每发生一次leader选举,值加1),低32位表示该事务在当前选择周期内的递增次序(leader每处理一个事务请求,值加1,发生一次leader选择,低32位要清0)。 事务日志:所有事务操作都是需要记录到日志文件中的,可通过 dataLogDir配置文件目录,文件是以写入的第一条事务zxid为后缀,方便后续的定位查找。zk会采取“磁盘空间预分配”的策略,来避免磁盘Seek频率,提升zk服务器对事务请求的影响能力。默认设置下,每次事务日志写入操作都会实时刷入磁盘,也可以设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。

  • 数据快照:数据快照是zk数据存储中另一个非常核心的运行机制。数据快照用来记录zk服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中,可通过dataDir配置文件目录。可配置参数snapCount,设置两次快照之间的事务操作个数,zk节点记录完事务日志时,会统计判断是否需要做数据快照(距离上次快照,事务操作次数等于snapCount/2~snapCount 中的某个值时,会触发快照生成操作,随机值是为了避免所有节点同时生成快照,导致集群影响缓慢)。

  • 过半:所谓“过半”是指大于集群机器数量的一半,即大于或等于(n/2+1),此处的“集群机器数量”不包括observer角色节点。leader广播一个事务消息后,当收到半数以上的ack信息时,就认为集群中所有节点都收到了消息,然后leader就不需要再等待剩余节点的ack,直接广播commit消息,提交事务。选举中的投票提议及数据同步时,也是如此,leader不需要等到所有learner节点的反馈,只要收到过半的反馈就可进行下一步操作。

Zookeeper的角色及架构

zk集群由多个节点组成,其中有且仅有一个leader,处理所有事务请求;follower及observer统称learner。learner需要同步leader的数据。follower还参与选举及事务决策过程。zk客户端会打散配置文件中的serverAddress 顺序并随机组成新的list,然后循环按序取一个服务器地址进行连接,直到成功。follower及observer会将事务请求转交给leader处理。

Zookeeper的一致性协议

ZAB(ZooKeeper Atomic Broadcast)是为ZooKeeper设计的一种支持崩溃恢复的原子广播协议。

包括崩溃恢复(选主+数据同步)和消息广播(事务操作)。

当leader崩溃或者leader失去大多数的follower 进入恢复模式

崩溃恢复  

半数通过 – 3台机器 挂一台 2>3/2– 4台机器 挂2台 2!>4/2

(1)设置状态为LOOKING,初始化内部投票Vote (id,zxid) 数据至内存,并将其广播到集群其它节点。节点首次投票都是选举自己作为leader,将自身的服务ID、处理的最近一个事务请求的ZXID(ZXID是从内存数据库里取的,即该节点最近一个完成commit的事务id)及当前状态广播出去。然后进入循环等待及处理其它节点的投票信息的流程中。
(2)循环等待流程中,节点每收到一个外部的Vote信息,都需要将其与自己内存Vote数据进行PK,规则为取ZXID大的,若ZXID相等,则取ID大的那个投票。若外部投票胜选,节点需要将该选票覆盖之前的内存Vote数据,并再次广播出去;同时还要统计是否有过半的赞同者与新的内存投票数据一致,无则继续循环等待新的投票,有则需要判断leader是否在赞同者之中,在则退出循环,选举结束,根据选举结果及各自角色切换状态,leader切换成LEADING、follower切换到FOLLOWING、observer切换到OBSERVING状态

广播(同步)

一旦leader已经和多数的follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态。
这时候当一个server加入zookeeper服务中,它会在恢复模式下启动,发现leader,并和leader进行状态同步。待到同步结束,它也参与消息广播。
Zookeeper服务一直维持在Broadcast状态,直到leader崩溃了或者leader失去了大部分的followers支持

非常棒的zk工作流程介绍

ZooKeeper提供了什么?

  • 文件系统
  • 通知机制

Zookeeper可以用来做什么(应用)

zk常用于命名服务、配置管理、集群管理、分布式锁、队列管理

命名服务

名字服务
这个主要是作为分布式命名服务,利用zookeeper的文件系统,通过调用zk的create node api,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。

命名服务是指通过指定的名字来获取资源或者服务的地址,提供者的信息。利用Zookeeper很容易创建一个全局的路径,而这个路径就可以作为一个名字,它可以指向集群中的集群,提供的服务的地址,远程对象等。简单来说使用Zookeeper做命名服务就是用路径作为名字,路径上的数据就是其名字指向的实体。
服务提供者在启动的时候,向ZK上的指定节点(如/{serviceName}/providers)目录下写入自己的URL地址,这个操作就完成了服务的发布。
服务消费者启动的时候,订阅/{serviceName}/providers/目录下的提供者URL地址(如果一次性使用直接读取即可),并向/{serviceName}/consumers/目录下写入自己的URL地址(如果服务提供者者需要通过ZK获取服务消费者身份,可选)。
所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。
如dubbo做的就是上面这部分操作

nameservice:

  • -m 程序运行的方式,指定是服务提供者provider还是服务消费者consumer
  • -n 服务名称
  • -s Zookeeper的服务地址IP:PORT
服务提供者:  
  nameservice -m provider -n ServiceDemo -s 172.17.0.36:2181  
服务消费者:
  nameservice -m consumer -n ServiceDemo -s 172.17.0.36:2181
服务监控者:
  nameservice -m monitor -n ServiceDemo -s 172.17.0.36:2181

第一条命令启动服务提供者,它提供一个ServiceDemo的服务,首次启动后会创建/NameService/、/NameService/ServiceDemo/、/NameService/ServiceDemo/provider/几个路径(永久节点)。然后在服务提供进程在/NameService/ServiceDemo/provider/下创建临时序列节点。

第二条命令启动一个服务消费进程,它在/NameService/ServiceDemo/consumer/下创建临时序列节点,并watch /NameService/ServiceDemo/provider/下的子节点变化事件,及时更新provider列表。

第三条命令是启动一个服务监控进程,它watch /NameService/ServiceDemo/provider,/NameService/ServiceDemo/consumer/两个路径的子节点变化,及时更新provider列表和comsumer列表。

配置管理

通过zookeeper的监听功能 实现配置管理 common.yml改了,也不需要系统A、B、C重启。

系统A、B、C都使用着这份配置 做法:我们可以将common.yml这份配置放在ZooKeeper的Znode节点中,系统A、B、C监听着这个Znode节点有无变更,如果变更了,及时响应。

系统A、B、C监听着ZooKeeper的节点,一旦common.yml内容有变化,及时响应

参考 : 基于zookeeper实现统一配置管理

集群管理

利用ZooKeeper的两大特性,就可以实现另一种集群机器存活性监控的系统。例如,监控系统在/clusterServers节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建一个临时节点:/clusterServer/[Hostname]。这样一来,监控系统就能够实时检测到机器的变动情况,至于后续处理就是监控系统的业务了。

分布式锁

zookeeper实现分布式锁的算法流程,假设锁空间的根节点为/lock:

  1. 客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
  2. 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;
  3. 执行业务代码;
  4. 完成业务流程后,删除对应的子节点释放锁。

这个算法有个极大的优化点:假如当前有1000个节点在等待锁,如果获得锁的客户端释放锁时,这1000个客户端都会被唤醒,这种情况称为“羊群效应”;在这种羊群效应中,zookeeper需要通知1000个客户端,这会阻塞其他的操作,最好的情况应该只唤醒新的最小节点对应的客户端。应该怎么做呢?在设置事件监听时,每个客户端应该对刚好在它之前的子节点设置事件监听,例如子节点列表为/lock/lock-0000000000、/lock/lock-0000000001、/lock/lock-0000000002,序号为1的客户端监听序号为0的子节点删除消息,序号为2的监听序号为1的子节点删除消息。 所以调整后的分布式锁算法流程如下:

  1. 客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
  2. 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;
  3. 执行业务代码;
  4. 完成业务流程后,删除对应的子节点释放锁。

我们这里直接使用curator这个开源项目提供的zookeeper分布式锁实现

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.0</version>
</dependency>
public static void main(String[] args) throws Exception {
    //创建zookeeper的客户端
    RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
    CuratorFramework client = CuratorFrameworkFactory.newClient("10.21.41.181:2181,10.21.42.47:2181,10.21.49.252:2181", retryPolicy);
    client.start();

    //创建分布式锁, 锁空间的根节点路径为/curator/lock
    InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock");
    mutex.acquire();
    //获得了锁, 进行业务流程
    System.out.println("Enter mutex");
    //完成业务流程, 释放锁
    mutex.release();
    
    //关闭客户端
    client.close();
}

参考 : www.dengshenyu.com/java/%E5%88…

队列管理

Zookeeper可以处理两种类型的队列:

  • 同步队列
    当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。例如一个班去旅游,看是否所有人都到齐了,到齐了就发车。例如有个大任务分解为多个子任务,要所有子任务都完成了才能进入到下一流程。
  • 先进先出队列
    按照FIFO方式进行入队和出队