第一章概念与基础
选举主节点、管理组内成员关系、管理元数据等
主从工作模式:主节点分配任务给从节点。获取主节点的过程就是获取锁的过程,即互斥排他锁。
竞争:两个进程不能同时处理工作的情况,一个必须等到另外一个
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 | ||
---|---|---|---|---|
- | /workers | foo.com:2181 | /workers/work-1 | |
- | /tasks | run cmd; | /tasks/task-1-2 | |
- | /assign | /assign/worker-1 | run 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单次触发器
- 事件
表示一个znode节点执行了更新操作 - 监视点
表示一个znode和事件类型 组成的 单次触发器(如:znode数据被赋值,被删除) - 通知
当一个监视点被一个事件触发的时候,就会产生一个通知,通知是注册了监视点的
应用客户端收到的事件报告的消息。
客户端每个监视点与会话关联,会话过期,等待中的监视点会被删除
监视点可以跨不同服务端连接而保持,如:一个服务端崩溃,此时会连接另外的一个服务端,客户端会发送 未触发的监视点列表,在注册监视点的时候,服务端会检查已监视的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左右内存