Zookeeper学习记录

184 阅读17分钟

zookeeper

初识 Zookeeper

Zookeeper概念

Zookeeper 是 Apache Hadoop 项目下的一个子项目,是一个树形目录服务。Zookeeper 翻译过来就是 动物园管理员,他是用来管 Hadoop(大象)、Hive(蜜蜂)、Pig(小 猪)的管 理员。简称zk Zookeeper 是一个分布式的、开源的分布式应用程序的协调服务。 它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。

ZooKeeper 的架构通过冗余服务实现高可用性。

Zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。

一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

Zookeeper 提供的主要功能包括:

  • 配置管理
  • 分布式锁
  • 集群管理

zookeeper 数据结构

zookeeper 提供的名称空间非常类似于标准文件系统,key-value 的形式存储。名称 key 由斜线 / 分割的一系列路径元素,zookeeper 名称空间中的每个节点都是由一个路径标识。

CAP理论

CAP 理论指出对于一个分布式计算系统来说,不可能同时满足以下三点:

  • 一致性:在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性,等同于所有节点访问同一份最新的数据副本。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。
  • 可用性: 每次请求都能获取到正确的响应,但是不保证获取的数据为最新数据。
  • 分区容错性: 分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

在这三个基本需求中,最多只能同时满足其中的两项,P 是必须的,因此只能在 CP 和 AP 中选择,zookeeper 保证的是 CP,对比 spring cloud 系统中的注册中心 eruka 实现的是 AP。

BASE 理论

BASE 是 Basically Available(基本可用)、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。

  • 基本可用: 在分布式系统出现故障,允许损失部分可用性(服务降级、页面降级)。
  • 软状态: 允许分布式系统出现中间状态。而且中间状态不影响系统的可用性。这里的中间状态是指不同的 data replication(数据备份节点)之间的数据更新可以出现延时的最终一致性。
  • 最终一致性: data replications 经过一段时间达到一致性。

BASE 理论是对 CAP 中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

ZooKeeper 命令操作

Zookeeper命令操作数据模型

ZooKeeper 是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构。这里面的每一个节点都被称为: ZNode,每个节点上都会保存自己的数据和节点信息。节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下。

节点可以分为四大类:

  • PERSISTENT 持久化节点 :
  • EPHEMERAL 临时节点 : -e
  • PERSISTENT_SEQUENTIAL 持久化顺序节点 : -s
  • EPHEMERAL_SEQUENTIAL 临时顺序节点 :-es

Zookeeper命令操作服务端命令

  • 启动 ZooKeeper 服务: ./zkServer.sh start
  • 查看 ZooKeeper 服务状态: ./zkServer.sh status
  • 停止 ZooKeeper 服务: ./zkServer.sh stop
  • 重启 ZooKeeper 服务: ./zkServer.sh restart

image.png

Zookeeper客户端常用命令

连接ZooKeeper服务端

./zkCli.sh –server ip:port

断开连接

quit

查看帮助命令

help

显示指定目录下节点

ls 目录

创建节点

create /节点path value

获取节点值

get /节点path

设置节点值

set /节点path value

删除单个节点

delete /节点path

删除带有子节点的节点

deleteall /节点path

image.png

客户端命令-创建临时有序节点

创建临时节点

create -e /节点path value

创建顺序节点

create -s /节点path value

查询节点详细信息

ls -s /节点path

Znode 的状态属性

cZxid创建节点时的事务ID
ctime创建节点时的时间
mZxid最后修改节点时的事务ID
mtime最后修改节点时的时间
pZxid表示该节点的子节点列表最后一次修改的事务ID,添加子节点或删除子节点就会影响子节点列表,但是修改子节点的数据内容则不影响该ID (注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid)
cversion子节点版本号,子节点每次修改版本号加1
dataversion数据版本号,数据每次修改该版本号加1
aclversion权限版本号,权限每次修改该版本号加1
ephemeralOwner创建该临时节点的会话的sessionID。 * 如果该节点是持久节点,那么这个属性值为0)*
dataLength该节点的数据长度
numChildren该节点拥有子节点的数量 (只统计直接子节点的数量)

ZooKeeper JavaAPI 操作

常见的ZooKeeper Java API :

  • 原生Java API
  • ZkClient
  • Curator

实例代码

MAVEN

<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.8</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.0.0</version> </dependency>

客户端的 zookeeper 原生 API

使用 zookeeper 原生 API,因为连接需要时间,用 countDownLatch 阻塞,等待连接成功,控制台输出连接状态!

public static void main(String[] args) {
    try {
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        ZooKeeper zooKeeper = new ZooKeeper("172.26.130.169:2181", 4000, new Watcher() {
            public void process(WatchedEvent watchedEvent) {
                if (Event.KeeperState.SyncConnected == watchedEvent.getState()) {
                    // 如果收到了服务端的响应事件,连接成功
                    countDownLatch.countDown();
                }
            }
        });
        countDownLatch.await();
        //CONNECTED
        System.out.println(zooKeeper.getState());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

image.png 简单示例添加节点 API:

zooKeeper.create("/qstest","0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

在客户端可以看到已经添加成功 image.png

Curator介绍

Curator 是 Apache ZooKeeper 的Java客户端库。Curator 是 Netflix 公司开源的一套 zookeeper 客户端框架,解决了很多 Zookeeper 客户端非常底层的细节开发工作,包括连接重连、反复注册 Watcher 和 NodeExistsException 异常等。

Curator 项目的目标是简化 ZooKeeper 客户端的使用。Curator 最初是 Netfix 研发的,后来捐献了 Apache 基金会,目前是 Apache 的顶级项目。官网:curator.apache.org/

Curator 包含了几个包:

  • curator-framework:对 zookeeper 的底层 api 的一些封装。
  • curator-client:提供一些客户端的操作,例如重试策略等。
  • curator-recipes:封装了一些高级特性,如:Cache 事件监听、选举、分布式锁、分布式计数器、分布式 Barrier 等。

使用curator连接zookeeper

CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString("172.26.130.169:2181")
        .sessionTimeoutMs(4000)
        // 重试策略
        .retryPolicy(new ExponentialBackoffRetry(1000, 3))
        .namespace("")
        .build();
// 开始连接
curatorFramework.start();

创建节点

创建节点:create 持久 临时 顺序 数据

  • 基本创建 :create().forPath("")
  • 创建节点 带有数据:create().forPath("",data)
  • 设置节点的类型:create().withMode().forPath("",data)
  • 创建多级节点 /app1/p1 :create().creatingParentsIfNeeded().forPath("",data)
String path = "";
// 1. 基本创建
// 如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
path = curatorFramework.create().forPath("/app1");
System.out.println(path);
// 2. 创建节点 带有数据
// 如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
// 删除结点2
path = curatorFramework.create().forPath("/app2", "hehe".getBytes());
System.out.println(path);
// 3. 设置节点的类型
// 默认类型:持久化
path = curatorFramework.create().withMode(CreateMode.EPHEMERAL).forPath("/app3");
System.out.println(path);
// 4. 创建多级节点 /app1/p1
// creatingParentsIfNeeded():如果父节点不存在,则创建父节点
path = curatorFramework.create().creatingParentsIfNeeded().forPath("/app4/p1");
System.out.println(path);

查询节点

查询节点:

  1. 查询数据:get: getData().forPath()
  2. 查询子节点: ls: getChildren().forPath()
  3. 查询节点状态信息:ls -s:getData().storingStatIn(状态对象).forPath()
// 查询节点数据
//1. 查询数据:get
byte[] data = curatorFramework.getData().forPath("/app1");
System.out.println(new String(data));
// 2. 查询子节点: ls
List<String> pathList = curatorFramework.getChildren().forPath("/");
System.out.println(pathList);
//3. 查询节点状态信息:ls -s
Stat stat = new Stat();
curatorFramework.getData().storingStatIn(stat).forPath("/app1");
System.out.println(stat);

修改节点

  1. 基本修改数据:setData().forPath()
  2. 根据版本修改: setData().withVersion().forPath() version 是通过查询出来的。目的就是为了让其他客户端或者线程不干扰我。
// 修改数据
curatorFramework.setData().forPath("/app1","qs".getBytes());
//3. 查询节点状态信息:ls -s
curatorFramework.getData().storingStatIn(stat).forPath("/app1");
int version = stat.getVersion();//查询出来的 3
System.out.println(version);
curatorFramework.setData().withVersion(version).forPath("/app1", "hehe".getBytes());

删除结点

删除节点: delete deleteall

  1. 删除单个节点:delete().forPath("/app1");
  2. 删除带有子节点的节点:delete().deletingChildrenIfNeeded().forPath("/app1");
  3. 必须成功的删除:为了防止网络抖动。本质就是重试。client.delete().guaranteed().forPath("/app2");
  4. 回调:inBackground
// 删除结点
// 1. 删除单个节点
curatorFramework.delete().forPath("/app1");
//2. 删除带有子节点的节点
curatorFramework.delete().deletingChildrenIfNeeded().forPath("/app4");
//3. 必须成功的删除
curatorFramework.delete().guaranteed().forPath("/app2");
//4. 回调
curatorFramework.delete().guaranteed().inBackground(new BackgroundCallback(){
    @Override
    public void processResult(CuratorFramework client, CuratorEvent event)
            throws Exception {
        System.out.println("我被删除了~");
        System.out.println(event);
    }
}).forPath("/app1");

Watch监听

概述

ZooKeeper 允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。

ZooKeeper 中引入了Watcher机制来实现了发布/订阅功能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。

ZooKeeper 原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便 需要开发人员自己反复注册Watcher,比较繁琐。

Curator引入了 Cache 来实现对 ZooKeeper 服务端事件的监听。 ZooKeeper提供了三种Watcher:

  • NodeCache : 只是监听某一个特定的节点
  • PathChildrenCache : 监控一个ZNode的子节点.
  • TreeCache : 可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合

NodeCache

// 演示 NodeCache:给指定一个节点注册监听器
public class NodeCacheTest {
    public static void main(String[] args) {
        CuratorFramework client = CuratorFrameworkFactory.builder().connectString("172.26.130.169:2181")
                .sessionTimeoutMs(4000)
                // 重试策略
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .namespace("")
                .build();
        client.start();
        try {
            // 1.创建NodeCache对象
            final NodeCache nodeCache = new NodeCache(client, "/app1");
            // 2.注册监听
            nodeCache.getListenable().addListener(new NodeCacheListener() {
                @Override
                public void nodeChanged() throws Exception {
                    System.out.println("节点变化了~");
                    //获取修改节点后的数据
                    byte[] data = nodeCache.getCurrentData().getData();
                    System.out.println(new String(data));
                }
            });
            //3. 开启监听.如果设置为true,则开启监听是,加载缓冲数据
            nodeCache.start(true);
            while (true) {
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

PathChildrenCache

public class PathChildrenCacheTest {
    public static void main(String[] args) {
        CuratorFramework client = CuratorFrameworkFactory.builder().connectString("172.26.130.169:2181")
                .sessionTimeoutMs(4000)
                // 重试策略
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .namespace("")
                .build();
        client.start();
        try {
            // 1.创建 PathChildrenCache 对象
            final PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/app2",true);
            // 2.注册监听
            pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
                  @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
                      System.out.println("节点变化了~");
                      System.out.println(event);
                      //监听子节点的数据变更,并且拿到变更后的数据
                      //1.获取类型
                      PathChildrenCacheEvent.Type type = event.getType();
                      //2.判断类型是否是update
                      if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
                          System.out.println("数据变了!!!");
                          byte[] data = event.getData().getData();
                          System.out.println(new String(data));

                      }
                  }
            });
            //3. 开启监听.如果设置为true,则开启监听是,加载缓冲数据
            pathChildrenCache.start(true);
            while (true) {
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TreeCache

// 演示 TreeCache:监听某个节点自己和所有子节点们
public class TreeCacheTest {
    public static void main(String[] args) {
        CuratorFramework client = CuratorFrameworkFactory.builder().connectString("172.26.130.169:2181")
                .sessionTimeoutMs(4000)
                // 重试策略
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .namespace("")
                .build();
        client.start();
        try {
            // 1. 创建监听器
            TreeCache treeCache = new TreeCache(client,"/app2");
            // 2. 注册监听
            treeCache.getListenable().addListener(new TreeCacheListener() {
                @Override
                public void childEvent(CuratorFramework client, TreeCacheEvent event)
                        throws Exception {
                    System.out.println("节点变化了");
                    System.out.println(event);
                }
            });
            // 3. 开启监听
            treeCache.start();
            while (true){
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Zookeeper watcher 事件机制原理剖析

客户端发送请求给服务端是通过 TCP 长连接建立网络通道,底层默认是通过 java 的 NIO 方式,也可以配置 netty 实现方式。

image.png 注册 watcher 监听事件流程图:

image.png

客户端发送事件通知请求

在 Zookeeper 类调用 exists 方法时候,把创建事件监听封装到 request 对象中,watch 属性设置为 true,待服务端返回 response 后把监听事件封装到客户端的 ZKWatchManager 类中。

image.png

服务端处理 watcher 事件的请求

服务端 NIOServerCnxn 类用来处理客户端发送过来的请求,最终调用到 FinalRequestProcessor,其中有一段源码添加客户端发送过来的 watcher 事件: image.png 然后进入 statNode 方法,在 DataTree 类方法中添加 watcher 事件,并保存至 WatchManager 的 watchTable 与 watchTable 中。

image.png

image.png

服务端触发 watcher 事件流程

若服务端某个被监听的节点发生事务请求,服务端处理请求过程中调用FinalRequestProcessor 类 processRequest 方法中的代码如下所示:

image.png image.png 删除调用链最终到 DataTree 类中删除节点分支的触发代码段:

image.png image.png 进入 WatchManager 类的 triggerWatch 方法:

image.png image.png 继续跟踪进入 NIOServerCnxn,构建了一个 xid 为 -1,zxid 为 -1 的 ReplyHeader 对象,然后再调用 sendResonpe 方法。

image.png

Zookeeper分布式锁-概念

在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决 多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题。

但当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线 程的锁解决同步问题。

那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题——这就是分布式锁。

image.png

zookeeper分布式锁原理

image.png

排他锁和共享锁

排他锁

排他锁(Exclusive Locks),又被称为写锁或独占锁,如果事务T1对数据对象O1加上排他锁,那么整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能进行读或写。

定义锁:

/exclusive_lock/lock

实现方式:

利用 zookeeper 的同级节点的唯一性特性,在需要获取排他锁时,所有的客户端试图通过调用 create() 接口,在  /exclusive_lock 节点下创建临时子节点  /exclusive_lock/lock,最终只有一个客户端能创建成功,那么此客户端就获得了分布式锁。同时,所有没有获取到锁的客户端可以在  /exclusive_lock 节点上注册一个子节点变更的 watcher 监听事件,以便重新争取获得锁。

共享锁

共享锁(Shared Locks),又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都释放。

定义锁:

/shared_lock/[hostname]-请求类型W/R-序号

实现方式: 1、客户端调用 create 方法创建类似定义锁方式的临时顺序节点。

2、客户端调用 getChildren 接口来获取所有已创建的子节点列表。

3、判断是否获得锁,对于读请求如果所有比自己小的子节点都是读请求或者没有比自己序号小的子节点,表明已经成功获取共享锁,同时开始执行度逻辑。对于写请求,如果自己不是序号最小的子节点,那么就进入等待。

4、如果没有获取到共享锁,读请求向比自己序号小的最后一个写请求节点注册 watcher 监听,写请求向比自己序号小的最后一个节点注册watcher 监听。

模拟12306售票案例

image.png

Curator实现分布式锁API

在Curator中有五种锁方案:

  • InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
  • InterProcessMutex:分布式可重入排它锁
  • InterProcessReadWriteLock:分布式读写锁
  • InterProcessMultiLock:将多个锁作为单个实体管理的容器
  • InterProcessSemaphoreV2:共享信号量

创建线程进行加锁设置

public class Ticket12306 implements Runnable{
    private int tickets = 10;// 数据库的票数
    private InterProcessMutex lock;
    @Override
    public void run() {
        while (true){
            // 获取锁
            try {
                lock.acquire(3, TimeUnit.SECONDS);
                if (tickets > 0){
                    System.out.println(Thread.currentThread()+ ":"+tickets);
                    Thread.sleep(100);
                    tickets--;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    // 释放锁
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

创建连接,并且初始化锁

public Ticket12306(){
    CuratorFramework client = CuratorFrameworkFactory.builder().connectString("172.26.130.169:2181")
            .sessionTimeoutMs(60 * 1000)
            // 重试策略
            .retryPolicy(new ExponentialBackoffRetry(3000, 10))
            .connectionTimeoutMs(15 *1000)
            .namespace("")
            .build();
    // 开启连接
    client.start();
    lock = new InterProcessMutex(client,"/lock");
}

运行多个线程进行测试

public class LockTest {
    public static void main(String[] args) {
        Ticket12306 ticket12306 = new Ticket12306();
        // 创建客户端
        Thread t1 = new Thread(ticket12306, "携程");
        Thread t2 = new Thread(ticket12306, "飞猪");
        t1.start();
        t2.start();
    }
}

Zookeeper Leader 选举原理

zookeeper 的 leader 选举存在两个阶段,一个是服务器启动时 leader 选举,另一个是运行过程中 leader 服务器宕机。在分析选举原理前,先介绍几个重要的参数。

  • 服务器 ID(myid):编号越大在选举算法中权重越大
  • 事务 ID(zxid):值越大说明数据越新,权重越大
  • 逻辑时钟(epoch-logicalclock):同一轮投票过程中的逻辑时钟值是相同的,每投完一次值会增加

选举状态:

  • LOOKING: 竞选状态
  • FOLLOWING: 随从状态,同步 leader 状态,参与投票
  • OBSERVING: 观察状态,同步 leader 状态,不参与投票
  • LEADING: 领导者状态

1、服务器启动时的 leader 选举

每个节点启动的时候都 LOOKING 观望状态,接下来就开始进行选举主流程。这里选取三台机器组成的集群为例。第一台服务器 server1启动时,无法进行 leader 选举,当第二台服务器 server2 启动时,两台机器可以相互通信,进入 leader 选举过程。

  • (1)每台 server 发出一个投票,由于是初始情况,server1 和 server2 都将自己作为 leader 服务器进行投票,每次投票包含所推举的服务器myid、zxid、epoch,使用(myid,zxid)表示,此时 server1 投票为(1,0),server2 投票为(2,0),然后将各自投票发送给集群中其他机器。

  • (2)接收来自各个服务器的投票。集群中的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自 LOOKING 状态的服务器。

  • (3)分别处理投票。针对每一次投票,服务器都需要将其他服务器的投票和自己的投票进行对比,对比规则如下:

    • a. 优先比较 epoch
    • b. 检查 zxid,zxid 比较大的服务器优先作为 leader
    • c. 如果 zxid 相同,那么就比较 myid,myid 较大的服务器作为 leader 服务器
  • (4)统计投票。每次投票后,服务器统计投票信息,判断是都有过半机器接收到相同的投票信息。server1、server2 都统计出集群中有两台机器接受了(2,0)的投票信息,此时已经选出了 server2 为 leader 节点。

  • (5)改变服务器状态。一旦确定了 leader,每个服务器响应更新自己的状态,如果是 follower,那么就变更为 FOLLOWING,如果是 Leader,变更为 LEADING。此时 server3继续启动,直接加入变更自己为 FOLLOWING。

image.png

2、运行过程中的 leader 选举

当集群中 leader 服务器出现宕机或者不可用情况时,整个集群无法对外提供服务,进入新一轮的 leader 选举。

  • (1)变更状态。leader 挂后,其他非 Oberver服务器将自身服务器状态变更为 LOOKING。
  • (2)每个 server 发出一个投票。在运行期间,每个服务器上 zxid 可能不同。
  • (3)处理投票。规则同启动过程。
  • (4)统计投票。与启动过程相同。
  • (5)改变服务器状态。与启动过程相同。

以上资源来自菜鸟教程及网络,整理自用,转载需本人同意。 www.runoob.com/w3cnote/zoo…