zookeeper

1、zookeeper的集群数量为什么是奇数?
zk写操作,主向从同步的时候要超过半数成功才算成功。
如果集群有3台服务器,挂掉一台,还剩两台,可以继续写成功。
集群有4台服务器,最多也是只允许挂掉一台,如果挂掉两台,剩下的等于半数(没超过半数),写就会失败。即3、4台都只允许挂掉1台。
2、什么是zookeeper
zookeeper是一个分布式服务框架,主要用来解决分布式应用中的数据管理问题,比如:统一命名服务、状态同步服务、集群管理、数据库主从等。

zookeeper文件结构

image
/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真正处理数据。
到此一个客户端更新请求算是完成。
image

上述流程中,当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下创建响应文件。
image

使用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){...}防止一致卡在本线程导致阻塞下一个线程的问题。

image

/**
    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();

返回顶部

posted @ 2020-03-29 21:52  平淡454  阅读(301)  评论(0编辑  收藏  举报