ZooKeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然 后接受观察者的注册,一旦这些数据的状态发生变化,ZooKeeper就负责通知已经在ZooKeeper上注册的那些观察者做出相应的反应。
特点:
应用场景:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等
数据结构:ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识。
官网下载:https://ZooKeeper.apache.org/releases.html
tar -zxvf apache-ZooKeeper-3.8.3-bin.tar.gz -C /opt/
mv apache-ZooKeeper-3.8.3-bin apache-ZooKeeper-3.8.3
conf
目录,复制 cp zoo_sample.cfg zoo.cfg
并修改配置文件 vim zoo.cfg
tickTime = 2000
:通信心跳时间,ZooKeeper服务器与客户端心跳时间,单位毫秒initLimit = 10
:LF初始通信时限次数,Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)syncLimit = 5
:LF同步通信时限次数,Leader和Follower之间通信时间如果超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除FollwerdataDir
:保存ZooKeeper中的数据,默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录clientPort = 2181
:客户端连接端口,通常不做修改dataDir=/opt/apache-ZooKeeper-3.8.3/zkData
/opt/apache-ZooKeeper-3.8.3
目录,创建目录 mkdir zkData
操作 ZooKeeper:
bin/zkServer.sh start|stop|status
bin/zkCli.sh
;退出 quit
按单机版方式先在三台机器上分别部署单机版(本地测试请先关闭防火墙,保证网络通畅能互相 ping 通),接下来做以下操作。
zkData
目录 vim myid
,文件内容写 1
即可,后续添加机器需填写不同编号zoo.cfg
配置文件,加上如下配置
server.A=B:C:D
集群服务配置详析
#######################cluster##########################
server.1=zkServer1:2888:3888
server.2=zkServer2:2888:3888
server.3=zkServer3:2888:3888
vim /etc/hosts
修改映射文件添加如下映射,也可不配置映射直接使用地址,但不推荐192.168.115.129 zkServer1
192.168.115.131 zkServer2
192.168.115.132 zkServer3
myid
即可统一启停脚本:ZooKeeper.sh
,创建该脚本后需修改脚本权限具备可执行权限 chmod 777
!/bin/bash
echo ---------- ZooKeeper $1 执行 ------------
for i in 192.168.115.129 192.168.115.131 192.168.115.132
do
echo ---------- $i $1 ------------
ssh $i "/opt/apache-ZooKeeper-3.8.3/bin/zkServer.sh $1"
done
之后执行 ./ZooKeeper.sh start
即可一键启动集群 ZooKeeper,也可将 start
换成 stop
或 status
做其他操作
使用 status 查看状态存在 mode :Mode: follower
和 Mode: leader
表示启动成功,否则可以进安装目录的 logs
目录看日志
上面脚本用到了 ssh 登陆,一般需要配置 ssh 免密登陆,步骤如下:
ssh-keygen -t rsa
:在创建脚本的机器上执行该命令生成密钥ssh-copy-id [email protected]
:上传1中生成的密钥到服务器 192.168.115.131
ssh [email protected] "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
:确保远程服务器上目录及文件权限正确,该命令执行时无需密码则表示已经配置成功如何确定应该安装多少 ZooKeeper?
经验:首先必然是奇数台,其次:
服务器台数多:好处,提高可靠性;坏处:提高通信延时
ZooKeeper 的选举机制分为两种,一种是首次启动时进行 leader 选举,另一种是非首次启动选举。首先需清楚,ZooKeeper 服务器有以下属性。
假设有 5 台 ZooKeeper 机器,myid 分别设置为 1~5,下面将详细描述首次启动时的选举情况:
即 投票过半数时,服务器 id 大的胜出
当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举
当一台机器进入Leader选举流程时,当前集群可能会处于以下两种状态:
假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻,
3和5服务器出现故障,因此开始进行Leader选举。
SID为1、2、4的机器情况(EPOCH,ZXID,SID): (1,8,1) (1,8,2) (1,7,4)
按照选举规则,机器2将获得票数(3)过半,机器2将当选 Leader 。如此时机器3、5恢复,则会按照集群中本来就存在 Leader 的情况进行,即都变为 follower
连接客户端:bin/zkCli.sh -server zkServer1:2181
不加 -server 默认连接本机
命令基本语法 | 功能描述 |
---|---|
ls [-w] [-s] path | 查看当前 znode 的子节点,-w 监听子节点变化,-s 附加次级信息 |
create [-e] [-s] path value | 普通创建,-s 含有序列,-e 临时(重启或者超时消失) |
get [-w] [-s] path | 获得节点的值,-w 监听节点内容变化,-s 附加次级信息 |
set path value | 设置节点的具体值 |
stat | 相当于上面加 -s 参数 |
delete | 删除节点 |
deleteall | 递归删除节点 |
ls path
:示例 ls /
,可使用 -w
监听子节点变化或 -s
查看附加次级信息[zk: localhost:2181(CONNECTED) 8] ls -s /
[ZooKeeper]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
节点类型分为持久和短暂
节点类型还可分为有序号和无序号。创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
创建命令如下,-s
含有序列,-e
临时,创建对应节点时哪怕不使用 -s
参数,底层也会占用顺序号,且同一连接,不同节点的顺序号也是共用的
create [-e] [-s] path value
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。
监听语法:下面监听均只会触发一次,想要多次触发就要触发后再监听
ls -w path
:监听节点路径变化情况,不含子层,如 ls -w /a
可监听到 /a
本身及最近子结点 /a/b
或 /a/c
的变化,但不能监听到 /a/b/d
的变化,且监听事件只会触发一次get -w path
:监听节点的值的变化,监听事件只会触发一次原理:
如写入请求发送给 Leader:
如写入请求发送给 Follower:
<dependency>
<groupId>org.apache.ZooKeepergroupId>
<artifactId>ZooKeeperartifactId>
<version>3.8.3version>
dependency>
logback.xml
到类资源目录 resources
下,此步十分必要,不然连接集群会一直打 debug 日志<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %npattern>
encoder>
appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
root>
configuration>
public class DistributeServer {
public final static String ZK_SER = "192.168.115.129:2181,192.168.115.131:2181,192.168.115.132:2181";
public final static int SESSION_TIME_OUT = 2000;
public final static String ROOT_NODE = "/servers";
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
ZooKeeper ZooKeeper = getZooKeeper();
Scanner sc = new Scanner(System.in);
do {
System.out.print("请输入要注册的服务名:");
String temp = sc.nextLine();
System.out.println();
if ("stop".equals(temp)) {
break;
}
registerServer(ZooKeeper, temp);
} while (true);
}
public static ZooKeeper getZooKeeper() throws IOException, InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper ZooKeeper = new ZooKeeper(ZK_SER, SESSION_TIME_OUT, watchedEvent -> {
System.out.println("连接成功会执行一次" + watchedEvent.getType());
countDownLatch.countDown();
});
countDownLatch.await();
return ZooKeeper;
}
public static void registerServer(ZooKeeper ZooKeeper, String serverName) throws InterruptedException, KeeperException {
Stat exists = ZooKeeper.exists(ROOT_NODE, false);
if (exists == null) {
String res = ZooKeeper.create(ROOT_NODE, "Distribute服务".getBytes(StandardCharsets.UTF_8),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);// 持久节点
System.out.println("创建持久节点响应:" + res);
}
String res = ZooKeeper.create(ROOT_NODE + "/" + serverName, serverName.getBytes(StandardCharsets.UTF_8),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 临时顺序节点
System.out.println("创建临时节点响应:" + res);
}
}
public class DistributeClient {
private static ZooKeeper ZooKeeper = null;
public static ZooKeeper getZooKeeper() throws IOException, InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper = new ZooKeeper(DistributeServer.ZK_SER, DistributeServer.SESSION_TIME_OUT, watchedEvent -> {
countDownLatch.countDown();
System.out.println("监听事件变化" + watchedEvent);
// NodeChildrenChanged(4)
if (watchedEvent.getType().getIntValue() == 4) {
try {
// 再次监听,达到重复监听的效果
ZooKeeper.getChildren(DistributeServer.ROOT_NODE, true);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
// 等待完全连接上
countDownLatch.await();
return ZooKeeper;
}
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
ZooKeeper ZooKeeper = getZooKeeper();
// 监听所有节点变化,只会触发一次
List<String> children = ZooKeeper.getChildren(DistributeServer.ROOT_NODE, true);
System.out.println("监听节点:" + children);
System.in.read();
}
}
分布式锁在 redis 篇章已经描述够详细,故这里编码只是简易实现一个分布式锁,不考虑原子性,不考虑重入,也不继承 Locks
接口,方便理解基于 ZooKeeper 分布式锁的基础实现算法。算法流程如图:
public class DistributeLock {
public final static String ZK_SER = "192.168.115.129:2181,192.168.115.131:2181,192.168.115.132:2181";
public final static int SESSION_TIME_OUT = 2000;
public final static String ROOT_NODE = "/locks";
private static ZooKeeper ZooKeeper = null;
private static final CountDownLatch INIT_LATCH = new CountDownLatch(1);
static {
try {
System.out.println("ZooKeeper 开始连接");
ZooKeeper = new ZooKeeper(ZK_SER, SESSION_TIME_OUT, watchedEvent -> {
System.out.println("连接 ZooKeeper成功");
INIT_LATCH.countDown();
});
Stat exists = ZooKeeper.exists(ROOT_NODE, false);
if (exists == null) {
String res = ZooKeeper.create(ROOT_NODE, "DistributeLock".getBytes(StandardCharsets.UTF_8),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);// 持久节点
System.out.println("初始化锁节点成功:" + res);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private String lock;
public DistributeLock(String lock) {
this.lock = lock;
try {
INIT_LATCH.await();
} catch (Exception e) {
e.printStackTrace();
}
}
private String currentNode;
public synchronized void lock() throws InterruptedException, KeeperException {
currentNode = ZooKeeper.create(ROOT_NODE + "/" + lock, null, // /locks/lock0000000014 | /locks/lock0000000015
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 临时顺序节点
List<String> children = ZooKeeper.getChildren(ROOT_NODE, false);
// 过滤 + 排序 = [lock0000000014, lock0000000015]
List<String> lockList = children.stream().filter(item -> item.startsWith(lock)).sorted().toList();
// 当前节点不是最小节点则监听并阻塞
if (!currentNode.substring(currentNode.lastIndexOf("/") + 1).equals(lockList.get(0))){
CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper.getChildren(ROOT_NODE + "/" + lockList.get(0), event -> {
if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
countDownLatch.countDown();
}
});
countDownLatch.await();
}
// 是最小节点则返回直接执行
}
public synchronized void unlock() throws InterruptedException, KeeperException {
ZooKeeper.delete(currentNode, -1); // 删除临时节点
}
}
测试代码:
public static void main(String[] args) throws Exception {
DistributeLock lock1 = new DistributeLock("lock");
DistributeLock lock2 = new DistributeLock("lock");
new Thread(() -> {
try {
lock1.lock();
System.out.println("线程1 抢到锁");
TimeUnit.SECONDS.sleep(3);
System.out.println("线程1 即将解锁");
lock1.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
lock2.lock();
System.out.println("线程2 抢到锁");
TimeUnit.SECONDS.sleep(3);
System.out.println("线程2 即将解锁");
lock2.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
输出:
线程2 抢到锁
线程2 即将解锁
线程1 抢到锁
线程1 即将解锁
Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题
官网:https://curator.apache.org/docs/about
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>4.3.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>4.3.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-clientartifactId>
<version>4.3.0version>
dependency>
public class MainMaven {
private final static String ROOT_NODE = "/locks";
public final static String ZK_SER = "192.168.115.129:2181,192.168.115.131:2181,192.168.115.132:2181";
public final static int SESSION_TIME_OUT = 2000;
public final static int CONNECT_TIME_OUT = 2000;
public static void main(String[] args) {
final InterProcessLock lock1 = new InterProcessMutex(getCuratorFramework(), ROOT_NODE);
final InterProcessLock lock2 = new InterProcessMutex(getCuratorFramework(), ROOT_NODE);
new Thread(() -> {
try {
lock1.acquire();
System.out.println("线程1 抢到锁");
TimeUnit.SECONDS.sleep(3);
System.out.println("线程1 即将解锁");
lock1.release();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
lock2.acquire();
System.out.println("线程2 抢到锁");
TimeUnit.SECONDS.sleep(3);
System.out.println("线程2 即将解锁");
lock2.release();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
// 分布式锁初始化
public static CuratorFramework getCuratorFramework(){
//重试策略,初试时间 3 秒,重试 3 次
RetryPolicy policy = new ExponentialBackoffRetry(3000, 3);
//通过工厂创建 Curator
CuratorFramework client =
CuratorFrameworkFactory.builder()
.connectString(ZK_SER)
.connectionTimeoutMs(CONNECT_TIME_OUT)
.sessionTimeoutMs(SESSION_TIME_OUT)
.retryPolicy(policy).build();
//开启连接
client.start();
System.out.println("ZooKeeper 初始化完成...");
return client;
}
}
输出:
线程2 抢到锁
线程2 即将解锁
线程1 抢到锁
线程1 即将解锁
Paxos算法:一种基于消息传递且具有高度容错特性的一致性算法。
Paxos算法解决的问题:就是如何快速正确的在一个分布式系统中对某个数据值达成一致,并且保证不论发生任何异常,都不会破坏整个系统的一致性。
算法描述:在一个Paxos系统中,首先将所有节点划分为Proposer(提议者),Acceptor(接受者),和Learner(学习者)。每个节点都可以身兼数职
一个完整的Paxos算法流程分为三个阶段:
详细描述:
情景示例1:
set a 5
set a 5
Accept请求set a 5
的Accept请求,不违背事务 ID 小于 0,则接受并持久化当前事务 ID 及提案值 5情景示例2:
set a 5
,E收到写入请求 set a 10
Zab协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议,是ZooKeeper保证数据一致性的核心算法。Zab借鉴了Paxos算法,但又不像Paxos那样,是一种通用的分布式一致性算法。它是特别为ZooKeeper设计的支持崩溃恢复的原子广播协议。
Zab协议的核心:定义了事务请求的处理方式
Zab 协议包括两种基本的模式:崩溃恢复和消息广播
崩溃恢复:一旦Leader服务器出现崩溃或者由于网络原因导致Leader服务器失去了与过半 Follower的联系,那么就会进入崩溃恢复模式
崩溃恢复主要包括两部分:Leader选举和数据恢复。
Leader选举:
数据同步:
Zab数据同步过程中,如何处理需要丢弃的Proposal?
在Zab的事务编号zxid设计中,zxid是一个64位的数字。其中低32位可以看成一个简单的单增计数器,针对客户端每一个事务请求,Leader在产生新的Proposal事务时,都会对该计数器加1。而高32位则代表了Leader周期的epoch编号。
epoch编号可以理解为当前集群所处的年代,或者周期。每次Leader变更之后都会在 epoch的基础上加1,这样旧的Leader崩溃恢复之后,其他Follower也不会听它的了,因为 Follower只服从epoch最高的Leader命令。
每当选举产生一个新的 Leader,就会从这个Leader服务器上取出本地事务日志充最大编号Proposal的zxid,并从zxid中解析得到对应的epoch编号,然后再对其加1,之后该编号就作为新的epoch 值,并将低32位数字归零,由0开始重新生成zxid。
Zab协议通过epoch编号来区分Leader变化周期,能够有效避免不同的Leader错误的使用了相同的zxid编号提出了不一样的Proposal的异常情况。
基于以上策略,当一个包含了上一个Leader周期中尚未提交过的事务Proposal的服务器启动时,当这台机器加入集群中,以Follower角色连上Leader服务器后,Leader 服务器会根据自己服务器上最后提交的 Proposal来和Follower服务器的Proposal进行比对,比对的结果肯定是Leader要求Follower进行一个回退操作,回退到一个确实已经被集群中过半机器Commit的最新Proposal。
一个分布式系统不可能同时满足以下三种
ZooKeeper保证的是CP