zookeeper介绍

zookeeper

| 介绍

开源的分布式协调服务,雅虎创建,基于google chubby。

可以解决的问题

  • 数据的发布/订阅(配置中心:disconf)
  • 负载均衡(dubbo利用了zookeeper机制实现负载均衡)
  • 命名服务
  • master选举(kafka、hadoop、hbase)
  • 分布式队列
  • 分布式锁。

特性

  • 顺序一致性

  • 原子性

  • 可靠性

  • 实时性

    一旦一个事务被成功应用,客户端就能够立即从服务器端读取到事务变更后的最新数据状态;(zookeeper仅仅保证在一定时间内,近实时)

文件系统

数据模型和文件系统类似,每一个节点为称znode,zk中的最小数据单元,每个node上可以保存数据和挂载子节点。构成一个层次化的属性结构。

[外链图片转存失败(img-jI6h3nEF-1564466530214)(.assets/20141108213344_45.png)]

节点类型

  • 持久化节点
  • 持久化有序节点
  • 临时节点

临时节点的生命期和会话周期相同

  • 临时有序节点

存储数据大小:不要超过1M

命令

create [-s] [-e] path data

get path [watch]

set path data [version]

version表示锁的概念,乐观锁,数据库里面有一个version字段去控制数据的版本号

delete path [version]

必须从子节点开始删除,不会立即生效,有会话重试机制,过一段时间才会有

stat信息

名称说明
cversion子节点的版本号
dataVersion数据的版本号
aclVersion表示acl的版本号,修改节点权限
czxid节点被创建时的事务ID
mzxid节点最后一次被更新的事务ID
pzxid当前节点下的子节点最后一次被修改时的事务ID
ctime
mtime
ephemeralOwner创建临时节点时,会有一个sessionId
dataLength数据长度

Watcher特性

分布式数据发布/订阅。zk允许客户端向服务器注册一个watcher监听,当服务器端的节点触发指定事件(数据改变、删除、子目录节点增加删除等)的时候会触发watcher,服务端会向客户端发送一个事件通知。

watcher的通知是一次性的,一量触发一次通知后,该watcher就失效。

ACL

提供控制节点访问权限的功能,用于有效的保证zk中数据的安全性,避免误操作而导致出现重大事故。

| 集群

角色类型

角色说明
Leader接收所有Follower的提案请求并统一协调发起投票,负责与所有的Follower进行内部的数据交换(同步)
Follower直接为客户端服务并参与提案的投票,同时与Leader进行数据交换(同步)
Observer直接为客户端服务但并不参与提案投票,同时也与Leader进行数据交换(同步)

follower不接收写请求

| zk一致性协议 - zab工作原理

leader选举

三种选主算法:leaderElection/AuthFastLeaderElection/FastLeaderElection(默认)

FastLeaderElection
  • serverid: 在配置server集群时,给定服务器的标识id(myid)
  • zxid: 64位Long类型,高32位(Epoch,选举轮数)表示当前属于那个leader统治,低32位递增的事务id号,zxid值越大,表示数据越新
  • server的状态:Looking,Following,Observering,Leading
选举流程
  1. 状态设置为LOOKING,初始化内部投票Vote(id, zxid),将其广播到其它节点;首次投票都是自己作为Leader;然后循环等待其它节点的投票信息;
  2. 每收到一个Vote,都和自己的Vote数据PK,规则为ZXID大的优先,相等时给ID大的投票。若外部投票获胜,将该选票覆盖自己的Vote后再次广播出去;同时统计是否有过半的赞同者与自己的投票数据一致,无则继续等待Vote,有则需要判断Leader是否在赞同者之中,在则退出循环,选举结束,根据选举结果及各自角色切换状态。

每一次启动时初始化为Looking

[外链图片转存失败(img-oj3BwLk1-1564466530215)(.assets/20181129114824253-1564408184881.png)]

假设这些服务器从id1-5,依序启动:

因为一共5台服务器,只有超过半数以上,即最少启动3台服务器,集群才能正常工作。

(1)服务器1启动,发起一次选举。

服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成;
服务器1状态保持为LOOKING;

(2)服务器2启动,再发起一次选举。

服务器1和2分别投自己一票,此时服务器1发现服务器2的id比自己大,更改选票投给服务器2;
此时服务器1票数0票,服务器2票数2票,不够半数以上(3票),选举无法完成;
服务器1,2状态保持LOOKING;

(3)服务器3启动,发起一次选举。

与上面过程一样,服务器1和2先投自己一票,然后因为服务器3id最大,两者更改选票投给为服务器3;
此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数(3票),服务器3当选Leader。
服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;

(4)服务器4启动,发起一次选举。

此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。
此时服务器4服从多数,更改选票信息为服务器3;
服务器4并更改状态为FOLLOWING;

(5)服务器5启动,同4一样投票给3,此时服务器3一共5票,服务器5为0票;

服务器5并更改状态为FOLLOWING;

最终Leader是服务器3,状态为LEADING;

其余服务器是Follower,状态为FOLLOWING。

选主后的数据同步

二阶提交:leader生成提议并广播给followers,收到半数以上的ACK后,再广播commit消息,同时将事务操作应用到内存中。follower收到提议后先将事务写到本地事务日志,然后反馈ACK,等接到leader的commit消息时,才会将事务操作应用到内存中。

问题

假设一个事务P1在leader服务器被提交了,并且已经有过半的follower返回了ack。 在leader节点把commit消息发送给folower机器之前leader服务器挂了怎么办?

新生成的Leader之前是follower,未收到commit消息,内存中是没有P1数据,ZAB协议保证选主后,P1是需要应用到集群中的。即通过选主后的数据同步来弥补。

已被处理的消息不能丢

消息在Leader上Commit了,但是其它Server还没有收到Commit消息已经挂了,为了实现已经被处理的消息不能丢,Zab使用了以下策略:

  1. 选举拥有zxid最大的节点作为新的leader;由于所有Proposal需要过半节点ACK,必须有一个节点保存了所有被COMMIT消息的Proposal状态;
  2. 新leader将自己事务日志中的proposal但未commit的消息处理;
  3. 新laeder与follower建立先进先出队列,先将自身有而follower没有的proposal发送给follower,再将这些proposal的commit命令发送给follower,以保证所有的 follower都保存了所有的proposal并已处理。
被丢弃的消息不能再次出现

场景:当leader接收到消息请求生成proposal后就挂了,其它follower并没有收到此proposal,重新选了leader后,这条消息是被跳过的。之前的leader重新启动成了follower,保留的被跳过的proposal状态,与整个系统状态不一致,需要删除。

Zab通过巧妙的设计zxid来实现这一目的,高32 epoch表示leader选举的轮数,每选一次,epoch+1,低32位是消息计数器,每接收到一条消息,这个值+1,新leader选举后这个值重置为0。这样,旧的leader挂了后重启,它不会被选举为leader,因为此时它的zxid肯定小于当前新的leader,当旧的leader作为follwer接入新leader后,新leader会让它将所有的拥有旧的epoch号的未被commit的proposal清除

zab协议,一定需要保证已经被leader提交的事务也能够被所有follower提交

zab协议需要保证,在崩溃恢复过程中跳过哪些已经被丢弃的事务

事务操作

二阶提交,针对client的请求,leader服务器会为其生成对应的事务proposal,并将其发送给其它follower,然后收集各自的选票,最后进行事务提交。

[外链图片转存失败(img-rl2lu3Om-1564466530215)(.assets/1523632294541695.png)]

二阶提交过程中,移除了中断逻辑(事务回滚),所有follower要么正常反馈,要么抛弃。follower处理proposal后的处理很简单,写入事务日志,然后立马反馈ACK给leader,即是说如果不是网络,内存或磁盘问题,follower肯定会定入成功,并正常反馈ACK。Leader收到过半FollowerACK后,会广播Commit消息给所有learner,并将事务应用到内存;Learner收到commit消息后会将事务应用到内存

什么情况下zab协议会进入崩溃恢复模式

  • 当服务器启动时
  • 当leader服务器出现网络中断、崩溃或者重启的情况
  • 集群中已经不存在过半的服务器与该leader保持正常通信

zab协议进入崩溃恢复模式会做什么

  • 当leader出现问题,zab协议进入崩溃恢复模式,并且选举出新的leader。当新的leader选举出来以后,如果集群中已经有过半机器完成了leader服务器的状态同(数据同步),退出崩溃恢复,进入消息广播模式
  • 当新的机器加入到集群中的时候,如果已经存在leader服务器,那么新加入的服务器就会自觉进入数据恢复模式,找到leader进行数据同步

[外链图片转存失败(img-JUvIJkpS-1564466530215)(.assets/1564408409704.png)]

| 安装

单机安装

http://apache.fayea.com/zookeeper中下载

zkCli.sh -server ip:port
zoo.cfg配置说明
tickTime:时间单位,默认值是2000ms
initLimit:10*2000,leader服务器等待follow启动并完成同步的时间
syncLimit:5*2000,leader节点和follower节点进行心跳检测的最大延时时间
dataDir:zk服务器存储快照文件的目录
clientPort:客户端访问的端口
dataLogDir:表示配置zk事务日志的存储路径,默认在dataDir目录下

集群安装

[外链图片转存失败(img-tQ4V5Qtu-1564466530216)(.assets/1564197131345.png)]

第三步:启动每个zookeeper

如何增加observer节点

zoo.cfg中 增加 peerType=observer

server.1=192.168.11.129:2888:3181  
server.2=192.168.11.135:2888:3181   
server.3=192.168.11.136:2888:3181:observer

三个端口:

2181: 对Client端提供服务

2888:集群内机器通讯使用

3888:选举leader,leader挂掉时使用

使用docker安装,直接使用zookeeper:latest版本,最新版本为3.5.5

使用docker-compose启动zk集群

官方安装文档

version: '3.1'

services:
  zoo1:
    image: zookeeper
    restart: always
    container_name: zoo1
    ports:
      - 2181:2181
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
    networks:
      
  zoo2:
    image: zookeeper
    restart: always
    container_name: zoo2
    ports:
      - 2182:2181
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=0.0.0.0:2888:3888;2181 server.3=zoo3:2888:3888;2181

  zoo3:
    image: zookeeper
    restart: always
    container_name: zoo3
    ports:
      - 2183:2181
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=0.0.0.0:2888:3888;2181

docker-compose up -d

[外链图片转存失败(img-zqNFwUNn-1564466530216)(.assets/1563270840302.png)]

分别将本地的2181、2182、2183端口映射到对应容器的2181端口上。

ZOO_MY_ID:表示zk服务的id

ZOO_SERVERS:表示zk集群的主机列表

查看各个节点的状态

[root@ceos03 zookeeper-svc]# docker exec -it zoo1 zkServer.sh status

ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

[root@ceos03 zookeeper-svc]# docker exec -it zoo2 zkServer.sh status

ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

[root@ceos03 zookeeper-svc]# docker exec -it zoo3 zkServer.sh status

ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader

| 应用 - 分布式锁

锁分为两类,一类是保持独占,另一种是控制时序。

独占锁

所有client去创建同一个节点,如/Lock,最终成功创建的那个client拥有了这把锁,用完成之后再删除。

控制时序

所有client在/LOCKs下创建临时有序节点,编号最小的获得锁,用完删除,其它client依次使用,未获得锁的监控相临较小编号节点即可。

控制时序代码实现

  1. 导入jar包

查找zk包的版本,http://www.mvnrepository.com/search?q=zookeeper

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.5.5</version>
</dependency>
  1. zk连接
public class ZkClient {

    private final static String CONNECTSTRING="192.168.10.13:2181";

    private static int sessionTimeout=5000;

    //获取连接
    public static ZooKeeper getInstance() throws IOException, InterruptedException {

        final CountDownLatch conectStatus=new CountDownLatch(1);

        ZooKeeper zooKeeper=new ZooKeeper(CONNECTSTRING, sessionTimeout, new Watcher() {
            public void process(WatchedEvent event) {
                if(event.getState()== Event.KeeperState.SyncConnected){
                    conectStatus.countDown();
                }
            }
        });

        conectStatus.await();
        return zooKeeper;
    }

    public static int getSessionTimeout() {
        return sessionTimeout;
    }
}
  1. 分布式锁实现
public class DistributeLock {

    private static final String ROOT_LOCKS="/LOCKS";//根节点

    private ZooKeeper zooKeeper;

    private int sessionTimeout; //会话超时时间

    private String lockID; //记录锁节点id

    private final static byte[] data={1,2}; //节点的数据

    private CountDownLatch countDownLatch=new CountDownLatch(1);

    public DistributeLock() throws IOException, InterruptedException {
        this.zooKeeper=ZookeeperClient.getInstance();
        this.sessionTimeout=ZookeeperClient.getSessionTimeout();
    }

    //获取锁的方法
    public boolean lock(){
        try {
            //1. create LOCKS/00000001
            lockID=zooKeeper.create(ROOT_LOCKS+"/",data, ZooDefs.Ids.
                    OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(Thread.currentThread().getName()+"->成功创建了lock节点["+lockID+"], 开始去竞争锁");
			//2. get all node
            List<String> childrenNodes=zooKeeper.getChildren(ROOT_LOCKS,true);//获取根节点下的所有子节点
            //3. sort and get minist
            SortedSet<String> sortedSet=new TreeSet<String>();
            for(String children:childrenNodes){
                sortedSet.add(ROOT_LOCKS+"/"+children);
            }
            String first=sortedSet.first(); //拿到最小的节点
            if(lockID.equals(first)){
                //表示当前就是最小的节点
                System.out.println(Thread.currentThread().getName()+"->成功获得锁,lock节点为:["+lockID+"]");
                return true;
            }
            SortedSet<String> lessThanLockId=sortedSet.headSet(lockID);
            // 4. watch close less No. node
            if(!lessThanLockId.isEmpty()){
                String prevLockID=lessThanLockId.last();//拿到比当前LOCKID这个几点更小的上一个节点
                zooKeeper.exists(prevLockID,new LockWatcher(countDownLatch));
                countDownLatch.await(sessionTimeout, TimeUnit.MILLISECONDS);
                //上面这段代码意味着如果会话超时或者节点被删除(释放)了
                System.out.println(Thread.currentThread().getName()+" 成功获取锁:["+lockID+"]");
            }
            return true;
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    public boolean unlock(){
        System.out.println(Thread.currentThread().getName()+"->开始释放锁:["+lockID+"]");
        try {
            zooKeeper.delete(lockID,-1);
            System.out.println("节点["+lockID+"]成功被删除");
            return true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
        return false;
    }
}
  1. main
    public static void main(String[] args) {
        final CountDownLatch latch=new CountDownLatch(10);
        Random random=new Random();
        for(int i=0;i<10;i++){
            new Thread(()->{
                DistributeLock lock=null;
                try {
                    lock=new DistributeLock();
                    latch.countDown();
                    latch.await();
                    lock.lock();
                    Thread.sleep(random.nextInt(500));
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    if(lock!=null){
                        lock.unlock();
                    }
                }
            }).start();
        }
    }

| 应用 - master选举

master-slave模式

资源:https://www.cnblogs.com/sky-sql/p/6804467.html

[外链图片转存失败(img-6zPbMhnm-1564466530216)(.assets/393620-20160627154234109-1109968833.png)]

zookeeper进行master选举使用场景是什么?

分布式,Master往往用来协调集群中的其他系统单元,具有对分布式状态变更的决定权。如:Master负责处理一些复杂的逻辑,并将结果同步给集群中其他系统单元。

多个client同时去创建相同的节点,创建成功的即是master;

创建失败的需要获取节点上的数据,即具体的master信息;

代码实现

1. jar依赖

    <dependency>
      <groupId>org.apache.zookeeper</groupId>
      <artifactId>zookeeper</artifactId>
      <version>3.5.5</version>
    </dependency>
    <dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.11</version>
    </dependency>

2. MasterSelector

package com.wjg.master_slave;

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MasterSelector {

    private ZkClient zkClient;

    private final static String MASTER_PATH = "/master"; //需要争抢的节点

    private IZkDataListener dataListener; //注册节点内容变化

    private UserCenter server;  //其他服务器

    private UserCenter master;  //master节点

    private boolean isRunning = false;

    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

    public MasterSelector(UserCenter server, ZkClient zkClient) {

        System.out.println("[" + server + "] 去争抢master权限");
        this.server = server;
        this.zkClient = zkClient;

        this.dataListener = new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {

            }

            @Override
            public void handleDataDeleted(String s) throws Exception {
                //节点如果被删除, 发起选主操作,这里可以处理,判断之前的master是否为本机,若是,则立即进行选举,否则 ,等待5s再进行选举
                if(master.getMc_id() != server.getMc_id())
                    TimeUnit.SECONDS.sleep(4);
                chooseMaster();

            }
        };
    }

    public void start() {
        //开始选举
        if (!isRunning) {
            isRunning = true;
            zkClient.subscribeDataChanges(MASTER_PATH, dataListener); //注册节点事件
            chooseMaster();
        }
    }


    public void stop() {
        //停止
        if (isRunning) {
            isRunning = false;
            scheduledExecutorService.shutdown();
            zkClient.unsubscribeDataChanges(MASTER_PATH, dataListener);
            releaseMaster();
        }
    }


    //具体选master的实现逻辑
    private void chooseMaster() {
        if (!isRunning) {
            System.out.println("当前服务没有启动");
            return;
        }
        try {
            zkClient.createEphemeral(MASTER_PATH, server);
            master = server; //把server节点赋值给master
            System.out.println(master + "->我现在已经是master,你们要听我的");

            //定时器
            //master释放(master 出现故障),5秒后释放一次
            scheduledExecutorService.schedule(() -> {
                releaseMaster();//释放锁
            }, 2, TimeUnit.SECONDS);
        } catch (ZkNodeExistsException e) {
            //表示master已经存在
            UserCenter userCenter = zkClient.readData(MASTER_PATH, true);
            if (userCenter == null) {
                System.out.println("启动操作:");
                chooseMaster(); //再次获取master
            } else {
                master = userCenter;
            }
        }
    }

    private void releaseMaster() {
        //释放锁(故障模拟过程)
        //判断当前是不是master,只有master才需要释放
        if (checkIsMaster()) {
            System.out.println(server+" release master!");
            zkClient.delete(MASTER_PATH); //删除
        }
    }


    private boolean checkIsMaster() {
        //判断当前的server是不是master
        UserCenter userCenter = zkClient.readData(MASTER_PATH);
        if (userCenter.getMc_name().equals(server.getMc_name())) {
            master = userCenter;
            return true;
        }
        return false;
    }

}

3. UserCenter

package com.wjg.master_slave;

import java.io.Serializable;

public class UserCenter implements Serializable {

    private static final long serialVersionUID = -1776114173857775665L;
    private int mc_id; //机器信息

    private String mc_name;//机器名称

    public int getMc_id() {
        return mc_id;
    }

    public void setMc_id(int mc_id) {
        this.mc_id = mc_id;
    }

    public String getMc_name() {
        return mc_name;
    }

    public void setMc_name(String mc_name) {
        this.mc_name = mc_name;
    }

    @Override
    public String toString() {
        return "UserCenter{" +
                "mc_id=" + mc_id +
                ", mc_name='" + mc_name + '\'' +
                '}';
    }
}

4. main

public class App 
{
    private final static String CONNECTSTRING="192.168.10.13:2181";


    public static void main(String[] args) throws IOException {

        List<MasterSelector> selectorLists = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread th = new Thread(() -> {
                System.out.println(id+" started!");
                ZkClient zkClient = new ZkClient(CONNECTSTRING, 5000,
                        10000,
                        new SerializableSerializer());

                UserCenter userCenter = new UserCenter();
                userCenter.setMc_id(id);
                userCenter.setMc_name("客户端:" + id);

                MasterSelector selector = new MasterSelector(userCenter, zkClient);
                selectorLists.add(selector);
                selector.start();//触发选举操作
            });
            th.start();
        }



        System.out.println("press any key to stop!");
        System.in.read();

        for (MasterSelector selector : selectorLists) {
            selector.stop();
        }


    }
}

说明:在实际生产环境中,可能会由于插拔网线等导致网络短时的不稳定,也就是网络抖动。由于正式生产环境中可能server在zk上注册的信息是比较多的,而且server的数量也是比较多的,那么每一次切换主机,每台server要同步的数据量(比如要获取谁是master,当前有哪些salve等信息,具体视业务不同而定)也是比较大的。那么我们希望,这种短时间的网络抖动最好不要影响我们的系统稳定,也就是最好选出来的master还是原来的机器,那么就可以避免发现master更换后,各个salve因为要同步数据等导致的zk数据网络风暴。所以在抢主的时候,如果之前主机是本机,则立即抢主,否则延迟5s抢主。这样就给原来主机预留出一定时间让其在新一轮选主中占据优势,从而利于环境稳定。

| 应用 - 分布队列

先进先出队列

  1. 通过getchildren获取指定根节点下的所有子节点,子节点就是任务
  2. 确定自己节点在子节点中的顺序
  3. 如果自己不是最小的节点,那么监控比自己小的上一个子节点,否则处于等待
  4. 接收watcher通知,重复流程
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值