zookeeper 的简单介绍和应用

zookeeper是一个开源的分布式解决方案,可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、master选举、分布式锁和分布式队列等功能。zookeeper能保证一下一致性:

顺序一致性:从同一个客户端发起的事务请求,最终会按照其发起顺序被应用到zookeeper中。

原子性:事务处理在zookeeper集群所有机器中是一致的。

单一视图:无论客户端连接的是哪个zookeeper服务器,其看到的服务器数据都是一致的。

可靠性:一定服务器成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务器状态将被一直保持下来,处理又有另一个事务对其修改。

实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。


数据模型

zookeeper将所有的数据存在内存中,数据模型是一棵树,由(/)分割路径 如 /foo/path1, 每个节点(ZNode)会保存自己的数据内容。

运行过程中zookeeper会定时向磁盘dump快照数据,同时在Zookeeper启动时,会通过磁盘的事务日志和快照文件恢复成一个完整的内存数据。

zookeeper集群

Zookeeper集群的角色主要有以下三类


系统模型如图所示:


ZAB -- Zookeeper原子消息广播协议

Zookeeper使用了一种称为Zab(Zookeeper Atomic Broadcast)的协议作为其一致性复制的核心,具有高吞吐量、低延迟、健壮、简单的特点,但不过分要求其扩展性,是特别为zookeeper实际的崩溃可恢复的原子消息广播算法。具体的,zookeeper使用一个单一的主进程来接收并处理客户端的所有事物请求,并采用ZAB的原子广播协议,将服务器的状态变更以事物proposal的形式广播到所有的副本进程上去。

Zab协议有两种模式,它们分别是恢复模式和广播模式。

  1. 恢复模式  当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和server具有相同的系统状态。

  2. 广播模式 旦Leader已经和多数的Follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态。这时候当一个Server加入ZooKeeper服务中,它会在恢复模式下启动,发现Leader,并和Leader进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper服务一直维持在广播状态,直到Leader崩溃了或者Leader失去了大部分的Followers支持。

广播模式极其类似于分布式事务中的2pc(two-phrase commit 两阶段提交):即Leader提起一个决议,由Followers进行投票,Leader对投票结果进行计算决定是否通过该决议,如果通过执行该决议(事务),否则什么也不做。

两阶段提交示意图


消息广播流程示意图


所有的事物请求必须由一个全局唯一的leader服务器来协调处理,将客户端的请求转化成一个事物Proposal(提议),并发给机器中所有的follower服务器,然后等待follower服务器反馈,一旦超过半数机器反馈正确后,那么leader服务器会再次向所有的follower服务器发送commit消息提交事物。

为了保证事务的顺序一致性,Zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。Zxid是由64位数字组成的,低32位用作简单计数器。高32位是一个epoch。每当新Leader接管它时,将获取日志中Zxid最大的epoch,新Leader Zxid的epoch位设置为epoch+1,counter位设置0。

在服务崩溃恢复时,Zab协议确定两条规则确保数据的一致性:

  1. 那些已经在leader服务器上提交的事物需要最终被所有服务器提交。
  2. 丢弃那些只在leader服务器上被提出的事物。

leader选举


当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,选举完成后follower或从leader服务器同步数据,恢复到一个正确的状态提供服务。

选举时每个server都会发出一票 , 形式为:(my_id,ZXID)——(服务器ID, 最大事物ID)。第一次投票,每个服务器都会投给自己,之后会根据所有服务器的选票情况投给ZXID最大的服务器,如果ZXID有相同再投最大my_id, 如果某个服务器得票超过半数服务器的数量,则该服务器当选为leader,选举结束。

以下图表示选举流程



leader选举需要过半服务器响应,这样投票结果只能产生一个leader。事务提交需要过半服务器响应,保证了一半服务器的正在运行,而且在服务崩溃时(过半服务器不可用)恢复时,根据选举规则,选举的leader一定是最后一半正常运行的,数据最完整的服务器之一,保证了数据一致性。

zookeeper客户端

zookeeper客户端API包括增、删、改、查、监听(注册watcher)zookeeper目录,节点和数据。

watcher——数据变更的通知

zookeeper提供了分布式的数据发布、订阅功能,能够让多个订阅者同时收到订阅过的节点发生变化的通知,zookeeper使用watcher实现了这个功能。客户端向服务器注册一个watcher监听,当服务器的一些指定事件触发了这个watcher,那么客户端就会收到通知。 watcher注册只能收到一次变更通知,之后无效需要再次进行注册监听。

版本——保证分布式数据原子性操作

zookeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作(包括值没有变化的更新)都会引起版本号的变化。


version属性是用来实现乐观锁机制中的“写入校验”,如果客户端不需要版本校验则version可传-1,操作时不进行version对比。

zookeeper开源客户端的使用

zookeeper开源客户端在原生的zookeeper API上进行了包装,使用zookeeper开源客户端进行一些基本操作,简化了开发,最常用的客户端是Curator,它解决了zookeeper客户端非常底层的细节开发工作,包括重连、反复注册watcher、和NodeExistsException异常等。

下面是基本操作方式

private CuratorFramework client;
@Before
public void init() {
    client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2128")
            .sessionTimeoutMs(16000)
            .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();
    client.start();
}
 
/**
 * 新增节点
 * @throws Exception
 */
@Test
public void create() throws Exception {
    client.create().forPath("/zookeeper/gsw", "hello!".getBytes());
}
 
/**
 * 创建模式
  1、PERSISTENT
  持久化目录节点,存储的数据不会丢失。
 
  2、PERSISTENT_SEQUENTIAL
  顺序自动编号的持久化目录节点,存储的数据不会丢失,并且根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名。
 
  3、EPHEMERAL
  临时目录节点,一旦创建这个节点的客户端与服务器端口也就是session 超时,这种节点会被自动删除。
 
  4、EPHEMERAL_SEQUENTIAL
  临时自动编号节点,一旦创建这个节点的客户端与服务器端口也就是session 超时,这种节点会被自动删除,并且根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名。
 * @throws Exception
 */
@Test
public void createMode() throws Exception {
    client.create().withMode(CreateMode.EPHEMERAL).forPath("/zookeeper/gsw/ephemeral1");
    client.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath("/zookeeper/gsw/ephemeral2");
    System.out.println(client.getChildren().forPath("/zookeeper/gsw"));
}
/* ---- result ----
    [ephemeral20000000015, ephemeral1]
 */
 
 
/**
 * 获取数据
 * @throws Exception
 */
@Test
public void getData() throws Exception {
    System.out.println(new String(client.getData().forPath("/zookeeper/gsw")));
}
/* ---- result ----
    hello
 */
 
/**
 * 更新数据
 * @throws Exception
 */
@Test
public void updateData() throws Exception {
    client.setData().forPath("/zookeeper/gsw", "hello world!".getBytes());
}
 
/**
 * 根据版本更新数据
 * @throws Exception
 */
@Test
public void updateDataWithVersion() throws Exception {
    Stat stat = new Stat();
    client.getData().storingStatIn(stat).forPath("/zookeeper/gsw");
    client.setData().withVersion(stat.getVersion()).forPath("/zookeeper/gsw", "hello world!".getBytes());
}
 
/**
 * 异步更新
 * @throws Exception
 */
@Test
public void updateDataBackGround() throws Exception {
    Stat stat = new Stat();
    client.getData().storingStatIn(stat).forPath("/zookeeper/gsw");
    client.setData().withVersion(stat.getVersion()).inBackground(new BackgroundCallback() {
        @Override
        public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
        }
    }).forPath("/zookeeper/gsw", "hello world!".getBytes());
}
 
/**
 * 删除节点
 * @throws Exception
 */
@Test
public void delete() throws Exception {
    for (String path : client.getChildren().forPath("/zookeeper/gsw")) {
        client.delete().forPath("/zookeeper/gsw/" + path);
        System.out.println(client.getChildren().forPath("/zookeeper/gsw"));
    }
}
/* ---- result ----
    []
 */

zookeeper典型使用场景

事件监听

zookeeper原生支持注册watcher来进行事件监听,但是其使用起来不是很方便,需要开发人员反复注册watcher,比较繁琐,curator引入Cache来实现对zookeeper服务器的监听, Cache 监听分为两类:节点监听、子节点监听, NodeCache,PathChildrenCache。

代码清单

/**
 * 事件监听
 * @throws Exception
 */
@Test
public void nodeCache() throws Exception {
 
    client.create().forPath("/home");
 
    NodeCache nodeCache = new NodeCache(client, "/home");
    nodeCache.start(true);
    nodeCache.getListenable().addListener(new NodeCacheListener() {
        @Override
        public void nodeChanged() throws Exception {
            System.out.println("node has changed");
        }
    });
    client.delete().deletingChildrenIfNeeded().forPath("/home");
 
 
    client.create().creatingParentsIfNeeded().forPath("/home/hello");
    PathChildrenCache pcc = new PathChildrenCache(client, "/home", false);
    // 添加监听器
    pcc.getListenable().addListener(new PathChildrenCacheListener() {
        @Override
        public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
            System.out.println("PathChildrenNode has changed event:" + event.getType());
        }
    });
    pcc.start();
 
    TimeUnit.SECONDS.sleep(1);
    client.delete().forPath("/home/hello");
    TimeUnit.SECONDS.sleep(1);
    client.delete().forPath("/home");
}

master选举

实现思路:选择一个根节点,例如 /master_select ,多台机器同时向该节点创建一个子节点/master_select/lock, 利用zookeeper的特性,最终只有一台机器能够创建成功,成功的机器成为master。

代码清单

@Test
public void selectMaster() throws InterruptedException {
    new Thread(this::master1).start();
    new Thread(this::master2).start();
    new Thread(this::master3).start();
    Thread.sleep(Integer.MAX_VALUE);
 
}
 
public void master1() {
    try {
        LeaderSelector selector = new LeaderSelector(client, "/zookeeper/gsw/master_path", new LeaderSelectorListenerAdapter() {
            @Override
            public void takeLeadership(CuratorFramework client) throws Exception {
                System.out.println("1成为master角色");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("1完成master 操作退出");
            }
        });
        selector.autoRequeue();
        selector.start();
        Thread.sleep(Integer.MAX_VALUE);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
 
public void master2() {
    try {
        LeaderSelector selector = new LeaderSelector(client, "/zookeeper/gsw/master_path", new LeaderSelectorListenerAdapter() {
            @Override
            public void takeLeadership(CuratorFramework client) throws Exception {
                System.out.println("2成为master角色");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("2完成master 操作退出");
            }
        });
        selector.autoRequeue();
        selector.start();
        Thread.sleep(Integer.MAX_VALUE);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
 
public void master3() {
    try {
        LeaderSelector selector = new LeaderSelector(client, "/zookeeper/gsw/master_path", new LeaderSelectorListenerAdapter() {
            @Override
            public void takeLeadership(CuratorFramework client) throws Exception {
                System.out.println("3成为master角色");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("3完成master 操作退出");
            }
        });
        selector.autoRequeue();
        selector.start();
        Thread.sleep(Integer.MAX_VALUE);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

分布式锁

分布式环境中,为了保证数据一致性,经常需要分布式锁的功能,zookeeper锁的实现思路与master类似。

 @Test
public void lock() throws Exception {
    String lockPath = "/lock_path";
    InterProcessLock lock = new InterProcessMutex(client, lockPath);
    try {
        lock.acquire();
        TimeUnit.SECONDS.sleep(3);
    } finally {
        lock.release();
    }
}

分布式计数器

实现思路:指定一个zookeeper数据节点作为计数器,多个应用实例再分布式锁的控制下,通过更新该数据节点的内容来实现计数功能。

/**
 * 分布式计数器
 * @throws Exception
 */
@Test
public void distribute_int() throws Exception {
    String dis_Path = "/distribute_atomic_int_page";
    DistributedAtomicInteger distributedAtomicInteger = new DistributedAtomicInteger(client, dis_Path, new RetryNTimes(1000, 3));
    AtomicValue<Integer> rc = distributedAtomicInteger.add(2);
    System.out.print("result:" + rc.succeeded());
} 

分布式Barrier

功能与JDK自带的CyclicBarrier一样,zookeeper实现分布式barrier实现原理与计数器类似。zookeeper的实现barrier有两种

DistributedBarrier--必须由其他线程释放barrier
DistributedDoubleBarrier -- 计数自己释放的barrier
/**
 * 分布式barrier
 * @throws Exception
 */
@Test
public void barrier() throws Exception {
 
    String dis_Path = "/distribute_barrier_int_page";
    DistributedBarrier distributedBarrier = new DistributedBarrier(client, dis_Path);
    new Thread(() -> {
        try {
            distributedBarrier.setBarrier();
            distributedBarrier.waitOnBarrier();
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
    // 主线程释放
    distributedBarrier.removeBarrier();
 
    for (int i=0; i<5; i++) {
        dis_Path = "/double_distribute_barrier_int_page";
        DistributedDoubleBarrier distributedDoubleBarrier = new DistributedDoubleBarrier(client, dis_Path, 5);
        // 第一次阻塞
        distributedDoubleBarrier.enter();
        // 第二次阻塞
        distributedDoubleBarrier.leave();
        // 执行5次自己能够释放
    }
 
} 

Kafka中zookeeper的应用

为了更好的理解,这里先介绍kafka中常用的术语

  • 消息生产者:Producer
  • 消息消费者:Consumer
  • 主题:Topic,配置在服务器端,用于消费者和生产者之间的订阅关系,生产者发送消息到topic下,消费者消费该topic下的消息。
  • 消息分区:Partition,一个topic下面会分为多个分区,一个分区只能有一个消费者消费。
  • Broker:mafka服务器,用于存储消息。
  • 消费者分组:Group
  • Offset:读取消息所需的消息在文件中的偏移量。

消费者与partition的关系 一对多:

Broker注册

每个Broker服务器在启动时,都会到zookeeper上进行注册,在路径为:/broker/ids/[0...N] 下创建属于自己的节点,每个broker会将自己的IP地址和和端口等信息写入到该节点中去。这些节点都是临时节点,也就是说,一旦哪个broker服务器宕机或是下线了,那么对应的broker节点也就被删除了。如此我们可以通过broker节点的变化信息来动态表broker服务器的可用性。

Topic注册

在kafka中,会将同一个Topic的消息分成多个分区分布到多个broker上,这样的分区信息以及与broker的对应关系也都是由zookeeper维护的。broker服务器在启动后,回到对应的节点下注册自己的broker ID,并写入topic的分区总数。

如 /brokers/topics/topic_name/3  → 2 , 这个节点表明broker ID为3的服务器,对于"topic_name"这个topic的消息提供了2个分区消息储存。同样这个分区节点也是一个临时节点。

使用zookeeper进行负载均衡

使用zookeeper,kafka的生产者会对zookeeper上的“broker的新增和减少”、“topic的新增和减少” 和 “broker与topic的关联关系的变化”等事件注册watcher监听,这样就可以实现一直动态的负载均衡机制了。

消息分区与消费者之间的关系

zookeeper上记录了消费分区与消费者之间的关系,每个消费者一旦确定了对一个消息分区的消费的权利,那么需要将其Consumer ID写入到对应消息分区的临时节点上。

如: /consumers/[group_id]/owners/[topic]/[broker_id - partition_id], 其中 [broker_id - partition_id]是一个消息分区的标识,其节点数据就是Consumer ID。

消息消费进度offset记录

在消息者对指定消息分区进行消费的过程中,需要定时地将分区消息的消费进度offset记录到走哦keeper上去,以便在消费者重启或是其他消费者重新接管该消息分区后,能够从之前的进度开始读取消息。 offset在zookeeper上由专门的一个节点负责,其路径为:/consumers/[group_id]/offsets/[topic]/[broker_id - partition_id] , 其节点数据就是offset值。

消费者注册

消费者注册的临时节点:/consumers/[group_id]/ids/[consumer_id], 每个消费者都需要关注所属消费者分组中消费者服务器和broker服务器的变化情况,即对 /consumers/[group_id]/ids/ 与 /broker/ids/[0...N] 节点注册watcher进行监听,根据变化来决定是否需要进行消费者负载均衡。











  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值