zookeeper
1、zookeeper的集群数量为什么是奇数?
zk写操作,主向从同步的时候要超过半数成功才算成功。
如果集群有3台服务器,挂掉一台,还剩两台,可以继续写成功。
集群有4台服务器,最多也是只允许挂掉一台,如果挂掉两台,剩下的等于半数(没超过半数),写就会失败。即3、4台都只允许挂掉1台。
2、什么是zookeeper
zookeeper是一个分布式服务框架,主要用来解决分布式应用中的数据管理问题,比如:统一命名服务、状态同步服务、集群管理、数据库主从等。
zookeeper文件结构
/workers : 其下的每个node代表集群中一个可用从节点信息。
/tasks : 其下每个node代表一个客户端请求命令。
/assign : 其下每个node代表一个已经同步到从节点的任务,如图表示给从节点worker-1分配了一个命令。
客户端写请求命令发送到首领节点,然后首领会在/tasks下创建一个node来存储命令,首领执行完后会将该任务下发到其他服务器(数据同步),同时会在/assign下创建一个node记录已经下发的任务。
zk节点类型
zk支持四种类型节点:持久有序、临时有序、持久、临时
持久的意思:客户端与zk断开连接后,该节点不会被删除
有序的意思:zk为每个节点增加一个递增证书序号
监视与通知
客户端可以监听服务端node的变化,当服务端节点数据发生变化,会主动通知到客户端,这样就不用客户端轮询查看客户端是否发生变化。
运行模式
zk支持两种模式:独立模式(一台服务器)和仲裁模式(主从)。
仲裁模式:客户端随机和任何一台服务器连接并且发出命令,各个服务器之间同步数据。
法定数量
每次客户端更新zk节点后,更新信息要同步到其他服务器上,如果要等到所有服务器同步完毕后在通知客户端,效率低下,所以可以设置一个数量,当同步完成服务器数量达到这个值后就回复客户端完成。 一般服务器数量都为奇数,而这个法定数量为服务器数量半数+1,也就是当服务器为5台时,同步完成达到3台时就回复客户端完成。
会话
每个客户端请求服务器都是先建立一个会话。
会话内的所有请求命令都是按照fifo队列顺序被服务端执行。
角色
zk有三种角色:
1、leader(群首):处理写请求,发起投票
2、follower(跟随着):接收处理读请求,写请求转发给leader
3、observer:observer和follower的区别是它不参与投票,因为如果参与投票的服务器太多,会使投票的过程变慢,所以加了observer,这样就可以横向增加observer服务器来增加客户端读的速度,而且还不减少投票的速度。
zxid
一次写请求代表一个事务,一个事务会生产一个zxid,它可以保障消息的有序性。
它是64位整数,前32位代表事务生成时的leader(也就是每个leader统治期内所有事务的zxid的前32位是相同的,全局唯一递增),后32位代表一个计数,每个leader统治期内唯一递增。
通过这种数据结构能判断出每条命令请求的先后顺序,保障zk的有序性。
生成时机:leader广播给follower之前生产。
zab协议(zk一致性协议)
zab协议贯穿整个zk的流程,为了使zk正常运作,zk实现了一些规则,这些规则可以理解成zab协议。
1、所有客户端的写请求操作都会被转发到leader来处理,leader会为每个请求生产一个事务,每个事务生成一个zxid,
2、然后发送给每一个follower(leader与每个follower有一个fifo队列),follower会回复ack给leader,
3、一旦leader收到ack数量超过半数(这里是与二阶段提交的区别,二阶段提交是要收到所有follower的ack),则leader会给每个follwer发送commit命令,follower真正处理数据。
到此一个客户端更新请求算是完成。
上述流程中,当leader崩溃恢复时有两个问题需要zab协议解决:
1、leader崩溃恢复后,要保证旧leader commit的事务被所有的服务器执行
zab协议保障:新leader一定包含最大的zxid。
2、leader崩溃恢复后,要保证旧leader未commit的事务不会被服务器执行
zab协议保障:就leader崩溃后可能马上恢复,参与新leader的选举流程,他有一些未提交的事务,zk就会禁止他参与新leader的选举流程
zab有两种模式:恢复模式(leader崩溃后进入恢复模式)和广播模式(leader正常运行时进入广播模式)。
首领选举
zookeeper默认采用fastLeaderElection算法选举首领。
logicClock(逻辑始终):每一轮投票都会对应一个全局自增唯一的id,防止多轮投票混。
每台服务器首轮投票默认选举自己为leader,然后将票发出去。
收到其他服务器的选票后:
1、首先比对logicClock,如果自己的过时了则更新自己的logicClock。
2、然后比对zxid,zxid最大的sever为选举的leader
3、然后比对serverid,zxid相同的情况比对serveri
4、然后把自己的票发送出去,值到有一个服务器支持者达到半数。
使用场景
1、命名服务
zk文件系统中文件都是唯一性的,命名服务通过这一点实现
2、配置中心
将应用的配置文件都存储在zk的某个目录节点中,然后应用的服务器监控这个节点,一旦发生变化,应用会马上知晓
3、集群
4、分布式锁
应用服务器通过在zk上创建节点,谁成功创建就相当于抢锁成功
下面是用zk实现主从的一个例子:
注意这个主从和zk内部自己的主从是两个概念,这里的主从是利用zk的节点唯一性来实现自己应用的选主。
场景
自己有一个集群应用,要实现主从,如果不用zk怎么实现?
可以这么做,自己实现一个选主的算法,选出一个主,然后所有的从节点向主节点送心跳,一旦心跳失败,则认为主挂了,重新选出一个主。这样实现起来较为复杂,需要解决的问题很多。
可以利用zk来实现。
同时zk还可以实现对不同的请求可以分配到不同的节点上。
手里有多台服务器,要通过zk来实现主从,那么这些服务器每台都可以看成是zk的一个客户端,他们作为客户端向zk发起各种命令来实现zk对他们主从的管理。
通过zk来实现一个主从大致思路:
首先是启动工作:
1、选举一个主节点,通过向zk创建/master节点,成功的就是主,失败的是从。
并且从会一直监听/master,随时准备主死后上位。
2、在/workers、/tasks、/assign目录下初始化各个从节点的目录
3、主节点监听/workers和/tasks,每当有新的从节点加入,workers会发生变化,主节点会监控到,然后会在/assign下创建响应目录。
从节点监听相应的assign,当有任务分配的时候,从节点会获取到具体任务来执行。
4、客户端请求的时候,在/tasks下创建相应的命令文件
5、主节点监控到/task是下变化,会分配给一个从节点(具体什么请求分配给什么节点可以自己实现方法),也就是在/assign下创建响应文件。
使用zk api
接下来使用zookeeper的api来实现上边的主从示例。
创建会话连接:
/**
connectString: 连接zookeeper的主机和端口信息,如:127.0.0.1:2182,127.0.0.1:2181
sessionTimeout: 超时时间,单位ms。一般设置为5-10秒。sessionTimeout=15s表示Zookeeper如果有15s无法和客户端通信,则会终止该会话。
watcher:Watcher对象(需自己实现Watcher接口),用于监控会话(如建立/失去连接,Zookeeper数据变化,会话过期等事件)
*/
Zookeeper(String connectString,int sessionTimeout, Watcher watcher)
实现一个Watcher:
public class Master implements Watcher {
@Override
public void process(WatchedEvent watchedEvent) {
// 监控到连接断开时,自己不要关闭会话后再启动一个新的会话,这样会增加系统负载,并导致更长时间的中断, Zookeeper客户端库会处理
System.out.println("监控: "+watchedEvent);
}
public static void main(String[] args) throws InterruptedException, IOException {
String connectString = "139.196.53.21:2182,139.196.53.21:2181";
// 注意:如果服务器发现请求的会话超时时间太长或太短,服务器会调整会话超时时间
int sessionTimeout = 10 * 1000;
Master obj = new Master();
ZooKeeper zooKeeper = new ZooKeeper(connectString, sessionTimeout, obj);
Thread.sleep(600000);
//客户端可以调用Zookeeper.close()方法主动结束会话. 否则即便客户端断开连接,会话也不会立即消失,服务端要等会话超时以后才结束会话
zooKeeper.close();
}
}
执行结果:
## 执行上述代码,启动客户端
2017-02-24 22:49:29,359 - INFO - [main:ZooKeeper@438] - Initiating client connection, connectString=139.196.53.21:2182,139.196.53.21:2181 sessionTimeout=10000 watcher=org.apache.zookeeper.book.luyunfei.Master@3e6fa38a
2017-02-24 22:49:29,399 - INFO - [main-SendThread(139.196.53.21:2182):ClientCnxn$SendThread@1032] - Opening socket connection to server 139.196.53.21/139.196.53.21:2182. Will not attempt to authenticate using SASL (unknown error)
2017-02-24 22:49:29,488 - INFO - [main-SendThread(139.196.53.21:2182):ClientCnxn$SendThread@876] - Socket connection established to 139.196.53.21/139.196.53.21:2182, initiating session
2017-02-24 22:49:29,649 - INFO - [main-SendThread(139.196.53.21:2182):ClientCnxn$SendThread@1299] - Session establishment complete on server 139.196.53.21/139.196.53.21:2182, sessionid = 0x25a7098967b0000, negotiated timeout = 10000
监控: WatchedEvent state:SyncConnected type:None path:null
## 此时关闭Zookeeper服务器(/tmp/z3/zookeeper-3.4.8/bin/zkServer.sh stop), 程序监控到Disconnected事件
2017-02-24 22:57:31,009 - INFO - [main-SendThread(139.196.53.21:2182):ClientCnxn$SendThread@1158] - Unable to read additional data from server sessionid 0x25a7098967b0000, likely server has closed socket, closing socket connection and attempting reconnect
监控: WatchedEvent state:Disconnected type:None path:null
....
....
## 此时又开启Zookeeper服务器(/tmp/z3/zookeeper-3.4.8/bin/zkServer.sh start), 客户端自动重新连接服务
2017-02-24 22:59:02,559 - INFO - [main-SendThread(139.196.53.21:2182):ClientCnxn$SendThread@1299] - Session establishment complete on server 139.196.53.21/139.196.53.21:2182, sessionid = 0x25a7098967b0000, negotiated timeout = 10000
监控: WatchedEvent state:SyncConnected type:None path:null
到现在主节点已经和zk建立会话连接了,接下来就是在zk上创建/master节点。
创建节点命令:
/**
String path: 节点path
byte[] data: 节点数据, 只能传入字节数组类型的数据. 如果没有数据,则可传new byte[0]
List<ACL> acl: ACL策略。例如
1. ZooDefs.Ids.OPEN_ACL_UNSAFE为所有人提供了所有权限(该策略在不可信环境下非常不安全)
2. 其它策略
CreateMode: 节点类型(枚举,如:临时节点,临时有序节点,持久节点,持久有序节点)
抛出两类异常:
1. KeeperException
1.1 ConnectionLossException: KeeperException异常的子类,发生于客户度与Zookeeper服务端失去连接时,通常由网络原因导致
注意:虽然Zookeeper会自己处理重建连接,但是我们必须知道未决请求的状态(是否已经处理/需重新请求)
2. InterruptedException:发生于客户端线程调用了Thread.interrupt, 通常是因为应用程序部分关闭,但还在其他相关应用的方法中使用
处理方式:1. 向上直接抛出异常,让程序最外层捕获,然后主动关闭zk句柄(zookeeper.stop()),然后做清理善后
2. 如果句柄没有关闭,则可能会有其它异步执行的后续操作,这种情况做清理善后会比较棘手
*/
create(String path, byte[] data, List<ACL> acl, CreateMode createMode) throws KeeperException, InterruptedException
创建/master:
String serverId = Integer.toHexString(new Random(this.hashCode()).nextInt());
boolean isLeader = false;
ZooKeeper zooKeeper;
// 创建zonde节点(申请成为主节点)
public void runForMaster() throws KeeperException, InterruptedException {
while (true) {
try {
// 创建/master节点,如果执行成功,本客户端将会成为主节点
zooKeeper.create(
"/master",
serverId.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL
);
} catch (KeeperException.NodeExistsException e) {
// 主节点已经存在了,不再申请成为主节点
isLeader = false;
break;
} catch (KeeperException.ConnectionLossException e) {
// 这里留空,以便在网络异常情况下,继续while循环来申请成为主节点
}
// 如果已经存在主节点,则跳出while循环,不再申请成为主节点
if (checkMaster()) break;
}
}
// 检查是否存在主节点
public boolean checkMaster() throws KeeperException, InterruptedException {
while (true) {
try {
Stat stat = new Stat();
// 通过获取/master节点数据,并和自身的serverId比较,来检查活动主节点
byte[] data = zooKeeper.getData("/master", false, stat);
isLeader = new String(data).equals(serverId);
// 已经存在主节点了,返回true,告知runForMaster()不要再进行while循环了
return true;
} catch (KeeperException.NoNodeException e) {
return false;
} catch (KeeperException.ConnectionLossException e) {
}
}
}
上边创建主节点是同步的方式,接下来看看异步方式,异步方式实现起来比同步方式要简单,因为没有while(true){...}防止一致卡在本线程导致阻塞下一个线程的问题。
/**
StringCallback: 提供回调方法的对象,它通过传入的上下文参数(Object ctx)来获取数据。它实现StringCallback接口
Object ctx: 用户指定上下文信息,最终会传入回调函数中
*/
void create(String path, byte[] data, List<ACL> acl, CreateMode createMode, StringCallback cb, Object ctx)
/**
以下演示了如何创建一个StringCallback回调方法的对象。
注意: 因为只有一个单独的线程处理所有回调调用,因此一个回调函数阻塞,会导致后续所有回调函数都阻塞。
所以要避免在回调函数里做集中操作(即while(true){...})或阻塞操作(如调用synchronized方法).以便其被快速处理
*/
AsyncCallback.StringCallback cb = new AsyncCallback.StringCallback() {
/**
rc : 返回码,0: 正常返回;其它表示对应的KeeperException异常
path: create()传入的参数,即znode路径
ctx: create()传入的参数,即上下文
name: znode节点最终的名称(一般path和那么值一样。单如果采用CreateMode.SEQUENTIAL有序模式,则name为最终名称)
*/
public void processResult(int rc, String path, Object ctx, String name) {
switch (Code.get(rc)) {
case CONNECTIONLOSS:
checkMaster();
break;
case OK:
state = MasterStates.ELECTED;
takeLeadership();
break;
case NODEEXISTS:
state = MasterStates.NOTELECTED;
masterExists();
break;
default:
state = MasterStates.NOTELECTED;
LOG.error("Something went wrong when running for master.",
KeeperException.create(Code.get(rc), path));
}
LOG.info("I'm " + (state == MasterStates.ELECTED ? "" : "not ") + "the leader " + serverId);
}
};
void checkMaster() {
zk.getData("/master", false, masterCheckCallback, null);
}
接下来要在zk中创建/tasks、/workers、/assign目录:
public void bootstrap(){
createParent("/workers", new byte[0]);
createParent("/assign", new byte[0]);
createParent("/tasks", new byte[0]);
createParent("/status", new byte[0]);//new byte[0] 表示传入空数据
}
void createParent(String path, byte[] data){
zk.create(path,
data,
Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT,
createParentCallback,
data);// A----这里将data作为上下文传入到回调函数中,以便在下面CONNECTIONLOSS时再递归调用createParent函数。
}
StringCallback createParentCallback = new StringCallback() {
public void processResult(int rc, String path, Object ctx, String name) {
switch (Code.get(rc)) {
case CONNECTIONLOSS:
/*
* Try again. Note that registering again is not a problem.
* If the znode has already been created, then we get a
* NODEEXISTS event back.
*/
createParent(path, (byte[]) ctx);//呼应A
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));
}
}
};
接下来注册从节点,最后就是写一个客户端模拟请求。
监听
监听是通过回调函数的形式发送通知给客户端,监听是会话级别有效,会话级别内可以跨服务器。
如何设置监视点:
public byte[] getData(final String path,Watcher watcher(自定义监视点),Stat stat)
public byte[] getData(String path,boolean watch(true=使用默认监视点),Stat stat)
// API中watcher的实现类
public class Master implements Watcher {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("监控: "+watchedEvent);
}
}
WatchedEvent 数据结构:
public class WatchedEvent {
private final KeeperState keeperState;// 会话状态,枚举类型: Disconnected,SyncConnected,AuthFailed,ConnectedReadOnly,SaslAuthenticated,Expired
private final EventType eventType;// 事件类型, 枚举: NodeCreated,NodeDeleted,NodeDataChanged,NodeChildrenChanged,None(无事件发生,而是Zookeeper的会话状态发生了变化)
private String path;//事件类型不是None时,返回一个znode路径
}
事务
Op deleteZnode(String z){
return Op.delete(z,-1);
}
List<OpResult> result = zk.multi(Arrays.asList(deleteZnode("/a/b"),deleteZnode("/a")));
Tansaction是对multi的封装:
Tansaction t = new Tansaction();
t.delete("/a/b",-1);
t.delete("/a",-1);
List result = t.commit();