zookeeper中的从选paxos和fast paxos算法到实现分布式锁和分布式队列
1.paxos算法
为什么需要paxos
相关概念:Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致。
解释为什么需要paxos锁:zookeeper需要同时对某一个节点进行某种操作,为了达成这种操作我们需要使用一个分布式算法来制定一种规则来制约,使所有节点的意见统一,于是就产生了paxos算法来统一这种规范。
paxos算法的思路
paxos的三种角色:
(1)Proposer: 提出提案 (Proposal)。Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)。
(2)Acceptor:参与决策,回应Proposers的提案。收到Proposal后可以接受提案,若Proposal获得多Acceptors的接受,则称该Proposal被批准。
(3)Learner:不参与决策,从Proposers/Acceptors学习最新达成一致的提案(Value)。
描述Paxos算法执行过程:
阶段一:
(a) Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。
(b) 如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于N的提案。
阶段二:
(a) 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor。注意:V就是收到的响应中编号最大的提案的value,如果响应中不包含任何提案,那么V就由Proposer自己决定。
(b) 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案。
三阶段
Learn阶段。Proposer在收到多数Acceptors的Accept之后,标志着本次Accept成功,决议形成,将形成的决议发一个Learners的子集。受到结果的Learners子集将结果发送给其他的Learners,Learners中保存的是选举结果。
paxos锁的规则限制:
(1)当有多个proposer时,acceptors会根据权值选择最大的提案
(2)一旦有一个proposer进入且被accpetor同意通过,那么acceptor就无法改变主意。
(3)proposer可以同时像多个acceptors提案,争取更多的accptors尽快同意,因为一旦超过了会议的时间限制,每个acceptors已经选定好了提案,那么无法更改。
2.分布式锁
有了分布式一致性,我们去取锁时多个需要锁的节点并发同步问题就迎刃而解了。
创建节点的类型:
(1)有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号,也就是说如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。
(2)临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。
(3)事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更。
取锁的过程:
(1)zookeeper实现分布式锁的算法流程,假设锁空间的根节点为/lock:
(2)客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
(3)客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;
(4)执行业务代码;
(5)完成业务流程后,删除对应的子节点释放锁。``
分布式锁:思路就是每次进入zookeeper的时候判断该节点是否是最小的编号的znode,如果是最小节点则获得锁成功,每次都是znode最小的节点获得锁,通过删除节点来释放锁。
package zookeeper.DistributedLock;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/** * zookeeper实现分布式锁 */
public class DistributedLock implements Watcher{
private int threadId;
private ZooKeeper zk = null;
private String selfPath;
private String waitPath;
private String PREFIX_OF_THREAD;
private static final int SESSION_TIMEOUT = 10000;
private static final String GROUP_PATH = "/lock";
private static final String SUB_PATH = "/lock/sub";
private static final String CONNECTION_STRING = "xxx:2181";
private static final int THREAD_NUM = 10;
//确保连接zk成功;
private CountDownLatch connectedSemaphore = new CountDownLatch(1);
//确保所有线程运行结束;
private static final CountDownLatch threadSemaphore = new CountDownLatch(THREAD_NUM);
public DistributedLock(int id) {
this.threadId = id;
PREFIX_OF_THREAD = "【第"+threadId+"个线程】";
}
public static void main(String[] args) {
for(int i=0; i < THREAD_NUM; i++){
final int threadId = i+1;
new Thread(){
@Override
public void run() {
try{
DistributedLock dc = new DistributedLock(threadId);
dc.createConnection(CONNECTION_STRING, SESSION_TIMEOUT);
//GROUP_PATH不存在的话,由一个线程创建即可;
synchronized (threadSemaphore){
dc.createPath(GROUP_PATH, "该节点由线程" + threadId + "创建", true);
}
dc.getLock();
} catch (Exception e){
System.out.println(("【第"+threadId+"个线程】 抛出的异常:"));
e.printStackTrace();
}
}
}.start();
}
try {
threadSemaphore.await();
System.out.println("所有线程运行结束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** * 获取锁:我们检查是否是最小的节点,如果是最小的节点就获得锁的所有权 * @return */
private void getLock() throws KeeperException, InterruptedException {
selfPath = zk.create(SUB_PATH,null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(PREFIX_OF_THREAD+"创建锁路径:"+selfPath);
if(checkMinPath()){
getLockSuccess();
}
}
/** * 创建锁节点 * @param path 锁节点path * @param data 初始数据内容 * @return */
public boolean createPath( String path, String data, boolean needWatch) throws KeeperException, InterruptedException {
if(zk.exists(path, needWatch)==null){
System.out.println(PREFIX_OF_THREAD + "节点创建成功, Path: "
+ this.zk.create( path,
data.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT)
+ ", content: " + data );
}
return true;
}
/** * 创建ZK连接 * @param connectString ZK服务器地址列表 * @param sessionTimeout Session超时时间 */
public void createConnection( String connectString, int sessionTimeout ) throws IOException, InterruptedException {
zk = new ZooKeeper( connectString, sessionTimeout, this);
connectedSemaphore.await();
}
/** * 获取锁成功 */
public void getLockSuccess() throws KeeperException, InterruptedException {
if(zk.exists(this.selfPath,false) == null){
System.out.println(PREFIX_OF_THREAD+"本节点已不在了...");
return;
}
System.out.println(PREFIX_OF_THREAD + "获取锁成功,开始干活!");
Thread.sleep(2000);
releaseConnection();
threadSemaphore.countDown();
}
/** * 关闭ZK连接 */
public void releaseConnection() {
if ( this.zk !=null ) {
try {
this.zk.close();
} catch ( InterruptedException e ) {}
}
System.out.println(PREFIX_OF_THREAD + "工作完毕,释放锁");
}
/** * 核心代码:检查自己是不是最小的节点 * @return */
public boolean checkMinPath() throws KeeperException, InterruptedException {
List<String> subNodes = zk.getChildren(GROUP_PATH, false);
Collections.sort(subNodes);
//判断当前此节点
int index = subNodes.indexOf( selfPath.substring(GROUP_PATH.length()+1));
switch (index){
case -1:{
System.out.println(PREFIX_OF_THREAD+"本节点已不在了..."+selfPath);
return false;
}
case 0:{
System.out.println(PREFIX_OF_THREAD+"子节点中,我最小,可以获得锁了!哈哈"+selfPath);
return true;
}
default:{
this.waitPath = GROUP_PATH +"/"+ subNodes.get(index - 1);
System.out.println(PREFIX_OF_THREAD+"排在我前面的节点是 "+waitPath);
try{
zk.getData(waitPath, true, new Stat());
return false;
}catch(KeeperException e){
if(zk.exists(waitPath,false) == null){
System.out.println(PREFIX_OF_THREAD+"排在我前面的"+waitPath+"已消失 ");
return checkMinPath();
}else{
throw e;
}
}
}
}
}
@Override
public void process(WatchedEvent event) {
if(event == null){
return;
}
Event.KeeperState keeperState = event.getState();
Event.EventType eventType = event.getType();
//已经是连接到zookeeper的状态
if ( Event.KeeperState.SyncConnected == keeperState) {
if ( Event.EventType.None == eventType ) {
System.out.println(PREFIX_OF_THREAD + "成功连接上ZK服务器" );
connectedSemaphore.countDown();
}else if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
//监听节点被删除状态
System.out.println(PREFIX_OF_THREAD + "watch说我前面那个节点挂了,我可以获取锁啦!!");
try {
//判断当前节点是否为最小节点
if(checkMinPath()){
getLockSuccess();
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}else if ( Event.KeeperState.Disconnected == keeperState ) {
//监听断开
System.out.println(PREFIX_OF_THREAD + "与ZK服务器断开连接" );
} else if ( Event.KeeperState.AuthFailed == keeperState ) {
//监听权限失败
System.out.println(PREFIX_OF_THREAD + "权限检查失败" );
} else if ( Event.KeeperState.Expired == keeperState ) {
//监听会话失效
System.out.println(PREFIX_OF_THREAD + "会话失效" );
}
}
}
3.zokeeper分布式队列
题目:利用zookeeper的分布一致性实现创建一个分布式队列,队列中有三个节点x1,x2,x3.
思路:先创建一个/queue节点,然后分别创建/queue/x1,/queue/x2,/queue/x3三个节点形成一个队列,每次创建x节点的时候就判断是否/queue下的子节点个数为3,如果为3就创建/queue/start,这样程序里面有一个connection实现了watch中的pocess方法就会监控到3个队列创建完成!
图标解释
app1,app2,app3,app4是4个独立的业务系统
zk1,zk2,zk3是ZooKeeper集群的3个连接点
/queue,是znode的队列,假设队列长度为3
/queue/x1,是znode队列中,1号排对者,由app1提交,同步请求,app1挂载等待
/queue/x2,是znode队列中,2号排对者,由app2提交,同步请求,app2挂起等待
/queue/x3,是znode队列中,3号排对者,由app3提交,同步请求,app3挂起等待
/queue/start,当znode队列中满了,触发创建开始节点
当/qeueu/start被创建后,app4被启动,所有zk的连接通知同步程序(红色线),队列已完成,所有程序结束
注:
1). 创建/queue/x1,/queue/x2,/queue/x3没有前后顺序,提交后程序就同步挂起。
2). app1可以通过zk2提交,app2也可通过zk3提交
3). app1可以提交3次请求,生成x1,x2,x3使用队列充满
4). /queue/start被创建后,zk1会监听到这个事件,再告诉app1,队列已完成!
package org.conan.zookeeper.demo;
import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
public class QueueZooKeeper {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
doOne();
} else {
doAction(Integer.parseInt(args[0]));
}
}
//初始化zokeeper连接:单机zookeeper
public static void doOne() throws Exception {
String host1 = "192.168.1.201:2181";
ZooKeeper zk = connection(host1);
initQueue(zk);
joinQueue(zk, 1);
joinQueue(zk, 2);
joinQueue(zk, 3);
zk.close();
}
//初始化zokeeper连接:分布式zookeeper,输入1表示用host1连接zookeeper创建节点/queue/x1
public static void doAction(int client) throws Exception {
String host1 = "192.168.1.201:2181";
String host2 = "192.168.1.201:2182";
String host3 = "192.168.1.201:2183";
ZooKeeper zk = null;
switch (client) {
case 1:
zk = connection(host1);
initQueue(zk);
joinQueue(zk, 1);
break;
case 2:
zk = connection(host2);
initQueue(zk);
joinQueue(zk, 2);
break;
case 3:
zk = connection(host3);
initQueue(zk);
joinQueue(zk, 3);
break;
}
}
// 创建一个与服务器的连接
public static ZooKeeper connection(String host) throws IOException {
ZooKeeper zk = new ZooKeeper(host, 60000, new Watcher() {
// 监控所有被触发的事件:监听名字叫做“/queue/start”的znode是否被创建,一旦创建就输出所有节点创建成功
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeCreated && event.getPath().equals("/queue/start")) {
System.out.println("Queue has Completed.Finish testing!!!");
}
}
});
return zk;
}
//初始化创建queue节点
public static void initQueue(ZooKeeper zk) throws KeeperException, InterruptedException {
System.out.println("WATCH => /queue/start");
zk.exists("/queue/start", true);
if (zk.exists("/queue", false) == null) {
System.out.println("create /queue task-queue");
zk.create("/queue", "task-queue".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
} else {
System.out.println("/queue is exist!");
}
}
//加入队列,创建x1,x2,x3
public static void joinQueue(ZooKeeper zk, int x) throws KeeperException, InterruptedException {
System.out.println("create /queue/x" + x + " x" + x);
zk.create("/queue/x" + x, ("x" + x).getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
isCompleted(zk);
}
//检查/queue下的节点个数是否为3,如果是就创建/queue/start节点
public static void isCompleted(ZooKeeper zk) throws KeeperException, InterruptedException {
int size = 3;
int length = zk.getChildren("/queue", true).size();
System.out.println("Queue Complete:" + length + "/" + size);
if (length >= size) {
System.out.println("create /queue/start start");
zk.create("/queue/start", "start".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
}
}
}
4.FastLeaderElection选举算法
FastLeaderElection选举算法是标准的Fast Paxos算法实现。
服务器状态:
LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举
FOLLOWING 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁
LEADING 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳
OBSERVING 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票
每个服务器在进行领导选举时,会发送如下关键信息:
logicClock 每个服务器会维护一个自增的整数,名为logicClock,它表示这是该服务器发起的第多少轮投票
state 当前服务器的状态
self_id 当前服务器的myid
self_zxid 当前服务器上所保存的数据的最大zxid
vote_id 被推举的服务器的myid
vote_zxid 被推举的服务器上所保存的数据的最大zxid
开始第一次投票
此时集群中的所有机器都处于一种试图选举出一个Leader的状态,我们把这种状态称为“LOOKING”,意思是说正在寻找Leader。当一台服务器处于LOOKING状态的时候,那么它就会向集群中所有其他机器发送消息,我们称这个消息为“投票”。
在这个投票消息中包含两个最基本的信息:
所推举的服务器的SID和ZXID,分别表示了被推举服务器的唯一标识和事务ID。
下文中我们将以“(SID, ZXID)”这样的形式 来标识一次投票信息。举例来说,如果当前服务器要推举SID为1、ZXID为8的服务器成为Leader,那么它的这次投票信息可以表示为(1,8)。
我们假设ZooKeeper由5台机器组成,SID分別为1、2、3、4和5, ZXID分别为9、9、9、8和8,并且此时SID为2的机器是Leader服务器。某一时刻,1和2所在的机器出现故障,因此集群开始进行Leader选举。
在第一次投票的时候,由于还无法检测到集群中其他机器的状态信息,因此每台机器都是将自己作为被推举的对象来进行投票,于是SID为3、4和5的机器,投票情况分别为:(3, 9)、(4, 8)和(5, 8)。
变更投票
每次对于收到的投票的处理,都是一个对(vote_sid,vote_zxid)和(self_sid,self_zxid) 对比的过程,假设Epoch相同的情况下。
规则如下:
1、如果vote_zxid大于自己的self_zxid,就认可当前收到的投票,并再次将该投票发送出去。
2、如果vote_zxid小于自己的self_zxid,那么就坚持自己的投票,不做任何变更。
3、如果vote_zxid等于自己的self_zxid,那么就对比两者的SID。如果vote_sid大于self_sid,那么就认可当前接收到的投票,并再次将该投票发送出去。
4、如果vote_zxid等于自己的self_zxid,并且vote_sid小于self_sid,那么同样坚持自己的投票,不做变更。
根据上面这个规则,我们结合图来分折上面提到的5台机器组成的ZooKeeper集群的投票变更过程。
对干Server3来说,它接收到了(4, 8)和(5, 8)两个投票,对比后,由子自己的ZXID要大于接收到的两个投票,因此不需要做任何变更。
对于Server4来说,它接收到了(3, 9)和(5, 8)两个投票,对比后,由于(3, 9)这个投票的ZXID大于自己,因此需要变更投票为(3, 9),然后继续将这个投票发送给另外两台机器。
同样,对TServer5来说,它接收到了(3, 9)和(4, 8)两个投票,对比后,由于(3, 9)这个投票的ZXID大于自己,因此需要变更投票为(3, 9),然后继续将这个投票发送给另外两台机器。
确定Leader
因为已经有超过半数的节点投票给3,故3成为了主节点。
5.paxos和fast paxos的区别
Paxos的几个阶段:
Phase 1a: Leader提交proposal到Acceptor
Phase 2b:Acceptor回应已经参与投票的最大Proposer编号和选择的Value
Phase 2a:Leader收集Acceptor的返回值
Phase 2a.1:如果Acceptor无返回值,则自由决定一个
Phase 2a.2: 如果有返回值,则选择Proposer编号最大的一个
Phase 2b:Acceptor把表决结果发送到Learner
fast paxos交互过程:
Phase 1a:Leader提交proposal到Acceptor
Phase 1b:Acceptor回应已经参与投票的最大Proposer编号和选择的Value
Phase 2a:Leader收集Acceptor的返回值
Phase 2a.1:如果Acceptor无返回值,则发送一个Any消息给Acceptor,之后Acceptor便等待Proposer提交Value
Phase 2a.2:如果有返回值,则根据规则选取一个
Phase2b:Acceptor把表决结果发送到Learner(包括Leader)
分析:在第二阶段中,因为fast paxos算法选举会是多个proposer进行提交给acceptor交互,如果提议成功,那么直接将最终决议发给learner。 所以通信过程为:proposer-acceptor-learner之间的关系。如果是提议未被acceptor接收,那么将会退回成为propose-learder-acceptor-learner。
paxos算法第二阶段的通信过程为:proposer-leader-acceptor-learner。
多个proposer可能提议冲突,那么paxos算法会先在leader中解决冲突,由于fast paxos这种乐观锁的思想,如果proposer提议成功则执行,执行失败则回滚,相对来说fast paxos通信次数更少。执行算法的速度更快。
图中的相关书术语:
Round:在Basic Paxos中,Proposer每次提案都用一个全序的编号表示,如果执行顺利,该编号的Proposal在经历Phase1,Phase2后最终会执行成功。在Fast Paxos称这个带编号的Proposal的执行过程为“Round”
Fast Round: 如果Leader发送了Any消息,则认为后续通信是一个Fast Round;若Leader未发送Any消息,还是跟之前一样通信,则后续行为仍然是Basic Round。根据Lamport描述,Basic Round和Fast Round可通过Round Number进行加以区分
参考文献:
https://blog.csdn.net/u013679744/article/details/79222103
https://blog.csdn.net/xhh198781/article/details/10949697
《云计算》(第二版)刘鹏主编
http://www.dengshenyu.com/java/分布式系统/2017/10/23/zookeeper-distributed-lock.html
http://blog.fens.me/zookeeper-queue/
https://www.souyunku.com/2018/05/23/zookeeper/
http://www.jasongj.com/zookeeper/fastleaderelection/
https://blog.csdn.net/Thousa_Ho/article/details/78825415
https://www.cnblogs.com/bangerlee/p/6189646.html