zookeeper学习笔记(分布式过程协同)

本文深入探讨Zookeeper分布式协调服务的关键概念与实践,涵盖主从模式、锁机制、会话管理、监视与通知等功能,解析Zookeeper在选举主节点、任务分配、状态同步中的作用,以及其实现分布式应用中的挑战与解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一章概念与基础
选举主节点、管理组内成员关系、管理元数据等

主从工作模式:主节点分配任务给从节点。获取主节点的过程就是获取锁的过程,即互斥排他锁。
竞争:两个进程不能同时处理工作的情况,一个必须等到另外一个
HBase:用于选举主节点,以便跟踪可用服务器,并保持集群元数据
kafka:检测崩溃,实现主题的发现,并保持主题的生产和消费状态
Solr:存储集群的元数据,并协作更新这些元数据

以上应用可以看做连接到zookeeper服务器端的一组客户端,通过zookeeper的客户端API连接到zookeeper服务端进行相应操作。
API包括
1、保障强一致性、有序性和持久性
2、实现通用的同步原语的能力
3、在实际分布式系统中,并发往往导致不正确的行为,zookeeper提供了一种简单的并发处理机制

以前采用 分布式锁管理器 或者 分布式数据库 来实现协作

zookeeper不适用于 海量数据存储,这里可以用数据库和分布式文件系统等

分布式系统间通信:1.通过网络 2.通过某种共享存储
真实的系统中应该注意以下问题
1.消息延迟
2.处理器性能 消息延迟约等于发送端消耗时间+传输+接受的总和
3.时钟偏移 依赖处理器的时钟也许会导致错误的决策
进程崩溃、网络延时、进程时钟偏移,在异步系统中我们无法判断

主从模式系统实现必须解决
1、主节点发送错误并失效,系统无法分配新的任务或重新分配已失败的任务
2、从节点崩溃,已分配任务无法完成
3、通信故障,主从节点无法进行信息交换,节点将无法得知新任务分配给它

主节点失效
备份主节点,当主要节点崩溃的时候,备份接管主要节点的角色,进行故障转移
PS:不仅需要处理进入主节点的请求,还要恢复到旧的主节点崩溃前的状态
除了状态恢复,还要考虑,若主节点负载过高、或网络延迟,导致备份节点认为主节点挂了,然后接管成为主节点,执行所有必需程序,然后成为了第二个主节点。更糟糕的是,若一些从节点与主节点无法通信,如:由于网络分区,转而与第二主节点通信,此时系统中出现了两种状态,导致整体行为不一致,这种情况成为脑裂

从节点失效
首要需求是让主节点具有检测从节点崩溃的能力,若崩溃,则恢复现场,然后重新派发任务给其它节点。

通信故障
如果从节点与主节点网络连接断开,重新分配一个任务可能导致两个从节点执行相同的任务,如果一个任务允许多次执行,我们在进行任务再分配的时候不用验证第一个节点是否完成了任务,如果不允许,那么我们应用需要适应多个从节点执行相同任务的可能性。
PS:首先客户端可以告诉zookeeper某些数据的状态是临时状态,其次,同时zookeeper需要客户端定时发送是否存活的通知,若未能及时发送消息,那么所有从属于改客户端的临时状态都将被全部删除。通过这两个机制,在通信故障发送时,我们可以预防客户端独立运行而发生的宕机。

分布式协作的难点

第二章 表示原语的实现
znode 采用类似于文件系统的层级数状结构进行管理

-server id/master
-/workersfoo.com:2181/workers/work-1
-/tasksrun cmd;/tasks/task-1-2
-/assign/assign/worker-1run cmd;/assign/worker-1/task-1-1

/workers 节点作为父节点,其下一个znode保存了系统中一个可用从节点信息 foo.com:2181
/tasks 节点作为父节点,其下一个znode保存了所有已经创建并且等待从节点执行的任务的信息,主从模式的应用从 /tasks 下添加一个znode节点,用来表示一个新任务,并等待任务状态的znode节点。
/assign节点作为父节点,其下每个znode保存了分配到某个节点的一个任务信息,当主节点为某个从节点分配了一个任务,就会在/assign下增加一个子节点。

znode可能含有数据,也有可能为空(表示还没有选举出主节点),如果一个znode包含任何数据,那么数据存储为字节数组,具体格式特定于每个应用的实现,zook并不直接提供解析支持,我们可以使用PB、Thrift、MessagePack等序列化包来方便的处理保存于znode节点的数据格式,不过有时候UTF-8、ascii就够用了

create /path data 创建一个名为/path的znode节点,并包含数据data
delete /path 删除/path的znode
exists /path 校验是否存在
setData /path data
getData /path
getChildren /path 返回所有/path节点的子节点列表
zook 不允许局部写入读取,设置一个znode节点或者读取时,会整个替换或者读取。

znode的不同类型
持久节点: 只能通过delete删除,与临时的相反,临时的,创建该节点的客户端崩溃、关闭连接时,节点会被删除,数据不会丢失,主从模式的例子中即使主节点崩溃了
临时节点:传达了应用某些方面的信息,仅当创建者的会话有效时,这些信息必须有效保存。1.创建临时节点客户端消失、断开连接。2.某个客户端(不一定是创建者)主动删除该节点。临时节点不允许拥有子节点。
***有序节点***被分配唯一一个单调递增的整数。如创建了一个有序节点,路径为/task/task- ,那么zook将会分配一个序号,如1,并追加到路径之后,最后该节点为/task/task-1.通过提供了创建唯一名称的节点的简单方式,可直观贯穿节点创建顺序。

监视与通知
轮询查看某个路径下是否存在新的节点,这样浪费资源,因此,这里采用基于通知的机制:客户端向Zookeeper注册需要接收通知的znode,通过对znode设置监视点(watch)来接收通知,监视点是一个单次触发的操作,为了接收多个通知,每次通知后会重置一个新的监视点,且设置前会查看节点状态是否有新的znode加入,有就通知然后再次设置,就不会丢失信息。 且先通知,再修改znode,防止连续两个通知导致客户端看到的节点不同

PS:znode会有一个版本号,随着更新而自增,每次调用需要一起传入版本号,不一致则修改失败。

Zookeeper架构
服务器端运行于两种模式之下:独立模式(standalone)、仲裁模式(quorum).
独立模式:只有单独一个服务器
仲裁模式:有一组服务器,若每个服务器都有保存数据后再继续,时延无法接受,因此,必须设置一个立法者的最小数量,即5个服务器,立法者必须过半(多数原则即至少(n/2+1),必须保持),此时为3,即任意3个节点存储了数据就可以执行下一个指令,而其他的节点会异步保存数据,最终5个节点都会保存数据,其中允许崩溃的数量为2。若4个服务器,则立法者为3,此时允许崩溃数量为1,因为为2就违反了多数原则
PS:若节点为5,法定2,节点1、2确认他们要保存创建的znode,此时网络分区隔离发生,剩下3个节点,这三个只保存了一个znode,无法保证创建的znode是持久化的。

会话
在对Zookeeper集合执行任何请求之前,一个客户端必须先与服务建立会话,会话异常退出,则所有临时节点都会被删除。
通过特定的语言套件创建一个Zookeeper句柄的时候,就会痛服务建立一个会话,客户端初始连接到集合中的某一个服务器或者一个独立服务器,通过TCP协议连接并通信,当不可通信时,可能会转移到另外的服务器上。
会话提供顺序保证,同一个会话请求FIFO顺序执行。多个会话就不能保证!!!

Zookeeper使用
下载发行包,解压
重命名配置文件 mv conf/zoo-sample.cfg conf/zoo.cfg
将zook的data目录移出/temp目录,防止zook占满了根分区
zoo.cfg 修改 dataDir = /user/mydata/zookeeper
启动服务器 bin目录下 zkServer.sh start 后台运行 (start-foreground前台运行)
ls / 列出所有根节点 显示[zookeeper]
create /workers “” ;ls / 显示[zookeeper,workers ]
//这里 “” 表示我们现在不希望在znode保存数据,之后可以替换
quit退出

会话的状态和生命周期
生命周期只创建到结束的时期,无论正常退出还是异常超时。
NOT_CONNECTED、CONNECTING、CONNECTED、CLOSED
设置超时为T ,经过时间T服务没收到这个会话任何消息,服务会声明连接过期,客户端如果经过T/3未收到任何消息,那么客户端向服务端发送心跳消息,若T2/3未收到任何响应,那么客户端还有T/3的时间去寻找新的服务器。且只能连接状态和最后连接服务器状态保持最新的服务器。通过zxid(事务标识符)来确定。

Zookeeper与仲裁模式
可以一台机器运行多个服务器,使用如下配置文件:
tickTime=2000
initLimit=10
syncLimit=5
dataDir=./data
ClientPort=2181
server.1=localhost:2222:2223 //ip:冲裁通信端口:群首选举端口
server.2=localhost:3333:3334
server.3=localhost:4444:4445
分别设置data目录
mkdir -r z1/data mkdir -r z2/data mkdir -r z3/data
通过读取/data/z1/data目录下的myid来获取服务id信息 echo 1 > z1/data/myid,其它同理
创建z1/z1.cfg z2/z2.cfg z3/z3.cfg 启动服务
这里由日志可知服务器端口 localhost:2181 2182 2183
使用zkCli.sh访问 zkCli.sh -server localhost:2181,localhost:2182,localhost:2183
简单的负载均衡 连接服务器端口会随机变化2181、2182、2183
若跨地区,则连接的时候去掉跨地区的连接串即可。

实现一个源语:通过Zookeeper实现锁
假设一个应用由n个线程组成,同一时间获取一个锁,每个进程尝试创建znode,名为/lock,若成功创建则,表示可以执行临界区的代码,为了防止获取锁后线程崩溃,导致系统锁死,此时应使用 临时节点,其它进程因为znode存在而创建/lock失败,因此进程监听/lock变化,并在检测到/lock删除的时候,再次尝试创建节点来获得锁。

一个主从模式例子的实现
主节点:负责监视新的从节点和任务,分配任务给可用的从节点。
从节点:从节点会通过系统注册自己,以确保主节点看到它们可以执行任务,然后开始监听新任务。
客户端:负责创建新任务并等待系统的响应。

1.主节点角色
因为只有一个进程会成为主节点,所以一个进程成为Zookeeper的主节点后必须锁定管理权限。为此进程需要创建一个临时节点名为/master
create -e /master “master1.example.com:2223” //-e 表示创建的临时节点
其它节点再次创建会报错
这里创建备份主节点,可以用 stat /master true // stat 查看znode状态,true表示增加一个监视点,当活动的主节点崩溃时,我们会收到一个NodeDeleted事件,通过这个备份主节点重新创建create -e /master “master1.example.com:2223” 并成为新的主节点

2.从节点、任务、分配
设置监听点在/workers /tasks
ls /workers true
ls /tasks true

3.从节点角色
从节点通过在/workers创建临时znode来表示可执行任务
create -e /workers/worker1.example.com “worker1.example.com:2224
这里就会被主节点捕获。
然后从节点需要创建一个父znode/assign/worker1.example.com来接收任务分配
create -e /assign/worker1.example.com “”
ls /assign/worker1.example.com true //注册监听器

4.客户端角色
客户端向系统中添加任务,具体是什么任务不重要
create -s /tasks /task- “cmd” // create task-0000
ls /tasks /task-0000 true
//监听任务是否执行完毕,执行任务的节点会在/tasks /task-0000下创建节点

提交任务后,服务器收到tasks 下创建了任务,查询可用workers,然后分配任务(创建节点)到worker的assign下。工作节点收到通知,检查新任务确认,执行完成后,在task-0000下创建一个 status "done"的znode ,客户端收到通知,检查执行结果get …/staus查看结果

第三章
1.设置classpath
linux
ZOOBINDIR="<path_to_distro>/bin"
."$ZOOBINDIR"/zkEnv.sh
windows 使用 call命令调用而不是执行zkEnv.bat

2.建立Zookeeper会话
API围绕Zookeeper的句柄而构建,每个PAI调用都需要传递这个句柄,代表与Zookeeper之间的一个会话。建立的会话如果断开,只有会话还活着就会迁移到另外一台服务器,以保证会话存活

Zookeeper(String connectString,int sessionTimeout,Watcher watcher)
/*
connectString包含主机名和zk服务器端口
sessionTimeout以毫秒为单位,表示zk等待客户端通信的最长时间,之后会声明会话已死一般为5-10s
watcher 用于接收会话事件的一个对象,这个对象我们自己创建**Watcher为接口**客户端使用Watcher接
口来监控zk之间的会话健康情况,与zk服务器建立或失去连接就会产生事件,同样能用来监控zk数据的变
化,zk会话过去也会通过这个通知
*/
public interface  Watcher{
	void process(WatchEvent event);
}
public class Master implements Watcher{
	ZooKeeper zk;
	String hostPort;
	Master(String hostPort){
		this.hostPort=hostPort;
		}
		void startZK(){
			zk=new ZooKeeper(hostPort,15000,this);
		}
		public void  process(WatchEvent e){
			System.out.println(e)
		}
		public static void main(String[] args){
			Master m = new Master(args[0]);
			m.startZK();
			Thread.sleep(60000);
		}
}
/*
不要自己去管理zk客户端连接,客户端库会监控与服务之间的连接,不仅会告诉我们连接发生问题,还会
主动尝试重新建立通信,一般客户端会很快重建会话,,以便最小化应用的影响所以不要关闭会话后再启
动一个新的会话,这样会增加系统负载,导致更长时间中断。
可以使用zk.close()立刻销毁会话
*/

3.获取管理权限

String serverId = Integer.toHexString(random.nextInt());
void runForMaster(){
	zk.create("/master",                 //创建master节点来获取管理权限,成功则为master
                  serverId.getBytes(),        // znode存储的是字节数组
                  OPEN_ACL_UNSAFE,   //认证策略,权限管理,用于限制某个用户对某个znode的哪些权限
                  CreateMode.EPHEMRAL);  //这里为所有权限打开,    ephemral 临时节点
}
create 方法会抛出两种异常  KeeperException ,InterruptedException我们需要确保处理了这两种异常
特别是connectionLossException(KeeperException的子类)和InterruptedException,对于其他异常可以
忽略,但是对于这两种异常,create方法可能已经成功了,所以需要处理。
connectionLossException出现于客户端与zk服务端失去连接, 常由于网络原因,客户端会尝试重连,
但是进程必须知道一个未决请求是否已经成功处理,还是重新发送请求。
InterruptedException源于客户端调用了Thread.interrupt();通常因为应用程序部分关闭,但是还在被
其它相关应用使用,进程会中断本地客户端的请求处理的过程,并使请求处于未知状态。
因此**处理这两种异常之前**必须知道系统的状态,如果发生群首选举,在我们没有确定情况之前,我们
不希望确定主节点。若create成功,活动主节点死亡之前,没有任何进程能够成为主节点,如果活动主
节点还不知道自己已经获得了管理权,不会有任何进程成为主节点进程。

处理connectionLossException时,我们需要找到哪个进程创建的/master节点,如果是自己就开始成为
群首角色  使用   byte[] getData(String path,//获取znode节点路径
                           boolean watch,//表示是否要监听后续变更
                            Stat stat);         //填充znode节点的元数据信息
//runFoMaster 方法中引入异常处理
String serverId = Integer.toString(random.nextLong());
boolean isLeader = false;
boolean checkMaster(){
	while(true){
		try{
			Stat stat = new Stat();
			byte data[] = zk.getData("/master",false,stat);
			isLeader = new String(data).equals(serverId );
			return true;
		}catch(NoNodeException e){
			//no master,so try create again
			return false;
		}catch(connectionLossException e){
		}
	}
}
void runForMaster()throws InterruptedException{
	while(true){
		try{
			zk.create("/master",        
				serverId.getBytes(),      
				OPEN_ACL_UNSAFE, 
				CreateMode.EPHEMRAL);
			isLeader=true;
			break;
		}catch(NoNodeException e){
			isLeader=false;
			break;
		}catch(connectionLossException e){
		}
		if(checkMaster()){
			break;
		}
	}
}

此时master的主函数
		public static void main(String[] args){
			Master m = new Master(args[0]);
			m.startZK();
			m.runForMaster();
			if(isLeader){
				System.out.println("i am leader");
				Thread.sleep(60000);
			}
		   m.stopZK();
		}

其中InterruptedException处理依赖于程序上下文环境,如果向上抛出InterruptedException异常,最终
关闭zk句柄,我们可以抛出异常到中栈顶,当句柄关闭的时候就清理所有一切。
如果zk句柄未关闭,在重新抛出异常之前,首先要弄清楚自己是不是主节点,或继续异步执行后续操作。

3.1异步获取管理权
void create(
String path,
byte[] data,
List acl, //权限
CreateMode cm,
AsyncCallback.StringCallback cb, //提供回调方法的对象
Object ctx // 用户指定上下文信息(回调方法调用是传入的对象实例)
)//不会抛出异常,回调对象实现只有一个方法的StringCallback接口
void processResult(
int rc , //返回调用的结构,,返回OK或者KeeperException异常对应的编码值
String path ,//传递给create的path值
Object ctx ,//传递给create的ctx
String name //创建节点 的name
);//目前调用成功后,path与name值一样,但是当采用 CreateMode.SEQUENTIAL模式,两者不同。

static boolean isLeader;
static StringCallback masterCreateCallback = new StringCallback(){
	void processResult(int rc , String path ,Object ctx , String name){
		switch(Code.get(rc)){
			case CONNECTIONLOSS:
					checkmaster();
					return;
			case OK:
					isLeader = true;
					break;
			default:
					isLeader = false;
		}
		System.out.println("i am"+(isLeader?" ":"not")+"the leader");
	}
};
void runForMaster(){
	zk.create("/master",serverId.getBytes(),OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL,
	masterCreateCallback, null );
}

//连接丢失的时候我们需要检测系统状态,并判定如何恢复
void checkMaster(){
	zk.getData("/master",false,masterCheckCallback,null);
}
DataCallback masterCheckCallback = new DataCallback (){
	void processResult(int rc , String path ,Object ctx , Stat stat){
		switch(Code.get(rc)){
			case CONNECTIONLOSS:
					checkmaster();
					return;
			case NONODE:
					runForMaster();
					return;
		}
	}
};

设置元数据
void createParent(String path , byte[] data){
	zk.create(path,data,Ids.OPEN_ACL_UNSAFE,cb,data);//表示callback中可以使用data
}
StringCallback cb = new StringCallback(){
	void processResult(int rc , String path ,Object ctx , String name){
		switch(Code.get(rc)){
			case CONNECTIONLOSS : 
			         createParent(path,(byte[])ctx );
			         break;
			 case OK:
			 		LOG.info("Parent created");
			 		break;
			 case NODEEXISTS:
			 		LOG.warn(" Parent already registered :"+path);
			 			break;
			 default:
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};

注册从节点   /workers    临时节点
public class Worker implements Watcher{
	private final Logger LOG = LoggerFactory.getLogger(Worker.class);
	Zookeeper zk;
	String hostPort;
	String serverId = Integer.toHexString(random.nextInt());
	Worker(String hostPort){
		this.hostPort = hostPort;
	}
	void startZK() throws IOException{
		zk = new Zookeeper(hostPort,15000.this);
	}
	public void process(WatchedEvent e){
		Log.info(e.toString(),+","+hostPort);
	}
	void register(){
		zk.create("/workers/worker-"+serverId,
						"Idle".getBytes(),
						Ids.OPEN_ACL_UNSAFE,
						CreateMode.EPHEMERAL,// temp
						cb,null
				);
	}
	StringCallback cb = new StringCallback(){
			void processResult(int rc , String path ,Object ctx , String name){
		switch(Code.get(rc)){
			case CONNECTIONLOSS : 
			         register();//这个进程是唯一创建znode的进程,连接丢失就应该重试
			         break;
			 case OK:
			 		LOG.info("register success!");
			 		break;
			 case NODEEXISTS:
			 		LOG.warn("already register"+serverId);
			 			break;
			 default:
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};
public static void main(String [] args){
	Worker w = new Worker(args[0]);
	w.startZK();
	w.register();
	Thread.Sleep(30000);
}
}

//设置状态
StatCallback scb = new StatCallback(){
		public void processResult(int rc , String path ,Object ctx , Stat stat){
			switch(Code.get(rc)){
				case CONNECTIONLOSS : 
				updateStatus((String)ctx); //
				return;
			}
		}
};
synchronized private void updateStatus(String status){
	if(status ==this.status){//检测连接丢失期间,状态变化的问题
		zk.setData("/workers/"+name,status.getBytes(),-1,scb,status);//执行无条件更新-1表示禁用
		版本检测
	}
}
public void setStatus(String status){
	this.status = status; //将status存到本地变量中失败了重试
	updateStatus(status);
}
//if(status ==this.status) 防止,从节点开始执行task1,设置状态为workingon1,然后客户端尝试
//使用setData来实现,此时遇到网络问题,客户端与zk连接断开,然后在cb之前任务完成了,
//从节点调用客户端库设置setData设置状态为Idle,然后cb执行,再次尝试设置状态为working,
//有如上检测以后就不会再执行设置状态为working了



任务队列化   /tasks  节点下添加子节点来表示从节点需要执行的命令
好处有:1.序列化指定了任务队列化的顺序 2.通过很少的工作为任务创建 基于序列号 的唯一路径
public class Client implements Watcher{
	Zookeeper zk;
	String hostPort;
	Client(String hostPort){this.hostPort= hostPort;}
		void startZK() throws IOException{
		zk = new Zookeeper(hostPort,15000.this);
		}
	String queueCommand(String command)throws KeeperException{
		while(true){
			try{
				String name = zk.create("/tasks/task-",
				                                       command.getBytes(),
				                                       OPEN_ACL_UNSAFE,
				                                       CreateMode.SEQUENTIAL
				                                        );
				return name;
			}catch(NodeExistsException e){
				throws new Exception(name+"already appears to be running");
			}catch(ConnectionLossException e){
/*
连接丢失的时候,最少执行一次的任务,多次执行重试没有什么影响
最多执行一次的任务,可以为每一个任务指定一个唯一的id(如会话id),并且编码到
znode节点中,在遇到连接丢失的异常的时候,只有在/tasks 下不存在以这个会话id命名的节点的时候才会重试命令
*/
			}
		}
	}
	void process(WatchedEvent e){
		sout(e);
	}
public static void main(String [] args){
	Client c = new Client (args[0]);
	c.start();
	String name = c.queueCommand(args[1]);
	sout("Created name:"+name);
}
}

第四章:处理状态变化
查询状态变化可以使用轮询,如主节点是否崩溃 如50ms,这样请求次数在从节点过多时就会消耗全部的请求量,由于崩溃情况很少发生,因此请求很多都是多余的,假设使用增加状态的轮询周期来减少对zook服务端的请求量,这里 通过设立一个 /master 节点来标记。

4.1单次触发器

  1. 事件
    表示一个znode节点执行了更新操作
  2. 监视点
    表示一个znode和事件类型 组成的 单次触发器(如:znode数据被赋值,被删除)
  3. 通知
    当一个监视点被一个事件触发的时候,就会产生一个通知,通知是注册了监视点的
    应用客户端收到的事件报告的消息。

客户端每个监视点与会话关联,会话过期,等待中的监视点会被删除
监视点可以跨不同服务端连接而保持,如:一个服务端崩溃,此时会连接另外的一个服务端,客户端会发送 未触发的监视点列表,在注册监视点的时候,服务端会检查已监视的znode节点在之前注册监视点之后是否已经变化,若变化,一个监视点的事件就会发给客户端,否则就在新的服务端注册监视点

单次触发会丢失数据,但是任何在接收通知与注册新监视点之间的变化情况,均可以通过读取zook的状态信息来获得。 ------- 一般来说多个事件使用一次通知更轻量化

设置监视点
WatchedEvent 包含
会话状态(KeeperState):disconnected,SyncConnected,AuthFailed,ConnectedReadOnly,
SaslAuthenticated,Expired。
事件类型(EventType):NodeCreated,NodeDeleted,NodeDataChanged,
NodeChildrenChanged,None。
//NodeCreated,NodeDeleted,NodeDataChanged涉及单个节点
//NodeChildrenChanged涉及znode的子节点
//None表示无事件发生,而是zook的会话状态发生了变化
如果事件类型不是None时,返回一个znode路径

监视点包含:
1.数据监视节点(exists,getData设置)
2.子监视节点(getChildren设置)
public byte[] getData(final String path,Watcher w,Stat stat);
public byte[] getData(String path,boolean watch,Stat stat);

Stat 包含znode节点的属性信息,如:上次更新的时间戳、znode子节点数等

zookeeper 监视点一旦设置便无法移除,除非 触发 或者 会话关闭过期

普遍模型
1.进行调用异步
2.实现回调对象,并且传入异步函数之中
3.如果操作需要设置监视点,实现一个Watcher对象,并传入调用函数之中
zk.exists("/myZnode",myWatcher,existCallback,null);

主节点通过创建 /master 节点来标记自己是主节点 ,这里其它替代主节点需要在 /master 设置监视点 来实现 主节点崩溃的时候接管工作。

StringCallback masterCreateCallback = new StringCallback(){
	public void processResult(int rc , String path ,Object ctx , String name){
		switch(Code.get(rc)){
			case CONNECTIONLOSS : 
			         checkMaster();//连接丢失,客户端检测master节点是否存在,客户端并不知道能
			                                //否创建该节点
			         break;
			 case OK:
			 		state = MasterStates.ELECTED;
			 		takeLeadership();  //执行领导权
			 		break;
			 case NODEEXISTS:
			 		state = MasterStates.NOTELECTED;
			 		masterExists(); //若其它进程已经创建节点,那么久注册一个监视点
			 		break;
			 default:
			 		state = MasterStates.NOTELECTED;
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};
void masterExists(){
	zk.exists("/master",
                  masterExistWatcher, //设置监视点
                  masterExistCallback,
                  null
                  );
}
Watcher masterExistWatcher = new Watcher(){
	public void process(WatchedEvent e){
		if(e.getType()==EventType.NodeDeleted){
			assert "/master".equals(e.getPath());
			runForMaster(); //若master宕机,那么重新竞选
		}
	}
};

StatCallback masterExistCallback = new StatCallback (){
		public void processResult(int rc , String path ,Object ctx , Stat stat){
			switch(Code.get(rc)){
				case CONNECTIONLOSS : //设置监视点失败重试
					masterExists();
					break;
				case OK://设置成功,当节点不存在时,stat为空,因此需要选举master
					if(stat==null){//这里是在create的回调方法执行和exists操作执行之间发生了
						state = MasterStates.RUNNING;///master删除的情况,因此需要检测stat是否为空
						runForMaster();
					}
					break;
				default://异常情况,需要检测master是否存在
					checkMaster();
					break;
			}
		}
};


节点
Watcher workersChangeWatcher = new Watcher(){// 从节点列表的监视点对象
	public void process(WatchedEvent e){
		if(e.getType()==EventType.NodeChildrenChanged){
			assert "/workers".equals(e.getPath());
			getWorkers();
		}
	}
};
void getWorkers(){
	zk.getChildren("/workers",workersChangeWatcher,workersGetChildrenCallback,null);
}
ChildrenCallback workersGetChildrenCallback = new ChildrenCallback(){
	public void processResult(int rc,String path,Object ctx,List<String> children){
		switch(Code.get(rc)){
			case CONNECTIONLOSS :
				getWorkers();//重新获取子节点并且设置监视节点操作
				break;
			case OK:
				LOG.info("success get a list of workers:"+children.size()+"workers");
				ressignAndSet(children);//重新分配崩溃从节点的任务,并且设置新的从节点列表。
				break;
			default:
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};


ChildrenCache workerCache;//用于保存上次获得的从节点列表缓存
void ressignAndSet(List<String> children){
	List<String> toProcess;
	if(workerCache==null){
		workerCache = new ChildrenCache(children);//第一次使用则初始化
		toProcess = null;
	}else{
		LOG.info("removing and setting");
		toProcess = workerCache.removedAndSet(children);//检测是否有从节点已经被移除了
	}
	if(toProcess !=null){
		for(String worker: toProcess ){
			getAbsentWorkerTasks(worker);//若有从节点被移除了,就需要重新分配任务
		}
	}
}


---主节点等待新任务进行分配
Watcher taskChangeWatcher = new Watcher(){//任务列表变化的时候处理通知的监视点
	public void process(WatchedEvent e){
		if(e.getType()==EventType.NodeChildrenChanged){
			assert "/tasks".equals(e.getPath());
			getTasks();
		}
	}
};

void getTasks(){//获取任务列表,并且设置监视点
	zk.getChildren("/tasks",taskChangeWatcher,tasksGetChildrenCallback,null);
}

ChildrenCallback tasksGetChildrenCallback = new ChildrenCallback(){
	public void processResult(int rc,String path,Object ctx,List<String> children){
		switch(Code.get(rc)){
			case CONNECTIONLOSS :
				getTasks();//
				break;
			case OK:
				if(children!=null){
					assignTasks(children);//分配任务列表中的任务
				}
				break;
			default:
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};
void assignTasks(List<String> tasks){
	for(String task : tasks){
		getTaskData(task);
	}
}
void getTaskData(){//获取任务信息
	zk.getData("/task/"+task,false,taskDataCallback,task);
}
DataCallback taskDataCallback = new  DataCallback (){
	void processResult(int rc , String path ,Object ctx , Stat stat){
		switch(Code.get(rc)){
			case CONNECTIONLOSS:
					getTaskData((String)ctx );
					break;
			case OK:
					int worker = rand.nextInt(workerList.size());//随机选取一个工作者,任务分配与之
					String designatedWorker = workerList.get(worker);
					String asignmentPath = "/assign/"+designatedWorker+"/"+(String)ctx;
					createAssignment(asignmentPath,data);
					break;
			default:
					LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};
void createAssignment(String path , byte[] data ){//创建分配任务节点,路径为/assign/worker-id/task-num
	zk.create(path,data,Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT
	,assignTaskCallback,data);
}
StringCallback assignTaskCallback = new StringCallback(){
	public void processResult(int rc , String path ,Object ctx , String name){
		switch(Code.get(rc)){
			case CONNECTIONLOSS : 
			         createAssignment;
			         break;
			 case OK:
			 		LOG.info("task assigned correctly:" +name);
			 		deleteTask(name.substring(name.lastIndexof("/")+1));//删除节点下任务节点
			 		break;
			 case NODEEXISTS:
			 		LOG.warn("Task already exists!");
			 		break;
			 default:
			 		state = MasterStates.NOTELECTED;
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};

---从节点等待分配新任务
void register(){
	zk.create("/workers/worker-"+serverid,new byte[0],
	                               Ids.OPEN_ACL_UNSAFE,
	                               CreateMode.EPHMERAL,
	                               createWorkerCallback,
	                               null
	                               );
}
StringCallback createWorkerCallback = new StringCallback(){
	public void processResult(int rc , String path ,Object ctx , String name){
		switch(Code.get(rc)){
			case CONNECTIONLOSS : 
			        register();
			         break;
			 case OK:
			 		LOG.info("register success:" +serverid);
			 		break;
			 case NODEEXISTS:
			 		LOG.warn("already register !");
			 		break;
			 default:
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};
Watcher newTaskWatcher = new Watcher(){
	public void process(WatchedEvent e){
		if(e.getType()==EventType.NodeChildrenChanged){
		assert new String("/assign/worker-"+serverId).equals(e.getPath());
		getTask();//收到子节点变化通知以后,获得子节点列表
		}
	}
};
void getTask(){//获取子节点列表,并且重新设置监视点
	zk.getChildren("/assign/worker-"+serverId,newTaskWatcher ,taskGetChildrenCallback,
	null);
}
ChildrenCallback taskGetChildrenCallback = new ChildrenCallback(){
	public void processResult(int rc,String path,Object ctx,List<String> children){
		switch(Code.get(rc)){
			case CONNECTIONLOSS :
				getTasks();//
				break;
			case OK:
				if(children!=null){
					executor.execute(new Runnable(){//单独线程中执行
						List<String> children;
						DataCallback cb;
						public init(	List<String> children,DataCallback cb){
							this.children=children;
							this.cb=cb;
							return this;
						}
						public void run(){
							LOG.info("Looping into task");
							synchronized(onGoingTasks){
								for(String task:children){//循环子节点列表
									if(!onGoingTasks.contains(task)){
										LOG.trace("new task:{}",task);
										zk.getData("/assign/worker-"+serverId+"/"+task,
										                     false,cb,task);//获取任务信息并且执行任务
										 onGoingTasks.add(task);//正在执行的任务添加到执行列表,避免多次执行
									}
								}
							}
						}
					}.init(children,taskDataCallback));
				}
				break;
			default:
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};


-----------------------------客户端等待任务执行结果
void submitTask(String task , TaskObject taskCtx){
	taskCtx.setTask(task);
	zk.create("/tasks/task-",task.getBytes(),
		                            Ids.OPEN_ACL_UNSAFE,
	                               CreateMode.PERSISTENT_SEQUENTIAL,
	                               createTaskCallback,
	                               taskCtx);//传递的上下文对象,为实现的task类的实例
}
StringCallback createTaskCallback= new StringCallback(){
	public void processResult(int rc , String path ,Object ctx , String name){
		switch(Code.get(rc)){
			case CONNECTIONLOSS : //连接丢失重试
			        submitTask((TaskObject )ctx.getTask(),(TaskObject )ctx );
			         break;
			 case OK:
			 		LOG.info("create task name:" +name);
			 		(TaskObject )ctx.setTaskName(name);
			 		watchStatus("/status/"+name.replace("/tasks/",""),ctx);//成功,设置一个监视点
			 		break;
			 default:
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
		}
	}
};
/*
创建有序节点的时候连接丢失,这里由于每次zook都分配一个序列号,对于连接断开的客户端
无法确认是否创建成功,尤其在并发(同一操作)的情况下,因此我们用serverid来标记唯一性。
*/
ConcurrentHashMap<String ,Object > ctxMap = new ConcurrentHashMap<String ,Object > ();
void watchStatus(String path,Object ctx){
	ctxMap.put(path,ctx);
	zk.exists(path,statusWatcher,existCallback,ctx);//客户端通过该方法传递上下文对象,当收到
	//状态节点的通知的时候,就可以修改这个表示任务的对象
}
Watcher statusWatcher = new Watcher(){//任务完成后回调
		public void process(WatchedEvent e){
		if(e.getType()==EventType.NodeCreated){
		assert e.getPath().contains("/status/task-");
			zk.getData(e.getPath(),false,getDataCallback,ctxMap.get(e.getPath()));
		}
	}
}
StatCallback existCallback = new StatCallback (){
		public void processResult(int rc , String path ,Object ctx , Stat stat){
			switch(Code.get(rc)){
				case CONNECTIONLOSS : //设置监视点失败重试
					watchStatus(path,ctx);
					break;
				case OK://
					if(stat!=null){//状态节点已经存在,因此客户端获取这个节点信息
						zk.getData(path ,false,getDataCallback,null);
					}
					break;
				case NONODE://不存在就忽略
						break;
				default://异常情况
			 		LOG.error("Something went wrong:"+
			 		KeeperException.create(Code.get(rc),path));
			 		break;
			}
		}
};

multiop 原子性执行多个Zookeeper的多个操作,要么都成功,要么都失败
create、delete、setData
Op deleteZnode(String z){
return Op.delete(z,-1);
}

List result = zk.multi(Iterrable ops);
//void multi(Iterrable ops,MultiCallback cb, Object ctx);
事务
Tansaction t = new Tansaction();
t.delete("/a/b",-1);
t.delete("/a",-1);
List result = t.commit();

Op.check("/a/b",stat.getVersion) 同list一起传入multi 版本不一致会执行失败

通过 监视点 来代替 显式缓存
客户端数量大的时候,显式缓存效率会很低,而且不同步问题严重
另外一种解决方案是:使消息通知队列化,会使用异步的方式进行消费

顺序的保障

  • 1.写操作顺序


…顺序一致性,例如服务端A先创建znode再删除,那么其他的服务端也保持同一顺序,但是并不需要同时执行这些操作,服务端集群的运行状态速度也不同

  • 2.读操作顺序

…Zookeeper客户端总是会观察到相同的更新顺序即使连接到不同的服务端,但是客户端可能是在不同时间观察到了更新
/*
隐蔽通道的例子,客户端1,2 服务端 1,2,3 1-1 2-2,客户端1在1创建了节点(服务端1,3同步数据,然后返回给客户端1说明创建节点成功),然后客户端1通过TCP直接通知客户端2说节点创建了,但是服务端2还没有将数据同步过来,仅服务端1有数据,此时客户端从服务端2获取不到数据,这种情况是由于客户端直接隐蔽通信引起的
PS:可以用 设置监视点解决
*/

  • 3.通知的顺序
    可以 创建一个znode 来标记 某一些数据无效
    –/config/1 …/config/n 这一片数据需要更新,且需要更新完毕以后才能被读取,
    那么可以创建/config/invalid 节点,其它需要读取config下数据(1-n)的客户端会监听该节点,直到该节点被删除,再去读取config下的1-n.
  • 4.避免羊群效应
    避免在一个节点上设置很多很多 监视点 ,这样会导致一瞬间服务器压力变大,
    获取锁这个可以优化为,每个客户端创建的都是序列化的节点,序号小的获取到锁!!!

避免创建过多的监视点,一个监视点会在服务端创建一个Watcher对象,100万个会占用0.3GB左右内存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值