ZooKeeper 集群使用简介
2019-07-21 22:31:39 阿炯

ZooKeeper是apache旗下的hadoop子项目,它一个开源的,分布式的服务协调器,通过zookeeper可以实现服务间的同步与配置维护。通常情况下,在分布式应用开发中,协调服务这样的工作是一件很难的事情,很容易出现死锁,不恰当的选举竞争等。zookeeper就是担负起了分布式协调的重担。

ZooKeeper 简介

ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services. All of these kinds of services are used in some form or another by distributed applications. Each time they are implemented there is a lot of work that goes into fixing the bugs and race conditions that are inevitable. Because of the difficulty of implementing these kinds of services, applications initially usually skimp on them ,which make them brittle in the presence of change and difficult to manage. Even when done correctly, different implementations of these services lead to management complexity when the applications are deployed.

ZooKeeper 是一个开源的为分布式应用提供分布式协调的服务。它公开了一组简单的原语,分布式应用程序可以基于这些原语实现更高级别的服务,包括同步、维护配置、组和命名。它的设计易于编程,它使用一个遵循文件系统中常见的目录树结构的数据模型。它在 Java 环境中运行,对 Java 和 C 都有绑定。协调服务是出了名的难,它们特别容易出错,如竞态条件和死锁。ZooKeeper 背后的动机是让分布式应用从零开始实现一站式协调服务。zookeeper只能启动单数,比如1台、3台、7台等等,不能偶数台,偶数台的话假设有两台,那么只有一台机器再运行,因为如果是偶数的话,选举出来的管理者有可能两个borker得到的票数相同,奇数的话就不会出现这个情况。

ZooKeeper的特点:
使用简单:ZooKeeper允许分布式程序通过一个类似于标准文件系统的共享的层次化名称空间来相互协调。名称空间由数据寄存器(称为znode)组成,在ZooKeeper中,它们类似于文件和目录。与为存储而设计的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以达到高吞吐量和低延迟数

同步与复制:组成ZooKeeper服务的服务器必须互相有感知。客户端连接到一个ZooKeeper服务器。客户端维护一个TCP连接,通过它发送请求、获取响应、获取观察事件和发送心跳。如果连接到服务器的TCP连接中断,客户端将连接到另一个服务器。

有序:在进行大量读操作时,运行速度奇快。

ZooKeeper提供的名称空间非常类似于标准文件系统,名称是由斜杠(/)分隔的路径元素序列,在ZooKeeper的名称空间中,每一个节点都是通过一条路径来标识的。如下图所示 :


ZooKeeper与标准文件系统不同的是,它的节点分为永久节点和临时节点(随着会话断开而消失)。客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。

Zookeeper=文件系统+通知机制

有四种类型的znode:

PERSISTENT-持久化目录节点:客户端与zookeeper断开连接后,该节点依旧存在

PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

EPHEMERAL-临时目录节点:客户端与zookeeper断开连接后,该节点被删除

EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点:客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

zookeeper功能非常强大,可以实现诸如分布式应用配置管理、统一命名服务、状态同步服务、集群管理等功能,我们这里拿比较简单的分布式应用配置管理为例来说明。假设程序是分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在把这些配置全部放到zookeeper上去,保存在 zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 zookeeper 的通知,然后从 zookeeper 获取新的配置信息应用到系统中。

配置文件详解

重命名 zoo_sample.cfg 文件为 zoo.cfg

配置说明

tickTime:Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)

initLimit:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 10*2000=20 秒。

集群中的follower跟随者服务器(F)与leader领导者服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限。投票选举新leader的初始化时间:Follower在启动过程中,会从Leader同步所有最新数据,然后确定自己能够对外服务的起始状态。Leader允许F在initLimit时间内完成这个工作。

syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 5*2000=10秒。

集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。在运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。如果发出心跳包在syncLimit之后,还没有收到响应,那么就认为这个已经不在线了。

dataDir:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。

clientPort:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。

server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

标识Server ID

创建三个文件夹zk1/data,zk2/data,zk3/data,在每个目录中创建文件myid 文件,写入当前实例的server id,即1.2.3。

运行特点

1)Zookeeper:一个领导者(leader),多个跟随者(follower)组成的集群
2)Leader负责进行投票的发起和决议,更新系统状态
3)Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
4)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务
5)全局数据一致:每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的
6)更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行
7)数据更新原子性,一次数据更新要么成功,要么失败
8)实时性,在一定时间范围内,client能读到最新数据

选举机制

1)半数机制:集群中半数以上机器存活,集群可用。所以zookeeper适合装在奇数台机器上。
2)Zookeeper虽然在配置文件中并没有指定master和slave。但zookeeper工作时,是有一个节点为leader,其他则为follower,Leader是通过内部的选举机制临时产生的。


运行模式

Zookeeper有两种运行模式:独立模式(standalone mode)和复制模式(replicated mode)。

独立模式:只有一个zookeeper服务实例,不可保证高可靠性和恢复性,可在测试环境中使用,生产环境不建议使用。

复制模式:复制模式也就是集群模式,有多个zookeeper实例在运行,建议多个zk实例是在不同的服务器上。集群中不同zookeeper实例之间数据不停的同步。有半数以上的实例保持正常运行,zk服务就能正常运行,例如:有5个zk实例,挂了2个,还剩3个,依然可以正常工作;如有6个zk实例,挂了3个,则不能正常工作。每个znode的修改都会被复制到超过半数的机器上,这样就会保证至少有一台机器会保存最新的状态,其余的副本最终都会跟新到这个状态。Zookeeper为实现这个功能,使用了Zab协议,该协议有两个可以无限重复的阶段:

选举领导
集群中所有的zk实例会选举出来一个“领导实例”(leader),其它实例称之为“随从实例”(follower)。如果leader出现故障,其余的实例会选出一台leader,并一起提供服务,若之前的leader恢复正常,便成为follower。选举follower是一个很快的过程,性能影响不明显。Leader主要功能是协调所有实例实现写操作的原子性,即:所有的写操作都会转发给leader,然后leader会将更新广播给所有的follower,当半数以上的实例都完成写操作后,leader才会提交这个写操作,随后客户端会收到写操作执行成功的响应。

原子广播
上边已经说到:所有的写操作都会转发给leader,然后leader会将更新广播给所有的follower,当半数以上的实例都完成写操作后,leader才会提交这个写操作,随后客户端会收到写操作执行成功的响应。这么来的话,就实现了客户端的写操作的原子性,每个写操作要么成功要么失败。逻辑和数据库的两阶段提交协议很像。

复制模式下的数据一致性

Znode的每次写操作都相当于数据库里的一次事务提交,每个写操作都有个全局唯一的ID,称为:zxid(ZooKeeper Transaction)。ZooKeeper会根据写操作的zxid大小来对操作进行排序,zxid小的操作会先执行。zk下边的这些特性保证了它的数据一致性:

顺序一致性
任意客户端的写操作都会按其发送的顺序被提交。如果一个客户端把某znode的值改为a,然后又把值改为b(后面没有其它任何修改),那么任何客户端在读到值为b之后都不会再读到a。

原子性
这一点再前面已经说了,写操作只有成功和失败两种状态,不存在只写了百分之多少这么一说。

单一系统映像
客户端只会连接host列表中状态最新的那些实例。如果正在连接到的实例挂了,客户端会尝试重新连接到集群中的其他实例,那么此时滞后于故障实例的其它实例都不会接收该连接请求,只有和故障实例版本相同或更新的实例才接收该连接请求。

持久性
写操作完成之后将会被持久化存储,不受服务器故障影响。

及时性
在对某个znode进行读操作时,应该先执行sync方法,使得读操作的连接所连的zk实例能与leader进行同步,从而能读到最新的类容。

注意:sync调用是异步的,无需等待调用的返回,zk服务器会保证所有后续的操作会在sync操作完成之后才执行,哪怕这些操作是在执行sync之前被提交的。


zookeeper端口说明

zookeeper有三个端口(可以修改定制)
2181,2888,3888

3个端口的作用:
2181:对cline端提供服务
3888:选举leader使用
2888:集群内机器通讯使用(Leader监听此端口)

另外还可能启动一个随机的高位端口(10000以上),侦听在0.0.0.0上,这可能是jmx相关的,用来做java jvm的监控;可远程连接, 在环境变量里设置:
export  JMXDISABLE=true

来关闭该功能。

部署时注意

单机单实例,只要端口不被占用即可。
单机伪集群(单机,部署多个实例),三个端口必须修改为组组不一样,如:
myid1 : 2181,3888,2888
myid2 : 2182,3788,2788
myid3 : 2183,3688,2688

集群(一台机器部署一个实例)
集群为大于等于3个基数,如 3、5、7....,不宜太多,集群机器多了选举和数据同步耗时时拉长,不稳定。三台选举+N台observe是一个很不错组合,下面会深入讲解相关的要点。


ZooKeeper 集群要点

1.集群角色

一个 ZooKeeper 集群同一时刻只会有一个 Leader,其他都是 Follower 或 Observer。ZooKeeper 配置很简单,每个节点的配置文件(zoo.cfg)都是一样的,只有 myid 文件不一样。myid 的值必须是 zoo.cfg中server.{数值} 的{数值}部分。

在装有 ZooKeeper 的机器的终端执行 zookeeper-server status 可以看当前节点的

ZooKeeper是什么角色(Leader or Follower)。

ZooKeeper 默认只有 Leader 和 Follower 两种角色,没有 Observer 角色。为了使用 Observer 模式,在任何想变成Observer的节点的配置文件中加入:peerType=observer 并在所有 server 的配置文件中,配置成 observer 模式的 server 的那行配置追加 :observer

2.节点读写服务分工

1).ZooKeeper 集群的所有机器通过一个 Leader 选举过程来选定一台被称为『Leader』 的机器,Leader服务器为客户端提供读和写服务。

2).Follower 和 Observer 都能提供读服务,不能提供写服务。两者唯一的区别在于, Observer机器不参与 Leader 选举过程,也不参与写操作的『过半写成功』策略,因 此 Observer 可以在不影响写性能的情况下提升集群的读性能。

3 . Session

Session 是指客户端会话,在讲解客户端会话之前,我们先来了解下客户端连接。在 ZooKeeper 中,一个客户端连接是指客户端和 ZooKeeper 服务器之间的TCP长连接。ZooKeeper 对外的服务端口默认是2181,客户端启动时,首先会与服务器建立一个TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测和服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能通过该连接接收来自服务器的 Watch 事件通知。Session 的 SessionTimeout 值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在 SessionTimeout 规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话 仍然有效。

4 . 数据节点

zookeeper的结构其实就是一个树形结构,leader就相当于其中的根结点,其它节点就相当于 follow节点,每个节点都保留自己的内容。 zookeeper的节点分两类:持久节点和临时节点
- 持久节点: 所谓持久节点是指一旦这个 树形结构上被创建了,除非主动进行对树节点的移除操 作,否则这个 节点将一直保存在 ZooKeeper 上。

- 临时节点: 临时节点的生命周期跟客户端会话绑定,一旦客户端会话失效,那么这个客户端创 建的所有临时节点都会被移除。

5 . 状态信息

每个节点除了存储数据内容之外,还存储了 节点本身的一些状态信息。用 get 命令可以同时获得某个 节点的内容和状态信息。
在 ZooKeeper 中,version 属性是用来实现乐观锁机制中的『写入校验』的(保证分布式数据原子性操作)。

6 .事物操作

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


7 .Watcher(事件监听器)

是 ZooKeeper 中一个很重要的特性。ZooKeeper允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去。该机制是 ZooKeeper 实现分布式协调服务的重要特性。


ZooKeeper应用的典型场景

ZooKeeper 是一个高可用的分布式数据管理与协调框架。基于对ZAB算法的实现,该框架能够很好地保证分布式环境中数据的一致性。也是基于这样的特性,使得 ZooKeeper 成为了解决分布式一致性问题的利器。

1. 数据发布与订阅(配置中心)

数据发布与订阅,即所谓的配置中心,顾名思义就是发布者将数据发布到 ZooKeeper 节点上供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和动态更新。对于数据量通常比较小数据内容在运行时动态变化,集群中各机器共享,配置一致。这样的全局配置信息就可以发布到 ZooKeeper上,让客户端(集群的机器)去订阅该消息。 发布/订阅系统一般有两种设计模式,分别是推(Push)和拉(Pull)模式。
- 推模式 服务端主动将数据更新发送给所有订阅的客户端
- 拉模式 客户端主动发起请求来获取最新数据,通常客户端都采用定时轮询拉取的方式 ZooKeeper 采用的是推拉相结合的方式:
客户端想服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应 的客户端发送Watcher事件通知,客户端接收到这个消息通知后,需要主动到服务端获取最新的数据。

2. 命名服务

命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务,远程对象等等——这些我们都可以统称他们为名字。

其中较为常见的就是一些分布式服务框架(如RPC)中的服务地址列表。通过在ZooKeepr里创建顺序节点,能够很容易创建一个全局唯一的路径,这个路径就可以作为一个名字。 ZooKeeper 的命名服务即生成全局唯一的ID。

3. 分布式协调服务/通知

ZooKeeper 中特有 Watcher 注册与异步通知机制,能够很好的实现分布式环境下不同机器, 甚至不同系统之间的通知与协调,从而实现对数据变更的实时处理。使用方法通常是不同的客户端如果机器节点发生了变化,那么所有订阅的客户端都能够接收到相应的Watcher通知,并做出相应的处理。 ZooKeeper的分布式协调/通知,是一种通用的分布式系统机器间的通信方式。

4. Master选举

Master 选举可以说是 ZooKeeper 最典型的应用场景了。比如 HDFS 中 Active NameNode 的选举、YARN 中 Active ResourceManager 的选举和 HBase 中 Active HMaster 的选举等。

针对 Master 选举的需求,通常情况下,我们可以选择常见的关系型数据库中的主键特性来 实现:希望成为 Master 的机器都向数据库中插入一条相同主键ID的记录,数据库会帮我们进行 主键冲突检查,也就是说,只有一台机器能插入成功我们就认为向数据库中成功插入数据 的客户端机器成为Master。

依靠关系型数据库的主键特性确实能够很好地保证在集群中选举出唯一的一个Master。 但如果当前选举出的 Master 挂了,那么该如何处理?谁来告诉我 Master 挂了呢?显然,关系型数据库无法通知我们这个事件。但ZooKeeper 可以做到! 利用 ZooKeepr 的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即 ZooKeeper 将会保证客户端无法创建一个已经存在的数据单元节点。也就是说,如果同时有多个客户端请求创建同一个临时节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行 Master 选举了。 成功创建该节点的客户端所在的机器就成为了 Master。同时,其他没有成功创建该节点的客户端,都会在该节点上注册一个子节点变更的 Watcher,用于监控当前 Master 机器是否存活,一旦发现当前的Master挂了,那么其他客户端将会重新进行 Master 选举。 这样就实现了 Master 的动态选举。

5. 分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式,分布式锁又分为排他锁和共享锁两种。

排它锁
ZooKeeper如何实现排它锁?

定义锁:ZooKeeper 上的一个 机器节点 可以表示一个锁

获得锁:把ZooKeeper上的一个节点看作是一个锁,获得锁就通过创建临时节点的方式来实现。ZooKeeper 会保证在所有客户端中,最终只有一个客户端能够创建成功,那么就可以认为该客户端获得了锁。同时所有没有获取到锁的客户端就需要到/exclusive_lock节点上注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。

释放锁:因为锁是一个临时节点,释放锁有两种方式
    当前获得锁的客户端机器发生宕机或重启,那么该临时节点就会被删除,释放锁
    正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除,释放锁。

无论在什么情况下移除了lock节点,ZooKeeper 都会通知所有在 /exclusive_lock 节点上注册了节点变更 Watcher 监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复『获取锁』过程。

共享锁
共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用 exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。

总的来说它提供的服务包括:分布式消息同步和协调机制、服务器节点动态上下线、统一配置管理、负载均衡、集群管理,分布式锁。





客户端命令行操作




ZooKeeper 配置使用


ZooKeeper 的使用和 ZooKeeper 集群的搭建并不复杂
创建配置文件
cd /opt/zookeeper/conf
cp zoo_sample.cfg zoo.cfg

vim zoo.cfg
tickTime=2000
dataDir=path_of_zookeeper/data
dataLogDir=path_of_zookeeper/logs
clientPort=2181
initLimit=5
syncLimit=2
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888

#在各个实例上创建其id,server.1和myid文件的1是要能对上的
echo 1 > path_of_zookeeper/data/myid
echo 2 > path_of_zookeeper/data/myid
echo 3 > path_of_zookeeper/data/myid

Default Ports

Port     Description
2181:Client connection port
2888:Quorum port for clustering
3888:Leader election port for clustering

zookeeper的配置文件zoo.cfg里面配置了三个server,实际上其他两个节点没有起来,因此根据zookeeper的选举算法,当整个集群超过半数机器宕机,zookeeper会认为集群处于不可用状态。然后使用zkCli.sh去检测zk状态,就是OK的。三个机器上的zookeeper.properties文件配置相同,需要注意的是日志保存的路径,不会自动生成,需要自己手动建立相关的路径,dataLogDir也是如此,日志文件太多时把日志文件区分开。 创建myid文件,进入zookeeper目录,创建myid文件,将三个服务器上的myid文件分别写入1,2,3。

3个端口的作用
1、2181:对cline端提供服务
2、3888:选举leader使用
3、2888:集群内机器通讯使用(Leader监听此端口)

因为防火墙的作用,三个节点之间2888和3888端口都是互相不通的,因此集群之前没有办法通信,可能就会认为是剩下两个节点都挂掉了。解决之法:重新修改防火墙,打开2888和3888端口或直接关闭防火墙。

操作zookeeper

ZooKeeper中共有9中操作:
create:创建一个znode
delete:删除一个znode
exists:测试一个znode是否存在
set data:向节点中写入数据
getACL,setACL:获取/设置一个znode的ACL(权限控制)
getChildren:获取一个znode的子节点
getData,setData:获取/设置一个znode所保存的数据
sync:将客户端的znode视图与ZooKeeper同步,等待要传播的数据

这里更新数据是必须要提供znode的版本号(也可以使用-1强制更新,这里可以执行前通过exists方法拿到znode的元数据Stat对象,然后从Stat对象中拿到对应的版本号信息),如果版本号不匹配,则更新会失败。因此一个更新失败的客户端可以尝试是否重试或执行其它操作。

(1)启动zookeeper
[zheng@freeoa zk]$ bin/zkServer.sh start

(2)查看进程是否启动
[zheng@freeoa zk]$ jps
3020 Jps
3001 QuorumPeerMain

(3)查看状态:
[zheng@freeoa zk]$ bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Mode: standalone

(4)启动客户端:
[zheng@freeoa zk]$ bin/zkCli.sh

(5)退出客户端:
[zk: localhost:2181(CONNECTED) 0] quit

(6)停止zookeeper
[zheng@freeoa zk]$ bin/zkServer.sh stop


连接与简单的测试
zkCli.sh -server one_zkserv:2181

create /zkm FreeOA
ls /
get /zkm
#
set /zkm FreeOA2020

测试期间可关闭或离线其它的zk服务器,查看一下可否正常取得其值。

# 加入到自启动里
echo '/opt/zookeeper/bin/zkServer.sh start'  >> /etc/rc.local

本文介绍的 Zookeeper 的基本知识,以及介绍了几个典型的应用场景。这些都是 Zookeeper 的基本功能,最重要的是 Zoopkeeper 提供了一套很好的分布式集群管理的机制,就是它这种基于 层次型的目录树的数据结构,并对树中的节点进行有效管理,从而可以设计出多种多样的分布式的数 据管理模型,而不仅仅局限于上面提到的几个常用应用场景。


zookeeper 排错

SASL 认证错误

现象:zookeeper频繁的抛出 {0={error=2019-03-08 13:40:30 318 INFO ... org.apache.zookeeper.ClientCnxn - Opening socket connection to server 172.30.0.3/172.30.0.3:2181. Will not attempt to authenticate using SASL (unknown error)}}  这个错误,但是服务却可以正常使用。

出现原因:zookeeper是一个外部应用,当它放到tomcat、jBoss、netty等容器里边,当有zk client 调用zk server 的时候需要通过容器向系统申请资源,就会默认要使用sasl。如果没有在这套安全模式里面给zookeeper做配置,在调用的时候就会抛出上述错误。

解决思路:通过2的描述,有两种解决思路:一种是给zookeeper做安全配置,一种就是让zookeeper绕过sasl安全机制,直接向系统获取资源。通过这两个思路进行搜索,发现第一种比较复杂,结合我们的业务场景也没有必要添加验证。在针对第二种思路进行搜索,在apache bug管理官网发现有相应的描述:https://issues.apache.org/jira/browse/ZOOKEEPER-1657

通过官网发现,在zookeeper 3.4.5之前,sasl认证是没有办法规避的,在3.4.6版本后修复了这个bug,因为不停的去检测认证虽然对功能没有什么影响,但是比较耗费服务器性能,比较占用容器资源。zookeeper 3.4.6之后,在创建zk连接之前可以通过设置系统参数(zookeeper.client.sasl)为false来禁用sasl认证:zookeeper.client.sasl=false


Zookeeper 实现分布式锁

Zookeeper 是一个分布式协调服务,主要是来解决分布式系统中多个应用之间的数据一致性,其内部的数据存储方式类似于文件目录形式的存储结构。使用场景为业务要求锁的强一致性。

加锁原理:在 Zookeeper 中的指定路径下创建节点,然后客户端根据当前路径下的节点状态来判断是否加锁成功,线程 1 创建节点成功后,线程 2 再去创建节点就会创建失败。

节点类型
持久节点:在 Zookeeper 中创建后会进行持久储存,直到客户端主动删除
临时节点:以客户端会话 Session 维度创建节点,一旦客户端会话断开,节点就会自动删除
临时/持久顺序节点:在同一个路径下创建的节点会对每个节点按创建先后顺序编号

zookeeper.exists("/watchpath",new Watcher() {
    @Override
    public void process(WatchedEvent event) {
    System.out.println("进入监听器");
    System.out.println("监听路径Path:"+event.getPath());
    System.out.println("监听事件类型EventType:"+event.getType());                
    }            
});    

利用临时顺序节点和监听机制来实现分布式锁
实现分布式锁的方式有多种,可以使用临时节点和顺序节点这种方案来实现分布式锁:
1:使用临时节点可以在客户端程序崩溃时自动释放锁,避免死锁问题
2:使用顺序节点的好处是,可以利用锁释放的事件监听机制,来实现阻塞监听式的分布式锁

下面将基于这两个特性来实现分布式锁

加锁原理
1:首先在 Zookeeper 上创建临时顺序节点 Node01、Node02 等
2:第二步客户端拿到加锁路径下所有创建的节点
3:判断自己的序号是否最小,如果最小的话,代表加锁成功,如果不是最小的话,就对前一个节点创建监听器
4:如果前一个节点删除,监听器就会通知客户端来准备重新获取锁。

加锁代码如下所示:
//加锁路径
String lockPath;
//用来阻塞线程
CountDownLatch cc = new CountDownLatch(1);
//创建锁节点的路径
Sting LOCK_ROOT_PATH = "/locks"

//先创建锁
public void createLock(){
    //lockPath = /locks/lock_01
    lockPath = zkClient.create(LOCK_ROOT_PATH+"/lock_", CreateMode.EPHEMERAL_SEQUENTIAL);
}

//获取锁
public boolean acquireLock(){
    //获取当前加锁路径下所有的节点
    allLocks = zkClient.getChildren("/locks");
    //按节点顺序大小排序
    Collections.sort(allLocks);
    //判断自己是否是第一个节点
    int index = allLocks.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));
    //如果是第一个节点,则加锁成功
    if (index == 0) {
        System.out.println(Thread.currentThread().getName() + "获得锁成功, lockPath: " + lockPath);
        return true;
    } else {
        //不是序号最小的节点,则监听前一个节点
        String preLock = allLocks.get(index - 1);
        //创建监听器
        Stat status = zkClient.exists(LOCK_ROOT_PATH + "/" + preLockPath, watcher);
        // 前一个节点不存在了,则重新获取锁
        if (status == null) {
            return acquireLock();
        } else {
            //阻塞当前进程,直到前一个节点释放锁
            System.out.println(" 等待前一个节点锁释放,prelocakPath:"+preLockPath);
            //唤醒当前线程,继续尝试获取锁
            cc.await();
            return acquireLock();
        }
    }
}

private Watcher watcher = new Watcher() {
    @Override
    public void process(WatchedEvent event) {
         //监听到前一个节点释放锁,唤醒当前线程
         cc.countDown();
    }
}

可重入锁实现
Zookeeper 实现可重入分布式锁的机制是在本地维护一个 Map 记录,因为如果在 节点维护数据的话,Zookeeper 的写操作是很慢,集群内部需要进行投票同步数据,所以在本地维护一个 Map 记录来记录当前加锁的次数和加锁状态,在释放锁的时候减少加锁的次数,原理如下所示:
//利用Map记录线程持有的锁
ConcurrentMap<Thread, LockData> lockMap = Maps.newConcurrentMap();
public Boolean lock(){
    Thread currentThread = Thread.currentThread();
    LockData lockData = lockMap.get(currentThread);
    //LockData不为空则说明已经有锁
    if (lockData != null)    
    {
       //加锁次数加一
       lockData.lockCount.increment();
       return true;
    }
    //没有锁则尝试获取锁
    Boolean lockResult = acquireLock();
    //获取到锁
    if (lockResult)
    {
        LockData newLockData = new LockData(currentThread,1);
        lockMap.put(currentThread, newLockData);
        return true;
    }
    //获取锁失败
    return false;
}

解锁原理

解锁的步骤如下:
(1)判断锁是不是自己的
(2)如果是则减少加锁次数
(3)如果加锁次数等于 0,则释放锁,删除掉创建的临时节点,下一个监听这个节点的客户端会感知到节点删除事件,从而重新去获取锁

public Boolean releaseLock(){
    LockData lockData = lockMap.get(currentThread);
    //没有锁
    if(lockData == null){
       return false;
    }
    //有锁则加锁次数减一
    lockCount = lockData.lockCount.decrement();
    if(lockCount > 0){
        return true;
    }
    //加锁次数为0
    try{
        //删除节点
        zkClient.delete(lockPath);
        //断开连接
        zkClient.close();
    finally{
        //删除加锁记录
        lockMap.remove(currentThread);
    }
    return true;
}


本文总结自互联网,感谢众多网友。

参考来源

Zookeeper的功能以及工作原理

ZooKeeper学习总结 第一篇:ZooKeeper快速入门

ZooKeeper学习总结 第二篇:ZooKeeper深入探讨