一、zk简介
ZooKeeper是java写的一个开源的分布式协调服务框架,一主多从架构,理想情况下,每个节点数据一致,数据存储模型为树型结构,节点znode可存放数据,数据存放在内存和磁盘中。通过对结点znode状态管理,可用于其它分布式组件的服务注册和发现,参与组件的管理,分布式框架比如kafka,hadoop等。zookeeper的锁机制,保证写安全和读高性能平衡,高并发情况下保证数据安全。
二、zk框架应用场景
- 分布式存储,基于内存,数据磁盘持久化,保证高可用和高性能以及数据安全。
- 数据一致性,在保证高并发的同时,zk客户端的修改可以对其它客户端来说有一致性
- Watch机制,通过对znode节点状态监控,对注册的服务节点状态进行管理。
- 分布式锁,读锁和写锁,可以保证读的高性能的同时保证并罚下数据修改的安全
- ZAB协议数据广播和选举机制,保证服务崩溃恢复和主从数据同步问题。
三、zk集群框架介绍
1.架构图
一主多从的架构,保证高可用和数据安全
2. 集群心跳机制
监控集群节点状态,并把状态同步给子节点,leader挂掉后,从节点发起选举机制
3.集群选举机制
3.1 ZAB协议四种节点状态:
- Looking:选举状态
- Following:follower节点所处的状态
- Leading:leader节点所处的状态
- Observing:观察者节点所处的状态
3.2 集群启动时选举
启动时都是LOOKING状态,有两台开始发起选举,分两轮投票,第一轮两个节点互相投自己生成的票(myid/zxid),分别选出最大的票投到投票箱,之后更新自己的选票为投票箱最大的选票。
第二轮投票,将自己最大选票投给彼此,得到选票后再投到箱内,更新最新的选票号。
当超过一半的节点参与选举后,得到的节点作为主节点leader。
3.3 集群恢复时选举
利用心跳机制,监测到leader节点挂点,follower节点状态变为Looking选举状态,发起选举。
4.zk内部数据保存和操作
4.1 数据模型,树形结构
- 每个节点称为znode,znode可以存放数据
- znode节点分为持久化节点和临时节点(和client会话生命周期一样,同时会生成sessionid)
- znode节点,利用watch机制,可设立监听,管理节点以及子节点状态信息
- 在高并发时,提供序列节点,保证服务可用和安全,分布式锁也会用到
- znode节点满足zk分布式锁机制,客户端可以数据同步安全
- znode节点有acl权限机制,保证数据安全
- znode节点delete删除提供乐观锁删除,类似cas算法
4.2 内部nio和bio机制
zk集群服务端连接client后,会建立tcp长连接,client会定时发送心跳上报给服务端,同时服务端利用nio来管理多个client的channel;同时client建立的监听,可以对多个znode监听,也是利用nio机制。bio是服务节点间内部选举的方式。
5.集群数据同步
5.1 写入事务
客户端写入数据时,如果连接的时follower,则follower会将写请求转发给leader节点处理,leader节点会生成事务编号,写入数据到本地数据文件,返回自己ack,并发送给follower节点,follower节点写入本地文件成功后会返回ack给leader,leader收集ack并过半数节点写入成功,向各节点和自己发送commit命令,节点将数据刷新到内存中。
5.2 高并发下写事务,分布式锁
- 读锁,客户端都可以读,上读锁的前提是,之前没有上写锁。
- 写锁,拿到写锁才可以写,之前没有任何锁。
5.2.1 羊群效应
读锁和写锁,在节点释放锁后,所有锁会激活,争抢节点使用权,资源消耗出现峰值,可能会导致集群雪崩。
5.2.2 链式监听
为了防止羊群效应,使用链式监听,后一个锁对象监听上一个锁对象,如果当前是读锁,上一个读锁已使用,则可获取;如果当前是写锁,则上一个锁对象被释放后才能使用,后面所有对象锁都不能使用。
6.集群搭建
leader | follower | follower | |
---|---|---|---|
ip | 192.168.47.128 | 192.168.47.129 | 192.168.47.130 |
client_port | 2181 | 2182 | 2183 |
server_data_port | 2001 | 2002 | 2003 |
server_elect_port | 3001 | 3002 | 3003 |
myid | 1 | 2 | 3 |
6.1 准备环境
搭建三台虚拟机,安装好jdk环境,下载apache-zookeeper-3.8.3-bin.tar.gz并解压。
6.2 修改配置文件
- 修改 vi /etc/profile source /etc/profile
- 修改zk包下conf/zoo.cfg文件
- 在/usr/local/zks/zkdata下新建myid文件,每个节点从1开始编号
6.3 启动集群
- 启动三台虚拟机节点的zk,zkServer.sh start ../conf/zoo.cfg
- 观察节点状态
6.4 新建客户端,连接zk集群
7. java客户端
7.1 依赖
7.2 配置zk客户端
#### ZK ####
curator.retryCount=5
curator.elapsedTimeMs=5000
curator.server=192.168.47.128:2181,192.168.47.129:2182,192.168.47.130:2183
curator.sessionTimeoutMs=600000
curator.connectTimeoutMs=5000
package com.spring.zkkafka.conf;
import lombok.Data;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(ConfigZK.ZkProperties.class)
public class ConfigZK {
@Bean(initMethod = "start")
CuratorFramework curatorFramework(ZkProperties properties) {
ZkProperties zkProperties = new ZkProperties();
return CuratorFrameworkFactory.builder()
.connectString(properties.getServer())
.connectionTimeoutMs(properties.getConnectTimeoutMs())
.sessionTimeoutMs(properties.getSessionTimeoutMs())
.retryPolicy(new RetryNTimes(properties.getRetryCount(), properties.getElapsedTimeMs()))
.build();
}
@ConfigurationProperties(prefix = "curator")
@Data
class ZkProperties {
private int retryCount;
private int elapsedTimeMs;
private String server;
private int sessionTimeoutMs;
private int connectTimeoutMs;
}
}
7.3 java客户端用例
package com.spring.zkkafka;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock;
import org.apache.zookeeper.CreateMode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
@Slf4j
public class zkClientTest {
@Autowired
private CuratorFramework curator;
@Test
public void createNode() throws Exception {
// 持久节点
// String res = curator.create().forPath("/client1", "abc".getBytes());
String res = curator.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath("/per", "abcd".getBytes());
// 临时节点(和会话生命周期一样)
// String res = curator.create().withMode(CreateMode.EPHEMERAL).forPath("/tempSeq", "abc".getBytes());
System.out.printf("create node : %s successful.%n", res);
System.in.read();
}
@Test
public void getNodeData() throws Exception {
byte[] bytes = curator.getData().forPath("/per0000000014");
System.out.printf("get node data : %s successful.%n", new String(bytes));
}
@Test
public void createNodeWithParent() throws Exception {
String res = curator.create().creatingParentsIfNeeded().forPath("/ttt/per1", "aaaaa".getBytes());
System.out.printf("create node : %s successful.%n", res);
}
@Test
public void deleteNodeWithParent() throws Exception {
curator.delete().guaranteed().deletingChildrenIfNeeded().forPath("/ttt");
System.out.printf("delete node : %s successful.%n", "/ttt");
}
@Test
public void addNodeListener() throws IOException {
CuratorCache curatorCache = CuratorCache.builder(curator, "/test1").build();
curatorCache.listenable().addListener(CuratorCacheListener.builder().forCreates((d) -> {
System.out.printf("create operation, data : %s%n", d.getData() == null ? null : new String(d.getData()));
}).forChanges((ori_d, cur_d) -> {
System.out.printf("data change operation, ori_data : %s, cur_data : %s%n",
ori_d.getData() == null ? null : new String(ori_d.getData()), new String(cur_d.getData()));
}).forDeletes((d) -> {
System.out.printf("delete node operation : %s%n", d.getPath());
}).build());
curatorCache.start();
System.in.read(); // 主线程不关闭
}
@Test
public void getReadLock() throws Exception {
InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(curator, "/rwlock");
InterProcessMutex readLock = readWriteLock.readLock(); // 非阻塞
System.out.println("等待获取锁");
readLock.acquire(); // 阻塞直到获取锁
System.out.println("获取到锁");
for (int i = 0; i < 100; i++) {
System.out.println(i);
Thread.sleep(3000);
}
readLock.release(); // 释放
System.out.println("释放锁");
}
@Test
public void getReadLock1() throws Exception {
InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(curator, "/rwlock");
InterProcessMutex readLock = readWriteLock.readLock(); // 非阻塞
System.out.println("等待获取锁");
readLock.acquire(); // 阻塞直到获取锁
System.out.println("获取到锁");
for (int i = 0; i < 100; i++) {
System.out.println(i);
Thread.sleep(3000);
}
readLock.release(); // 释放
System.out.println("释放锁");
}
@Test
public void getWriteLock() throws Exception {
InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(curator, "/rwlock");
InterProcessMutex writeLock = readWriteLock.writeLock(); // 非阻塞
System.out.println("等待获取锁");
writeLock.acquire(); // 阻塞直到获取锁
System.out.println("获取到锁");
for (int i = 0; i < 100; i++) {
System.out.println(i);
Thread.sleep(3000);
}
writeLock.release(); // 释放
System.out.println("释放锁");
}
}