目录
3.6.1 创建 ZooKeeper 客户端、创建子节点、获取子节点并监听节点
一、Zookeeper入门
1.1 概述
Zookeeper是一个开源的分布式的,伪分布式框架提供协调服务的Apache项目。ZooKeeper是一个高可用的分布式数据管理和协调框架,并且能够很好的保证分布式环境中数据的一致性。 在越来越多的分布式系统(Hadoop、HBase、Kafka)中,Zookeeper都作为核心组件使用。
1.2 工作机制
是基于观察者模式设计的分布式服务管理框架,负责储存和管理大家都关心的数据,然后接收观察者的注册,一旦数据状态变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。
Zookeeper = 文件系统 + 通知机制
1.3 Zookeeper架构
1.4 特点
1)Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。
2)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所 以Zookeeper适合安装奇数台服务器。
3)全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
4)更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。
5)数据更新原子性,一次数据更新要么成功,要么失败。
6)实时性,在一定时间范围内,Client能读到最新数据。
1.5 数据结构
ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每一个ZNode默认能够存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识。
1.6 应用场景
1)统一命名管理:对应用/服务进行统一命名,便于识别。
2)统一配置管理:所有节点的配置信息是一致的。配置文件修改后,能快速同步到各个节点上。
3)统一集群管理:实时监控节点变化。
4)服务器动态上下线:客户端能实时洞察到服务器上下线的变化。
5)软负载均衡:Zookeeper中会记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端需求。
下面选取两个案例进行演示:(1)ZooKeeper在Hadoop集群中的应用 (2)分布式锁
1.6.1 ZooKeeper在Hadoop集群中的应用
Master选举 用到了Zookeeper什么原理?
强一致性 :能够保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即ZooKeeper将会保证客户端无法创建一个已经存在的ZNode。 也就是说,如果同时有多个客户端请求创建同一个临时节点,那么最终一定只有一个客户端请求能够创建成功。 利用这个特性,就能很容易地在分布式环境中进行Master选举了。
1.6.2 分布式锁
二、Zookeeper配置参数解读
端口说明:
- 2181:对Client端提供服务
- 2888:集群内机器通讯使用(只有Leader监听此端口)
- 3888:重新选举Leader使用(每个Zookeeper都监听此端口)
配置项说明:
- tickTime=2000:Client与Server通信心跳时间
- initLimit=10:Leader与Follower初始通信时限
- syncLimit=5:Leader与Follower同步通信时限
三、Zookeeper集群操作
3.1 Zookeeper选举机制
3.1.1 Zookeeper选举机制——第一次启动
3.1.2 Zookeeper选举机制——非第一次启动
3.2 客户端命令行操作
- ctime :znode被创建的毫秒数(从 1970年开始)
- mzxid :znode最后更新的事务 zxid
- mtime :znode最后修改的毫秒数(从 1970年开始)
- pZxid :znode最后更新的子节点 zxid
- cversion:znode 子节点变化号,znode 子节点修改次数
- dataversion:znode 数据变化号
- aclVersion:znode 访问控制列表的变化号
- ephemeralOwner:如果是临时节点,这个是znode 拥有者的session id。如果不是,临时节点则是0。
- dataLength:znode 的数据长度
- numChildren:znode 子节点数量
3.3 节点类型
(1)持久节点(Persistent Node):如果不主动移除它,它将一直存在
create /sanguo "shuguo"
(2)临时节点(Ephemeral Node):一旦客户端断开连接,就被移除
create -e /sanguo "wuguo"
(3)顺序节点(Sequential Node):顺序自动编号的节点,节点会根据当前已存在的节点数自动加 1
create -s /sanguo "shuguo"
3.4 监听器原理
3.4.1 监听器原理
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。
3.4.2 事件监听器
客户端可以在节点上设置监视器(watches)。当节点的状态发生改变时(数据的增、删、改等操作)将会触发watch对应的操作。当watch被触发时,ZooKeeper将会向客户端发送且发送一个通知,因为watch只能触发一次。
演示:
set /sanguo "wei,shu,wu"
3.5 客户端API
3.5.1 IDEA环境
3.5.2 创建Maven工程
3.5.3 添加pom文件
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</version>
</dependency>
</dependencies>
3.5.4 拷贝log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c]- %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c]- %m%n
3.5.5 创建包名com.git.zk
3.5.6 创建类名zkClient
3.6 创建Zookeeper客户端
3.6.1 创建 ZooKeeper 客户端、创建子节点、获取子节点并监听节点
public class zkClient {
//注意,逗号左右都不能有空格
private String connectString ="hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout=2000;
private ZooKeeper zkClient;
@Before
public void init() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("===============================================");
List<String> children = null;
try {
children = zkClient.getChildren("/", true);
for(String child:children){
System.out.println(child);
}
System.out.println("----------------------------------------");
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//创建子节点
@Test
public void create() throws InterruptedException, KeeperException {
String nodeCreated = zkClient.create("/git", "rqz".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//监听节点变化
@Test
public void getChildren() throws InterruptedException, KeeperException {
List<String> children = zkClient.getChildren("/", true);
for(String child:children){
System.out.println(child);
}
//延时
Thread.sleep(Long.MAX_VALUE);
}
}
在zkCli.sh中进行删除,添加,都会在控制台上显示变化
===============================================
git2
git3
git
zookeeper
sanguo
kafka
hbase
----------------------------------------
===============================================
git3
git
zookeeper
sanguo
kafka
hbase
----------------------------------------
3.6.2 判断 Znode 是否存在
//判断 Znode 是否存在
@Test
public void exist() throws InterruptedException, KeeperException {
Stat stat = zkClient.exists("/git", false);
System.out.println(stat==null?"not exist":"exist");//exist
}
3.7 客户端向服务端写数据流程
3.7.1 写流程之写入请求直接发送给Leader节点
3.7.2 写流程之写入请求发送给follower节点
四、服务器动态上下线监听案例
4.1 需求
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。
4.2 需求分析
4.3 具体实现
4.3.1 服务器注册
public class DistributeServer {
private String connectString="hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout=2000;//延长时间
private ZooKeeper zk;
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
DistributeServer server = new DistributeServer();
//1.获取zk连接
server.getConnect();
//2.注册服务器到zk集群
server.regist(args[0]);
//3.启动业务逻辑(睡觉) 不然进程一下就结束了
server.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
private void regist(String hostname) throws InterruptedException, KeeperException {
String s = zk.create("/servers/"+hostname, hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname+" is online");
}
public void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
}
}
4.3.2 客户端监听
public class DistributeClient {
private ZooKeeper zk;
private String connectString="hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout=2000;//延长时间
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
DistributeClient client = new DistributeClient();
//1.获取zk连接
client.getConnect();
//2.监听/servers下面的节点增加和删除
client.getServerList();
//3.业务逻辑(睡觉)
client.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
private void getServerList() throws InterruptedException, KeeperException {
List<String> children = zk.getChildren("/servers", true);
ArrayList<String> servers = new ArrayList<>();
for(String child:children){
byte[] data = zk.getData("/servers/" + child, false, null);
servers.add(new String(data));
}
//打印
System.out.println(servers);
}
public void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//时刻监听
try {
getServerList();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
});
}
}
4.3.3 结果展示
五、Zookeeper分布式锁案例
什么叫做分布式锁呢? 比如说"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。
分布式锁案例分析
5.1 原生Zookeeper实现分布式锁案例
public class DistributeLock {
private final String connectString="hadoop102:2181,hadoop103:2181:hadoop104:2181";
private final int sessionTimeout=2000;
private final ZooKeeper zk;
private CountDownLatch connectLatch =new CountDownLatch(1);
private CountDownLatch waitLatch = new CountDownLatch(1);
private String waitPath;
private String currentMode;
public DistributeLock() throws IOException, InterruptedException, KeeperException {
//1.获取zk连接
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//connectLatch 如果连接上zk,可以释放
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
connectLatch.countDown();
}
//waitLatch 需要释放
if(watchedEvent.getType()==Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){
waitLatch.countDown();
}
}
});
//等待zk正常连接后,才往下走程序
connectLatch.await();
//2.判断根节点/locks是否存在
Stat stat = zk.exists("/locks", false);
if(stat==null){
//创建根节点
zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
//对zk加锁
public void zkLock(){
// 创建对应的临时带序号节点
try {
currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//wait一小会,结果会更清晰一些
Thread.sleep(10);
// 判断创建的节点是否是最小的序号节点,如果是就获取到锁;如果不是就监听他序号前一个节点
List<String> children = zk.getChildren("/locks", false);
//如果children只有一个值,那就直接获取锁;如果有多个值,需要判断,谁最小
if(children.size()==1){
return;
}else{
//先进行排序
Collections.sort(children);
//获取节点名称 seq-00000000,substring是将locks进行截取掉,只要剩下的seq-00000000XX
String thisNode = currentMode.substring("/locks/".length());
//通过seq-00000000获取该节点在children集合的位置
int index = children.indexOf(thisNode);
//判断
if(index==-1){
System.out.println("数据异常");
}else if(index==0){
//只有一个节点,可以获取锁了
return;
}else{
//有多个节点,此时需要进行监听了,监听前一个节点
waitPath="/locks/"+children.get(index-1);
zk.getData(waitPath,true,new Stat());
//等待前一个监听完毕之后,在执行
waitLatch.await();
return;
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//对zk解锁
public void unZkLock(){
// 删除节点
try {
zk.delete(currentMode,-1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
public class DistributedLockTest {
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
final DistributeLock lock1 = new DistributeLock();
final DistributeLock lock2 = new DistributeLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock1.zkLock();
System.out.println("线程1 启动,获取到锁");
Thread.sleep(5*1000);
lock1.unZkLock();
System.out.println("线程1 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock2.zkLock();
System.out.println("线程2 启动,获取到锁");
Thread.sleep(5*1000);
lock2.unZkLock();
System.out.println("线程2 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
5.1.1 运行结果
5.2 Curator 框架实现分布式锁案例
1)原生的 Java API 开发存在的问题
- 会话连接是异步的,需要自己去处理。比如使用CountDownLatch
- Watch 需要重复注册,不然就不能生效
- 开发的复杂性还是比较高的
- 不支持多节点删除和创建。需要自己去递归
2)Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。
详情请查看官方文档:https://curator.apache.org/index.html
3)Curator 案例实操
public class CuratorLockTest {
public static void main(String[] args) {
// 创建分布式锁1
InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
// 创建分布式锁2
InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");
new Thread(new Runnable() {
@Override
public void run() {
try {
lock1.acquire();
System.out.println("线程1 获取到锁");
lock1.acquire();
System.out.println("线程1 再次获取到锁");
Thread.sleep(5*1000);
lock1.release();
System.out.println("线程1 释放锁");
lock1.release();
System.out.println("线程1 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock2.acquire();
System.out.println("线程2 获取到锁");
lock2.acquire();
System.out.println("线程2 再次获取到锁");
Thread.sleep(5*1000);
lock2.release();
System.out.println("线程2 释放锁");
lock2.release();
System.out.println("线程2 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private static CuratorFramework getCuratorFramework() {
//重试策略,初试时间 3 秒,重试 3 次
ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);
//通过工厂创建 Curator
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("hadoop102:2181,hadoop103:2181,hadoop104:2181")
.connectionTimeoutMs(2000)
.sessionTimeoutMs(2000)
.retryPolicy(policy).build();
//启动客户端
client.start();
System.out.println("zookeeper 启动成功");
return client;
}
}
5.2.1 运行结果
六、企业面试真题
6.1 选举机制
半数机制,超过半数的投票通过,即通过。
(1)第一次启动选举规则:投票过半数时,服务器 id 大的胜出
(2)第二次启动选举规则:
①EPOCH 大的直接胜出
②EPOCH 相同,事务 id 大的胜出
③事务 id 相同,服务器 id 大的胜出
6.2 生产集群安装多少zk合适
- 安装奇数台。
- 生产经验:10 台服务器:3 台 zk;20 台服务器:5 台 zk;100 台服务器:11 台 zk;200 台服务器:11 台 zk。
- 服务器台数多:好处,提高可靠性;坏处:提高通信延时
6.3 常用命令
ls、get、create、delete
七、Paxos算法和Zab算法
7.1 Paxos算法
7.1.1 Paxos算法背景
首先来了解一下Paxos算法,从发生在Paxos小岛上的故事说起。
一个叫做 Paxos 的小岛(Island)上面住了一批居民,岛上面所有的事情由一些特殊的人决定,他们叫做议员(Senator)。
议员的总数(SenatorCount)是确定的且不能更改。
岛上每次环境事务的变更都需要通过一个提议(Proposal),每个提议都有一个编号(PID),这个编号是一直增长的,不能倒退。
每个提议都需要超过半数的议员同意才能生效。
每个议员只会同意大于当前编号的提议,包括已生效的和未生效的。
如果议员收到小于等于当前编号的提议,他会拒绝,并告知对方:你的提议已经有人提过了。这里的当前编号是每个议员在自己记事本上面记录的编号,他不断更新这个编号。整个议会不能保证所有议员记事本上的编号总是相同的。
现在议会有一个目标是保证所有的议员对于提议都能达成一致的看法。
7.1.2 Paxos算法举例
7.1.3 Paxos算法弊端
7.2 Zab算法
7.2.1 Zab算法举例
7.2.2 Zab算法实际上是对Paxos算法的改进