zookeeper笔记
简介
是一个分布式的协调服务框架,具有管理集群的功能,主要包括:
- ①管理集群中节点的状态(是宕机还是正常工作);
- ②统一集群的配置;
- ③统一集群命名;
- ④管理主备节点;
- ⑤集群中消息的订阅和发布;
- ⑥分布式锁。
特性总结:数据一致性、原子性、可靠性、顺序性、过半性、实时性。
安装:需要jdk环境,解压即可,然后将conf/zoo_sample.cfg文件拷贝或重命名为zoo.cfg;单机模式下就可以直接去bin目录启动了(zkServer.sh start/stop/status/···),并可以在同级目录下的zookeeper.out文件中查看启动日志。
概念
特点:
- zookeeper是一个类似于linux目录的树结构,称为Znode树,每个节点称为Znode节点,根节点为/,所有节点必须以根节点为起点,没有相对路径的概念。
- Znode树在内存和磁盘中都存在,存内存是为了快速的查询,存磁盘是快照的形式,保证数据可靠性。
- 所有客户端连接和对节点的写操作都是事物操作。
- zookeeper理论上可以作为缓存,但实际不用,原因是:①每个节点的数据最大只能1MB;②存储大量数据会降低协调服务效率;③增加节点不能增加容量,只能提高可靠性。
- 临时节点不能有子节点。
基本操作
zookeeper常用客户端命令有:
- ls path:查看path下的所有节点(示例:ls /);
- create /test01 “hello”:在/下创建test01持久节点,数据为“hello”(可以加选项:-e临时节点(客户端退出就会自动删除,这个是核心),-s顺序节点(会自动在节点名称后面拼接上递增的数字));
- set /test01 “world”:修改/test01节点的数据为“world;
- get /test01:获取/test01的节点信息和节点数据;
- rmr /test01:递归删除/test01节点(delete只能删除没有子节点的节点);
get命令查看到的节点信息的具体含义如下:
hello01 # 节点数据
cZxid = 0x5 # 创建节点的事物id
ctime = Mon May 13 09:54:40 CST 2019 # 创建时间
mZxid = 0x9 # 修改节点的事物id
mtime = Mon May 13 10:26:18 CST 2019 # 修改时间
pZxid = 0x5 # 子节点创建/删除的事物id
cversion = 0 # 子节点增删次数
dataVersion = 1 # 数据版本,即当前节点数据的修改次数
aclVersion = 0 # 当前节点的权限改变次数
ephemeralOwner = 0x0 # 临时节点是创建者客户端的会话id,持久节点则是0
dataLength = 7 # 数据长度,即字节数
numChildren = 0 # 子节点数量
zookeeper是一个基于观察者模式设计的分布式服务框架,可以存储和管理相关数据,可以接收观察者的注册监听,一旦这些数据状态发生变化,就会通知注册过的观察者。也就是说,以上的基本操作,都可以在程序中监听节点以及子节点的变化。这也是zookeeper的核心——节点+监听。
应用场景
应用场景及原理:
- 集群管理:有新节点连接时,会去zookeeper创建与客户端绑定的临时节点,并定时维护,集群中其他节点会发现有新增节点,便监听该节点,一旦该节点服务器宕机,则临时节点会删除,集群中其他节点就会收到节点删除的消息。
- 发布订阅:发布方创建节点,订阅方监听节点数据变化事件,这样每次发布方修改数据,订阅方都可以收到修改的数据。
- 分布式锁:分布式集群争抢共享资源时,抢到的服务器会创建顺序(递增)节点,最终会将资源分配给最小的那个节点对应的服务器,其他的释放资源。
集群
在config/zoo.cfg配置文件添加:
# 配置Leader节点不对外提供读操作,可以提升性能。(默认是true)
# leaderServers=false
# 其中server为关键字,紧跟的数字表示第几号服务器,(zookeeper会根据id大小选举主节点)
# 2888是原子广播端口、3888为选举端口,可以任意配置,不冲突就可以
server.1=1.0.0.3:2888:3888
server.2=1.0.0.4:2888:3888
server.3=1.0.0.5:2888:3888
# 配置观察者
# peerType=observer
# server.4=1.0.0.6:2888:3888:observer
另外,还需要在dataDir指定的目录下新建myid文件,内容为一个数字,即为选举id。
zookeeper选举机制:
一阶段:数据恢复阶段,会从本地数据目录中,找到自己所拥有的最大事物id;
二阶段:选举阶段,首先第一轮都推荐自己当leader,并且选举协议中还包含自己最大事物id、自己的选举id(myid文件中的值)、逻辑时钟值(用于判断选举轮次)、服务状态(Looking、Follower、Leader、Observer),若选举失败,则将收到的其他节点的相关信息进行比较,重新推荐比较中胜出的节点成为Leader,这样来回选举几次,直到某一台有过半的票数时就选举成功(若发现集群中已有Leader正常工作,则不再选举);
选举过程中比较顺序和推荐优先级依次为:①集群中已经有leader;②最大事物id越大优先;③选举id越大优先;
为避免脑裂,集群中节点个数不满足过半性的时候,整个集群就停止对外服务,也不进行选举。因此,搭建集群时也要避免节点个数为偶数个,因为偶数个节点在某些情况下集群因网络问题可能对半分为两部分,就有可能产生脑裂。
成功选举后,Leader会做原子广播,确保所有节点数据一致,若客户端对Follower收到事物请求,则会转交给Leader,让Leader做原子广播。(zookeeper集群没有数据分片,只有高可用,不能存海量数据)
2PC算法:即二阶段提交协议,是保证在分布式系统架构下的所有节点在进行事物处理的原子性和一致性的算法。实现原理是:每次事物操作分两个阶段,一阶段是由leader广播,等待所有节点完成事物操作,若全部返回成功再广播提交事物操作,并等待回应,否则广播回滚事物。该算法具有原理简单,实现方便的优点,但是又有同步阻塞、单点故障、脑裂、太保守等缺点。zookeeper没采用。
PAXOS算法:
ZAB协议(Zookeeper Atomic Broadcast):是一种支持崩溃恢复的原子广播协议。基于2PC和PAXOS算法设计。
zookeeper事物操作:
- 集群任意节点收到事物操作都会转交给Leader;
- Leader收到后先生成递增的事物id,接着写到本地硬盘的事物log中,然后广播事物操作;
- Follower收到原子广播后也写到本地硬盘的事物log中,成功后返回yes;
- Leader只要收到过半的yes就提交事物,即从本地事物log中读到内存中,接着广播提交事物;否则广播回滚事物;
- Follower收到后提交事物并返回yes;
- 若此过程中成功提交了事物但是有Leader没有收到部分节点的yes,则认为这部分Follower宕机,下次收到这部分Follower的消息时会同步数据。
崩溃恢复:
- 当leader宕机或者因为网络原因从集群中离开时,则整个集群要选举出一个新的leader;
- 在崩溃恢复中为了防止脑裂,在选出leader后会生成一个全局递增的epochid,并分发给所有follower。
观察者:不参与投票,也不参与过半机制,只监听投票结果,其他功能与Follower类似。适合于异地网络和网络不稳定的情况。
java操作
- 引入依赖:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
- 连接zookeeper:
private ZooKeeper zk;
@Before
public void connect() throws IOException, InterruptedException, KeeperException {
//zookeeper底层使用NIO,是非阻塞连接,所以使用闭锁等待同步
CountDownLatch cdl = new CountDownLatch(1);
zk = new ZooKeeper("1.0.0.3:2181", 1000, new Watcher() {
//监听事件
@Override
public void process(WatchedEvent event) {
if(event.getState().equals(KeeperState.SyncConnected)){
cdl.countDown();
System.out.println("连接成功!");
}
}
});
//等待连接成功
cdl.await();
//zk.close();
}
- 获取节点数据,并监听数据修改
@Test
public void get() throws KeeperException, InterruptedException {
CountDownLatch cdl = new CountDownLatch(1);
byte[] b = zk.getData("/java01", new Watcher() {
@Override
public void process(WatchedEvent event) {
if(event.getType().equals(Event.EventType.NodeDataChanged)){
try {
byte[] b = zk.getData("/java01", null, null);
System.out.println("节点数据被修改为:" + new String(b));
cdl.countDown();
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
}
}, null);
System.out.println("节点当前数据:" + new String(b));
cdl.await();
}