前言
前篇文章主要针对 zk的基本使用,以及特性和基本使用点 的分析,本篇文章会继续分析 zk的集群 如何搭建一个zk集群 部署 以及监控,以及 leader选举, 协议 核心,崩溃恢复 数据同步 数据配置 中心,以及 我们 常在dubbo上结合使用zk 做为配置中 心 的分析和实现 ,分布式队列的分析 和实现。
ZK集群安装与搭建
- 可靠的ZooKeeper 服务
- 只要集群的大多数都准备好了,就可以使用这项服务
- 容错集群设置至少需要三台集群,强烈使用奇数个服务器
- 建议都部署到不同的机器上
配置
直接 配置 具体的ip地址 以及 配置具体的 数据 日志 目录 和事务日志 以及 心跳时间配置,包括 里面的 2开头 端口 zk间通信的 端口 以及 选举的端口 3开头的。
集群节点
server.id=host:port:port
id ,通过在各自的dataDir目录下创建一个名为myid的文件来为每 台机器赋予一个服务器id。
两个端口号 ,第一个跟随者用来连接到领导者,第二个用来选举领导者。
initLimit
集群中的follower服务器(F)与leader服务器(L)之间完成初始化同 步连接时能容忍的最多心跳数(tickTime的数量)。如果zk集群 环境数量确实很大,同步数据的时间会变长,因此这种情况下 可以适当调大该参数。
syncLimit
集群中的follower服务器与leader服务器之间请求和应答之间能 容忍的最多心跳数(tickTime的数量)。
每台服务创建 myid文件 需要在 dataDir目录下创建;一行只包含机器id的文本,id在集群中必须是唯一的,其值应在1到255之间 如服务id为 1
集群的所有的节点 都提供服务,客户端链接时,连接串可以指定多个或者全部的节点的链接地址,当一个节点不通时,客户端会自动切换另一个节点。例如
集群的监控
第一种方式 telnet 去设置
第二种方式
使用JMX的方式
JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。
根据配置信息去查询。
leader
对于leader都在竞争选择 成为 具体的leader.
Paxos算法
Paxos算法是Lamport宗师提出的一种基于消息传递的分布式一致性算法,使其获得2013年图灵奖。
Paxos由Lamport于1998年在《The Part-Time Parliament》论文中首次公开,最初的描述使用希腊的一个小岛Paxos作为比喻,描述了Paxos小岛中通过决议的流程,并以此命名这个算法,但是这个描述理解起来比较有挑战性。后来在2001年,Lamport觉得同行不能理解他的幽默感,于是重新发表了朴实的算法描述版本《Paxos Made Simple》。
自Paxos问世以来就持续垄断了分布式一致性算法,Paxos这个名词几乎等同于分布式一致性。Google的很多大型分布式系统都采用了Paxos算法来解决分布式一致性问题,如Chubby、Megastore以及Spanner等。开源的ZooKeeper,以及MySQL 5.7推出的用来取代传统的主从复制的MySQL Group Replication等纷纷采用Paxos算法解决分布式一致性问题。
然而,Paxos的最大特点就是难,不仅难以理解,更难以实现。
P1a:提议发起者选择一个提案编号N,并且给大多数接收者发送一个带有编号n的提案预请求。
P1b:如果接收者收到一个编号n提案预请求,请求编号n大于前面已经响应过的预发请求编号,这是接收者做出相应,承若不再接受比编号n小的请求,并且如果存在比自己接受过的最高编号提案,则相应中带上,
P2a:如果提议发起者接收到了大多数接受者对于编号n预发请求的相应,这时会给这些接收者的每一个服务发送接受请求,接受请求内容为,编号n的提案并带上value的v,v的取值从哪里来,如果接收者响应中,有其他提议的内容,则接受者相应中取最高编号对应的值,如果响应中没有其他提议的内容,则可以是任意的值。
P2b:如果接受者受到一个编号n的提案接受请求,它接受该提案,如果它已经准备对大于n的编号的预发请求做出响应。则不接受编号n的请求。
paxos的结束
最终只有一个提议值会被选择,只有被选择的提议者才会被learner节点学习。 最终总有一个提议生成,paxos协议能够让properties 发送的提议朝着大多数acceptor接受那个提议靠拢,因此保证可终止性。
paxos的流程
proposer会发送两种类型的消息 acceptors prepare准备 和accept接收请求。
proposer发送的提议请求由两部分组成, n为序号, v为提议值。
proposerA B 都可以发送prepare提议请求。 acceptor C D 先接受到 proposer A 请求 acceptor E 先接受到 proposer b的请求。
paxos 的流程 prepare b
如果 acceptor 接收到 prepare提议的请求(n1,v1),并且还未接受到任何请求,会发送一个提议请求的响应。设置当前提议者 为(n1,v1) 保证不再接受序号小于 v1的提议请求。
paxos 的流程 accept a
随后 acceptor C D 会接受到 proposer b的提议请求 (n=4,v=8) 先前的提议请求 (n=2,v=5) 由于 4>2 由此发送 (n=2,v=5) 的提议响应,设置当前接收到的提议(n=4,v=5) 并且保证不再接受序号小于4的提议请求; acceptor E 先收到 Proposer B 的提议请求 (n=4.v=8) 后收到proposer a 提议请求(n=2,v=8) 后收到proposer a 的提议请求 (n=2,v=5) 由于 2<4 由此抛弃该提议请求。
Paxos 的流程 accept b
一旦proposer 收到超过半数 acceptor 所发送 prepare 提议响应。 便会向所有acceptor 发送一个 accept提议请求。
proper A 收到两个提议 响应 后,发送 accept提议请求 (n=2,v=5),会被所有acceptor 丢弃。 proposer b 收到两个提议响应 (n=2,v=5) 发送 acceptor 提议请求 , 需要注意 n=4 是最初 proposer b 使用的序号, v=5 不是初始值, 而是提议响应中最高的v值。
Paxox 的流程 learner
Acceptor C D E 收到 acceptor 提议请求后,会通知 所有的 learner
选举中的概念
包括 在 选举过程中 涉及到的状态。
选举算法
胜出条件 就是
投票数赞成大于半数则胜出的逻辑。
数据一致性
zk根据自身的情况建立了一套 ZAB协议
ZAB协议
ZAB协议,全称 Zookeeper Atomic Broadcast(Zookeeper 原子广播协议)。它是专门为分布式协调服务——Zookeeper,设计的一种支持崩溃恢复和原子广播的协议。
参考Paxos 算法实现的。
关注点数据一致性,无关数据准确性,权威性,实时性。
ZAB协议的过程
所有事务都转发给 leader节点 产生一个 zxid
leader分配全局递增事务id,广播事务提议
follwer处理提议,做出反馈
leader 收到半数的响应 广播commit
leader 做出响应
从这流程 就能看出 zk适合 写比较少 都比较多的场景。
并且有个重要特征是有序性 ,这里半数就可以提交了。 并不是 需要所有都同意才提交。
leader里面有个队列,用来保证 执行顺序
Leader崩溃
Leader 服务器 出现崩溃,或者说由于网络原因导致leader服务器失去了与过半Follwer 的联系,那么 就会进入崩溃恢复模式。
也就是在之前配置的时候 3开头的端口号。
ZAB协议规定一个事务Proposal在一台机器上处理成功,那么应该在所有机器上都被处理成功,哪怕机器出现故障崩溃。
ZAB协议确保那些已经在Leader服务器上提交事务最终被所有服务器都提交。
ZAB协议确保丢弃那些只在Leader服务器上被提出的事务。
对于leader也好 还是 follower 都为了保证数据的正确性,都要zxid的最大个,进行恢复 同步。
崩溃的恢复
ZAB协议需要设计的选举算法应该满足:确保提交已经被Leader提交事务Proposal.同时丢弃已经被跳过的事务 Proposal
如果让Leader选举算法能够保证新选举出来的leader服务器拥有集群中所有机器最高ZXID的事务proposal 那么就可以保证这个新选举出来leader 一定有所有已经提交的提案。同时 如果让具有最高事务编号 的机器成为leader 就可以省去leader 服务器检查 proposal 的提交和丢弃工作这一步骤的。
数据同步
Leader选举出来后,需完成Followers与Leader的数据同步,当半数的Followers完成同步, 则可以开始提供服务。同步的过程如下。 Leader服务器会为每一个Follower服务器都准备一个队列,并将那些没有被各Follower服 务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每一个Proposal消 息后面紧接着再发送一个Commit消息,以表示该事务已经被提交。等到Follower服务器将所 有其尚未同步的事务Proposal都从Leader服务器上同步过来并成功应用到本地数据库中后, Leader服务器就会将该Follower服务器加入到真正的可用Follower列表中,并开始之后的其他 流程。
丢弃事务的处理
在ZAB协议的事务编号ZXID设计中,ZXID是一个64位的数字,低32位是一个简单的单调递增的计 数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal的时候,都会对该计数 器进行加1操作;高32位代表了 Leader周期纪元的编号,每当选举产生一个新的Leader服务器,就会从这 个Leader服务器上取出其本地日志中最大事务Proposal的ZXID,并从该ZXID中解析出对应的纪元值,然后 对其进行加1操作,之后就会以此编号作为新的纪元,并将低32位置0来开始生成新的ZXIDo 基于这样的策略,当一个包含了上一个Leader周期中尚未提交过的事务
Proposal的服务器启动加入到集群 中,发现此时集群中已经存在leader,将自身以Follower角色连接上Leader服务器之后,Leader服务器会根据 自己服务器上最后被提交的Proposal来和Follower服务器的Proposal进行比对,发现follower中有上一个 leader周期的事务Proposal时,Leader会要求Follower进行一个回退操作一一回退到一个确实已经被集群中过 半机器提交的最新的事务Proposal 。
下面 zk的配置等等。
ZooKeeper: Because Coordinating Distributed Systems is a Zoo (apache.org)
经典应用场景
1、数据发布订阅 2、命名服务、3 master 选举 4.集群管理 5.分布式队列 6.分布式锁
分布式队列的应用场景
利用 zk达到解耦 的场景。
异步
在项目中 利用zk 去达到 订阅 等功能确实很少,但是也可以使用这种方式去解决
削峰填谷
并且可以利用 zk达到队列的效果
实现方式
主要还是利用 顺序节点 达到 实现队列的
入队逻辑
出队的逻辑
整体的代码逻辑
- ZKDistributeQueue 分布式队列实现
- 使用zk指定的目录作为队列,子节点作为任务。
- 生产者能往队列中添加任务,消费者能往队列中消费任务。
- 持久节点作为队列,持久顺序节点作为任务。
public class ZkDistributeQueue extends AbstractQueue<String> implements BlockingQueue<String> , java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* zookeeper客户端操作实例
*/
private ZkClient zkClient;
/**
* 定义在zk上的znode,作为分布式队列的根目录。
*/
private String queueRootNode;
private static final String default_queueRootNode = "/distributeQueue";
/**队列写锁节点*/
private String queueWriteLockNode;
/**队列读锁节点*/
private String queueReadLockNode;
/**
* 子目录存放队列下的元素,用顺序节点作为子节点。
*/
private String queueElementNode;
/**
* ZK服务的连接字符串,hostname:port形式的字符串
*/
private String zkConnUrl;
private static final String default_zkConnUrl = "localhost:2181";
/**
* 队列容量大小,默认Integer.MAX_VALUE,无界队列。
**/
private static final int default_capacity = Integer.MAX_VALUE;
private int capacity;
/**
* 控制进程访问的分布式锁
*/
final Lock distributeWriteLock;
final Lock distributeReadLock;
public ZkDistributeQueue() {
this(default_zkConnUrl, default_queueRootNode, default_capacity);
}
public ZkDistributeQueue(String zkServerUrl, String rootNodeName, int initCapacity) {
if (zkServerUrl == null) throw new IllegalArgumentException("zkServerUrl");
if (rootNodeName == null) throw new IllegalArgumentException("rootNodeName");
if (initCapacity <= 0) throw new IllegalArgumentException("initCapacity");
this.zkConnUrl = zkServerUrl;
this.queueRootNode = rootNodeName;
this.capacity = initCapacity;
init();
distributeWriteLock = new ZkDistributeImproveLock(queueWriteLockNode);
distributeReadLock = new ZkDistributeImproveLock(queueReadLockNode);
}
/**
* 初始化队列信息
*/
private void init() {
queueWriteLockNode = queueRootNode+"/writeLock";
queueReadLockNode = queueRootNode+"/readLock";
queueElementNode = queueRootNode+"/element";
zkClient = new ZkClient(zkConnUrl);
zkClient.setZkSerializer(new MyZkSerializer());
if (!this.zkClient.exists(queueElementNode)) {
try {
this.zkClient.createPersistent(queueElementNode, true);
} catch (ZkNodeExistsException e) {
}
}
}
/**
* 获取队列头部元素,但不执行删除,如果队列为空则返回null。
*
*/
@Override
public String peek() {
List<String> children = zkClient.getChildren(queueElementNode);
if(children != null && !children.isEmpty()) {
children = children.stream().sorted().collect(Collectors.toList());
String firstChild = children.get(0);
String elementData = zkClient.readData(queueElementNode+"/"+firstChild);
return elementData;
}
return null;
}
/**
* 往zk中添加元素
* @param e
*/
private void enqueue(String e) {
zkClient.createPersistentSequential(queueElementNode+"/", e);
}
/**
* 从zk中删除元素
* @param e
* @return
*/
private boolean dequeue(String e) {
boolean result = zkClient.delete(e);
return result;
}
// ==============================特殊值操作============================
/**
* 获取并删除头部元素,如果队列为空则返回null
*/
@Override
public String poll() {
String firstChild = peek();
distributeReadLock.lock();
try {
if (firstChild == null) {
return null;
}
boolean result = dequeue(firstChild);
if (!result) {
return null;
}
} finally {
distributeReadLock.unlock();
}
return firstChild;
}
/**
* 容量足够立即将指定的元素插入到队列中,成功时返回true,容量不够,则返回false。
*
*/
@Override
public boolean offer(String e) {
checkElement(e);
// 判断是否可以添加任务,不能则抛出异常
distributeWriteLock.lock();
try {
if(size() < capacity) {
enqueue(e);
return true;
}
} finally {
distributeWriteLock.unlock();
}
return false;
}
// ==============================抛出异常操作============================
/**
* 获取并删除队列头部元素,当队列为空时抛出异常
*/
@Override
public String remove() {
if(size() <= 0) {
throw new IllegalDistributeQueueStateException(
IllegalDistributeQueueStateException.State.empty);
}
distributeReadLock.lock();
String firstChild;
try {
firstChild = poll();
if (firstChild == null) {
throw new IllegalDistributeQueueStateException("移除失败");
}
} finally {
distributeReadLock.unlock();
}
return firstChild;
}
/**
* 容量足够的情况下,将指定的元素加入队列,插入成功返回true,
* 容量不足够的情况下,抛出IllegalDistributeQueueStateException异常。
*/
@Override
public boolean add(String e) {
checkElement(e);
distributeWriteLock.lock();
try {
// 判断是否可以添加任务,不能则抛出异常
if(size() >= capacity) {
throw new IllegalDistributeQueueStateException(
IllegalDistributeQueueStateException.State.full);
}
return offer(e);
} catch (Exception e1) {
e1.printStackTrace();
} finally {
distributeWriteLock.unlock();
}
return false;
}
// 阻塞操作
@Override
public void put(String e) throws InterruptedException {
checkElement(e);
distributeWriteLock.lock();
try {
if(size() < capacity) { // 容量足够
enqueue(e);
System.out.println(Thread.currentThread().getName() + "-----往队列放入了元素");
}else { // 容量不够,阻塞,监听元素出队
waitForRemove();
put(e);
}
} finally {
distributeWriteLock.unlock();
}
}
/**
* 队列容量满了,不能再插入元素,阻塞等待队列移除元素。
*/
private void waitForRemove() {
CountDownLatch cdl = new CountDownLatch(1);
// 注册watcher
IZkChildListener listener = new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
if(currentChilds.size() < capacity) { // 有任务移除,激活等待的添加操作
cdl.countDown();
System.out.println(Thread.currentThread().getName() + "-----监听到队列有元素移除,唤醒阻塞生产者线程");
}
}
};
zkClient.subscribeChildChanges(queueElementNode, listener);
try {
// 确保队列是满的
if(size() >= capacity) {
System.out.println(Thread.currentThread().getName() + "-----队列已满,阻塞等待队列元素释放");
cdl.await(); // 阻塞等待元素被移除
}
} catch (InterruptedException e) {
e.printStackTrace();
}
zkClient.unsubscribeChildChanges(queueElementNode, listener);
}
@Override
public String take() throws InterruptedException {
distributeReadLock.lock();
try {
List<String> children = zkClient.getChildren(queueElementNode);
if(children != null && !children.isEmpty()) {
children = children.stream().sorted().collect(Collectors.toList());
String takeChild = children.get(0);
String childNode = queueElementNode+"/"+takeChild;
String elementData = zkClient.readData(childNode);
dequeue(childNode);
System.out.println(Thread.currentThread().getName() + "-----移除队列元素");
return elementData;
}else {
waitForAdd(); // 阻塞等待队列有元素加入
return take();
}
} finally {
distributeReadLock.unlock();
}
}
/**
* 队列空了,没有元素可以移除,阻塞等待元素添加到队列中。
*/
private void waitForAdd() {
CountDownLatch cdl = new CountDownLatch(1);
// 注册watcher
IZkChildListener listener = new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
if(currentChilds.size() > 0) { // 有任务了,激活等待的移除操作
cdl.countDown();
System.out.println(Thread.currentThread().getName() + "-----监听到队列有元素加入,唤醒阻塞消费者线程");
}
}
};
zkClient.subscribeChildChanges(queueElementNode, listener);
try {
// 确保队列是空的
if(size() <= 0) {
System.out.println(Thread.currentThread().getName() + "-----队列已空,等待元素加入队列");
cdl.await(); // 阻塞等待
System.out.println(Thread.currentThread().getName() + "-----队列已有元素,线程被唤醒");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
zkClient.unsubscribeChildChanges(queueElementNode, listener);
}
private static void checkElement(String v) {
if (v == null) throw new NullPointerException();
if("".equals(v.trim())) {
throw new IllegalArgumentException("不能使用空格");
}
if(v.startsWith(" ") || v.endsWith(" ")) {
throw new IllegalArgumentException("前后不能包含空格");
}
}
/**
* 返回队列中已存在的元素数量
*/
@Override
public int size() {
int size = zkClient.countChildren(queueElementNode);
return size;
}
/**
* 暂不支持迭代子
*/
@Override
public Iterator<String> iterator() {
throw new UnsupportedOperationException();
}
// 暂不支持
@Override
public boolean offer(String e, long timeout, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public String poll(long timeout, TimeUnit unit) throws InterruptedException {
return null;
}
/**
* 返回剩余容量,没有限制返回Integer.MAX_value。
*/
@Override
public int remainingCapacity() {
int remaining = capacity - size();
return remaining;
}
@Override
public int drainTo(Collection<? super String> c) {
return drainTo(c, size());
}
/**
* 从队列移除指定大小的元素,并将移除的元素添加到指定的集合中。
*/
@Override
public int drainTo(Collection<? super String> c, int maxElements) {
if(c == null) {
throw new NullPointerException();
}
int transferredSize = 0;
List<String> children = zkClient.getChildren(queueElementNode);
if(children != null && !children.isEmpty()) {
List<String> removeElements = children.stream().sorted().limit(maxElements).collect(Collectors.toList());
transferredSize = removeElements.size();
c.addAll(removeElements);
removeElements.forEach((e)->{
zkClient.delete(e);
});
}
return transferredSize;
}
}
zk配置中心
参数配置以及动态修改
就像dubbo中 利用 zk来实现 配置中心。
将所有的配置项都放到 zk上面
public interface ConfigureWriter {
/**
* 创建一个新的配置文件
* @param fileName 文件名称
* @param items 配置项
* @return 新文件的在zk上的路径
*/
String createCnfFile(String fileName, Properties items);
/**
* 删除一个配置文件
* @param fileName
*/
void deleteCnfFile(String fileName);
/**
* 修改一个配置文件
* @param fileName
* @param items
*/
void modifyCnfItem(String fileName, Properties items);
/**
* 加载配置文件
* @param fileName
* @return
*/
Properties loadCnfFile(String fileName);
}
包括读的部分
public interface ConfigureReader {
/**
* 读取配置文件
* @param fileName 配置文件名称
* @param ChangeHandler 配置发生变化的处理器
* @return 如果存在文件配置,则返回Properties对象,不存在返回null
*/
Properties loadCnfFile(String fileName);
/**
* 监听配置文件变化,此操作只需要调用一次。
* @param fileName
* @param changeHandler
*/
void watchCnfFile(String fileName, ChangeHandler changeHandler);
/**
* 配置文件变化处理器
* ChangeHandler
*/
interface ChangeHandler {
/**
* 配置文件发生变化后给一个完整的属性对象
* @param newProp
*/
void itemChange(Properties newProp);
}
}
具体的实现
这些功能 基本 在客户端 已经实现了。 可以自己 进行升级修改。