【读书笔记】分布式架构下需要了解的ZooKeeper知识

痛点

在没有注册中心时候,采用硬编码方式服务间调用需要知道被调方的地址或者代理地址,当服务更换部署地址,就不得不修改调用当中指定的地址或者修改代理配置。

当服务越来越多时,服务URL配置管理变得非常困难,F5等硬件负载均衡器的单点压力也越来越大.

而有了注册中心之后,每个服务在调用别人的时候只需要知道服务名称就好,继续地址都会通过注册中心同步过来。

因此注册中心解决了服务之间的自动发现
举个栗子:

你去商场逛街,你作为消费端,商家作为服务端,你怎么找到想去的商家?

没有注册中心就类似靠记忆去寻找,假设商家从一楼搬到了二楼,你之前记得商家是在一楼,去了后发现商家没了,然后给商家打电话问搬哪了,你才知道商家搬到二楼了

注册中心就类似一个商家门店导览图,你从导览图上可以清晰了解商家在商场几楼,假设商家从一楼搬到二楼,导览图也会更新,你会直接走到二楼

 

背景

以下一些操作或作业是任意一个分布式系统都必须的:

  1. leader选举:对于leader/follower或master/slave结构的分布式系统而言,leader选举就是必须的,特别是现有leader崩溃后如何从剩余节点中选择新的leader。
  2. KV元数据存储:分布式系统中有一些元数据信息要保存在所有的节点上以维持系统的一致性
  3. 成员管理:分布式系统中节点的新增、移除都需要依托单独的组件来管理

Zookeeper把这些功能独立地实现在一个单独的协调服务软件中,ZooKeeper 最常用的使用场景就是用于担任服务生产者和服务消费者的注册中心,ZooKeeper底层提供两个主要功能,一是管理(存储、读取)用户程序提交的数据(文件系统);二是为用户程序提交数据节点监听服务(通知机制)。

 

ZooKeeper简介

ZooKeeper 是个针对大型分布式系统的高可用、高性能且具有一致性的开源协调服务,专业于任务协作。

分布式系统中统一配置、协调资源、统一命名服务,保证分布式一致性,订阅和发布、服务动态发现以及透明化路由。

  1. 顺序一致性:同一个客户端发出的事务请求,最终会严格按照其发起顺序被应用到ZooKeeper中去,这个编号也叫做时间戳—zxid(ZooKeeper Transaction Id)。
  2. 原子性:同数据库原子性。
  3. 单一视图:无论客户端连接的是哪个ZK服务器,看到服务端的数据是一致的。
  4. 可靠性:类似数据库的持久性。
  5. 实时性:ZK仅仅保证一定时间段内(“写”会导致所有的服务器间同步),客户端最终一定能够从服务端上读取到最新的数据状态。随着ZooKeeper集群机器增多,读请求的吞吐量会提高但是写请求的吞吐量会下降。

 

节点

节点结构

数据结构为树型,无论客户端连接的是哪个ZK服务器,看到服务端的数据是一致的,引用方式为路径引用,类似 /水果/苹果 这样的路径

 

整个ZNode树形的目录结构全部都放在内存中,因此ZooKeeper可以实现高吞吐量和低延时。

 

ZNode中包含了数据、子节点引用(左右子节点)

 

ACL(AccessControlLists)策略来进行权限控制,类似UNIX的文件权限控制,ZK定义了5中权限,CREATE:创建子节点权限,READ:读取数据节点和子节点的权限,WRITE:更新数据节点的权,DELETE:删除子节点的权限,ADMIN:设置节点ACL的权限。

 

状态Stat(如事务ID、版本号、时间戳、大小)其中版本中包括ZNode的版本,还包括ZNode子节点的版本以及ZNode ACL的版本。

 

每个ZNode 只是用来存储少量状态和配置信息,所以ZNode最大不超过1MB。

ZNode类型

临时节点Ephemeral)与会话生命周期绑定,当客户端和服务端断开连接后(会话失效),ZNode节点会自动删除,此类型的目录节点不能有子节点目录,EPHEMERAL_SEQUENTIAL临时有序节点,EPHEMERAL临时节点

持久化节点Persistent)当客户端和服务端断开连接后,ZNode节点不会自动删除,要想删除只能主动delete,主要目的是为应用保存数据,PERSISTENT_SEQUENTIAL 持久化有序节点,PERSISTENT持久化节点

SEQUENTIAL 加上这个后缀节点叫做顺序节点,一旦节点被标记上这个属性,那么这个节点被创建时自动在其后面追加一个整型数字,这个整型数字由父节点维护的自增数字。

ZooKeeper 的每一个节点都会为它的第一级子节点维护一份顺序编号,会记录每个子节点创建的先后顺序,这个顺序是分布式同步的,也是全局唯一的。

 

会话

Session指的是ZooKeeper服务端与客户端会话,客户端连接是指客户端和服务端之间一个TCP长连接,可以向服务端发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。以dubbo

服务提供者在启动的时候,向Zookeeper 上的制定节点/{rpc}/${serviceName}/provides 写入值得的API地址,这个操作就是服务的公开。

服务消费者在启动的时候,订阅节点/{rpc}/{serviceName}/proviedes下的服务提供者的URL地址,获得所有服务提供者的API。

SessionID:用来全局唯一识别会话
SessionTimeOut:当由于服务器压力太大、网络故障、或是客户端主动断开连接等原因导致客户端连接断开时,只要在SessionTimeOut规定时间内重新连接上集群中任意一台服务器,那么之前创建的会话任然有效
TickTime:下次会话超时时间点
isClosing:当服务端如果检测到会话超时失效,通过设置这个舒心将会话关闭

 

ZooKeeper如何保证主从节点状态同步——原子广播机制

实现这个机制为Zab(ZooKeeper Atomatic Broadcast)协议,作为ZooKeeper的核心实现算法Zab,就是解决了分布式系统下数据如何在多个服务之间保持同步问题的,即解决集群崩溃恢复(恢复模式)和以及数据同步(广播模式)问题。

ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services。

当整个ZooKeeper集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式

选举机制

Zookeeper的客户端和服务器通信采用长链接方式,每个客户端和服务器通过心跳来保持连接,更新数据时,先更新Leader节点再更新至Follower节点,读取数据时,直接读取随机Follower节点。

引入Observer的一个主要原因是提高读请求的可扩展性,不同于Follower的是,观察者不参与选举过程,每一个新加入的观察者将对应于每一个已提交事务点引入的一条额外消息。

另一个原因是进行跨多个数据中心部署。由于数据中心之间的网络链接延时,将服务器分散于多个数据中心将明显地降低系统的速度。

Leader选举 (Leader Election)

第一次投票:

所有机器都处于Looking状态,投票中包含SID(服务器唯一标示)和ZXID(事务ID),用(SID,ZXID)来标示一此投票

举个例子,有1、2、3、4、5 SID机器,其中2为Leader,但是1 ,2号机器崩溃,此时3,4,5号机器开始第一次投票,每台机器都会将自己作为投票对象,投票内容为(3,9)、(4,8)、(5,8)

变更投票:

每台机器发出投票后,也会收到其他机器的投票,会按照一定算法规则确定是否需要变更自己的投票

  • vote_sid:接收到的投票中所推举Leader服务器的SID。
  • vote_zxid:接收到的投票中所推举Leader服务器的ZXID。
  • self_sid:当前服务器自己的SID。
  • self_zxid:当前服务器自己的ZXID。

变更算法如下

规则一:如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去。

规则二:如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更。

规则三:如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去。

规则四:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更。

 

经过第二轮投票后,统计机器收到的投票,如果一台机器收到超过半数相同的投票,那么投票对应的SID机器被选举为Leader。

只有最新的服务器将赢得选举,因为其拥有最近一次的 zxid,如果多个服务器拥有的最新的 zxid 值,其中的 sid 值最大的将会赢得选举。

一般说来Zookeeper leader的选择过程都非常快,通常<200ms。注意:使用的服务器最好为奇数台服务器。

发现阶段(Discovery)

用于在从节点中发现最新的ZXID和事务日志,这一阶段,Leader接收所有Follower发来各自的最新epoch值。Leader从中选出最大的epoch,基于此值加1,生成新的epoch分发给各个Follower。

各个Follower收到全新的epoch后,返回ACK给Leader,带上各自最大的ZXID和历史事务日志。Leader选出最大的ZXID,并更新自身历史日志。

同步阶段(synchronization)

把Leader刚才收集得到的最新历史事务日志,同步给集群中所有的Follower。只有当半数Follower同步成功,这个准Leader才能成为正式的Leader。

 

数据广播

如何确认一个事务是否已经提交,ZooKeeper 由此引入了 zab 协议,两提交阶段。在接收到一个写请求操作后,追随者会将请求转发给群首,群首将会探索性的执行该请求,并将执行结果以事务的方式对状态更新进行广播。【ZooKeeper 如何保证数据一致性的原理】

  1. Leader生成一个新的事务(Proposal)并为这个事务生成一个唯一的ZXID,然后把此提议放入到FIFO队列中。
  2. Leader采用两阶段提交,发送该事务Proposal给所有的Follower节点。
  3. Follower节点将收到的事务请求加入到历史队列中和日志中,并发送ack给Leader ,通知Leader其已接收提案。
  4. 当Leader收到超过一半数量Follower的ACK消息后,会发送commit请求 。
  5. 当Follower收到commit请求时,会判断该事务的ZXID是不是比历史队列中的任何事务的ZXID都小,如果是则提交,如果不是则等待比它更小的事务的commit。

还是很民主的,Leader发送提议,超过一半的Follower 收到反馈同意,Leader才会发送commit命令,Follower才会执行。

监听

客户端获得服务端数据变化,不是通过轮询,而是通过通知机制,客户端向 ZooKeeper 服务器端注册需要接收通知的 ZNode,通过对 ZNode 设置监视点来接收通知。

ZooKeeper的Watcher特性:

  1. Watcher是一次性的,每次都需要重新注册,并且客户端在会话异常结束时不会收到任何通知,而快速重连接时仍不影响接收通知。
  2. 客户端调用 getData("/znode", true) 并且/znode1 节点上的数据发生了改变或者被删除了,客户端将会获取/znode1 发生变化的监视时间。
  3. Watcher的回调执行都是顺序执行的,并且客户端在没有收到关注数据的变化事件通知之前是不会看到最新的数据,另外需要注意不要在Watch回调逻辑中阻塞整个客户端的Watcher回调。
  4. Watcher是轻量级的,WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。ZooKeeper服务端只会通知客户端发生了什么,并不会告诉具体内容。
  5. 注册Watcher 其中getData、exists为数据监视,getChildren为子数据监视,触发Watcher create、delete、setData 。setData()会触发znode上设置的data watch(如果set成功的话)。一个成功的create() 操作会触发被创建的znode上的数据watch,以及其父节点上的child watch。而一个成功的delete()操作将会同时触发一个znode的data watch和child watch(因为这样就没有子节点了),同时也会触发其父节点的child watch。

既然Watcher 监听器是一次性的,如何要反复使用,怎么办呢?需要反复地通过构造者的usingWatcher方法取提前进行注册。所以,Watcher监听器不适合用于节点的数据频繁变动或者节点频繁变动这样的业务场景,而是适用于一些特殊的、变动不频繁的场景,例如会话超时、授权失败等这样的特殊场景。 

反复注册

Watcher需要反复注册比较繁琐,Curator 引入了Cache来监听ZooKeeper 服务器端的事件。Cache机制对ZooKeeper事件监听进行了封装,能够自动处理注册监听。包括Node Cache、PathCache和Tree Cache 三组类。

  1. Node Cache 节点缓存可用于ZNode 节点的监听。
  2. Path Cache 子节点缓存可用于ZNode的子节点的监听,PathChildrenCache 子节点缓存用于子节点的监听,监控当前节点的子节点被创建、更新或者删除,需要强调在只能监听子节点,监听不到当前节点;不能递归监听,子节点的子节点不能递归监控。
  3. Tree Cache 树缓存是Path Cache的增强,不光能监听子节点,还能监听ZNode 节点自身。

Curator 监听的原理:无论是PathChildrenCache,还是TreeCache,所谓的监听都是在进行Curator 本地缓存视图和ZooKeeper 服务器远程的数据节点的对比,并且在进行数据同步时触发相应的事件。

注意:3.6.0新增功能:客户端可以在ZNode上设置永久性的递归监视,这些监视在触发时不会删除,并且会以递归方式触发已注册ZNode以及所有子ZNode的修改。ZooKeeper所有读操作getData 、getChildren 、exists 都可以设置监视

 

ZooKeeper应用场景

统一命名服务

分布式系统中客户端使用命名服务,能够制定名字获取资源或服务地址,提供者信息,这些信息统称为名字,其中较常见的为RPC服务地址列表。

通过ZooKeeper提供的创建节点API,可创建一个全局唯一path,这个path就可以作为一个名字。

数据发布与订阅(配置中心)

发布者将数据发布到ZooKeeper节点,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。

分布式环境下配置文件管理和同步问题,一个集群中,所有节点的配置信息是一致的,可将配置信息写入ZooKeeper上一个ZNode,各个客户端监听这个ZNode,一旦 ZNode中的数据被修改,ZooKeeper将通知各个节点。

统一集群管理

关键点在于是否有机器退出或加入,选举Master

所有机器约定在父目录下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知。
所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为master就好。

分布式环境中,实时掌握每个机器的状态,可将每个机器的状态信息写入ZooKeeper上一个ZNode,监听这个ZNode可获取它的实时状态变化。

软负载均衡

注册服务下每一个ZNode存储IP地址,客户端机器监听注册服务。

分布式协调/通知

ZooKeeper中Watch注册和异步通知机制,能够很好实现分布式环境下不同机器,甚至不同系统之间的协调和通知,从而实现对数据变更的实时处理,实现对ZooKeeper 服务器的事件监听,是客户端操作服务器的一项重点工作。

心跳检测中让检测系统和被检测系统之间并不直接关联起来,而是通过ZK上某个节点关联,减少系统耦合,同理系统调度系统。

分布式锁

单体应用开发场景中涉及并发的时候,往往使用Sychronized或者其他同一个JVM内的Lock 机制来解决多线程间的同步问题。

在分布式集群工作中的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题。这种跨机器的锁就是分布式锁。

ZooKeeper分布式锁,首先需要创建一个父节点,尽量是持久节点,然后每个要获得锁的线程都在这个节点下创建个临时顺序节点。由于ZK节点是按照创建的顺序依次递增的,为了确保公平,可以规定编号最小的那个节点表示获得了锁。每一个锁在尝试占用锁之前,首先判断自己的排号是不是当前最小的,如果是则获取锁。

抢号成功后,如果不是排号最小的节点,就处于等待通知的状态。等前一个ZNode的通知即可。当前一个ZNode被删除的时候,就是轮到了自己占有锁的时候。第一个通知第二个,第二个通知第三个,以此类推。

ZooKeeper内部机制能保证由于网络异常或者其他原因造成集群中占用锁的客户端失联时,锁能够被有效释放。一旦占用锁的ZNode 客户端与ZooKeeperd集群失去联系,这个临时ZNode 也将自动删除,排在他后面的节点也能够收到删除事件,从而获得锁,所以在创建取号节点的时候,尽量创建临时ZNode节点。

ZooKeeper这种收尾相接,后面监听前面的方式,可以避免羊群效应,即一个节点挂掉后,所有节点都去监听,然后做出反应,这样会给服务器带来巨大的压力,所以有了临时顺序节点,当一个节点挂掉,之后它后面的那一个节点才做出反应。

锁服务可以分为两类:保持独占:只有一个可以成功获取到这把锁,把ZK上一个ZNode看作一把锁,通过 create znode的方式来实现,所有客户端都去创建/distribute_lock 节点,最终成功创建的那个客户端即拥有了这把锁,释放锁删除临时节点;控制时序:全局时序,客户端在创建临时有序节点维持一份Sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。

ZooKeeper分布式锁优点和缺点

优点是能有效地解决分布式问题,不可重入问题,使用起来也简单。

缺点是性能并不高,因为每次在创建锁和释放锁的过程,都要动态创建、销毁暂时节点来实现锁功能。ZK中创建和删除节点只能通过主节点(Leader)来执行,然后Leader服务器还需要将数据同步到所有从节点(Follower),这样频繁的网络通信,性能的短板是非常突出的。在高性能、高并发场景下,不建议使用ZooKeeper 分布式锁,由于ZK高可用,在并发量不高的应用场景中适合使用。对于并发量很大的、性能要求高的场景可以基于Redis 的分布式锁。

 

其他问题

1、ZXID如何确定? 

zxid 是一个 64 位的数字,它高 32 位是 epoch 用来标识 leader 关系是否改变,每次一个 leader 被选出来,它都会有一个新的 epoch,低 32 位是个递增计数。 zxid=epoch+counter

2、应用到哪些应用上

一般分布式项目都会使用分布式协调服务,Crane 中使用到ZooKeeper,其中ZK用于保存客户端的注册信息,如果ZK集群宕机,调度端会按之前缓存的客户端列表继续调度。

  1. HBase,Zookeeper用于选举集群Master,跟踪可用的Server,和保存集群元数据。
  2. Kafka,Zookeeper用于崩溃检测,实现Topic发现,和维护Topic的生产和消费状态。
  3. Solr,Zookeeper用于存储集群的元数据信息及协调元数据的更新。
  4. Fetching Service,Zookeeper用于Master选举,崩溃检测,元数据保存。
  5. Messages,Zookeeper用于实现分片和故障迁移的控制器,和服务发现。

3、ZooKeeper常见命令

bin/zkServer.sh start 启动
bin/zkServer.sh status 查看状态
bin/zkServer.sh stop 停止
bin/zkServer.sh restart 重启
zkCli.sh -server 127.0.0.1:2181 连接服务器
ls / 查看某个目录包含的所有文件
ls2 /查看某个目录包含的所有文件的time、version信息
create /test "test" 创建znode,并设置初始内容 
create -s 创建顺序节点
create -e 创建临时节点
get /test 获取znode的数据
set /test “ricky” 修改znode内容
delete /test 删除znode,若删除节点存在子节点,那么无法删除该节点,必须先删除子节点,再删除父节点
quit 退出客户端

 

4、ZooKeeper Java客户端有哪些?

zookeeper 原生 、curator-framework 、 zkclient,其中curator-framework 比较完美

<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-framework</artifactId>
  <version>4.3.0</version>
</dependency>

Curator 降低zk使用复杂性

1.重试机制:提供可插拔的重试机制, 它将给捕获所有可恢复的异常配置一个重试策略,并且内部也提供了几种标准的重试策略(比如指数补偿)

2.连接状态监控: Curator初始化之后会一直的对zk连接进行监听, 一旦发现连接状态发生变化, 将作出相应的处理

3.zk客户端实例管理:Curator对zk客户端到server集群连接进行管理.并在需要的情况, 重建zk实例,保证与zk集群的可靠连接

4.各种使用场景支持:Curator实现zk支持的大部分使用场景支持(甚至包括zk自身不支持的场景),这些实现都遵循了zk的最佳实践,并考虑了各种极端情况

 

举几个使用curator的例子

读取

    private String readTopicsFromZk() {
            String topics = "shop_topic";

            try {
                String path = "/shop/topics";
                if (null == this.curator.checkExists().forPath(path)) {
                    this.curator.create().creatingParentContainersIfNeeded().forPath(path, topics.getBytes());
                } else {
                    byte[] buf = (byte[])this.curator.getData().forPath(path);
                    if (buf != null) {
                        topics = new String(buf);
                    }
                }

                return topics;
            } catch (Exception var4) {
                log.error("failed to read topics from zk", var4);
                return "";
            }
        }
    }

 

监听

    private void watch() {
        try {
            if (this.curator != null) {
                this.nodeCache = new NodeCache(this.curator, "/shop/topics", false);
                this.nodeCache.getListenable().addListener(() -> {
                    this.refresh(this.readTopicsFromZk());
                });
                this.nodeCache.start();
            }
        } catch (Exception var2) {
            log.error("watch for topics failed, ", var2);
        }

    }

销毁

    public void destroy() {
        try {
            if (this.nodeCache != null) {
                this.nodeCache.close();
                this.nodeCache = null;
            }
        } catch (IOException var2) {
            log.info("failed to close nodeCache.", var2);
        }

    }

 

5、有可能会出现数据不一致的问题?

查询不一致:因为Zookeeper是过半成功即代表成功,假设我们有5个节点,如果123节点写入成功,如果这时候请求访问到4或者5节点,那么有可能读取不到数据,因为可能数据还没有同步到4、5节点中,也可以认为这算是数据不一致的问题。解决方案可以在读取前使用sync命令。

Leader未发送Proposal:leader刚生成一个proposal,还没有来得及发送出去,此时leader宕机,重新选举之后作为follower,但是新的leader没有这个proposal。

Leader发送Proposal 成功,发送commit前宕机:如果发送proposal成功了,但是在将要发送commit命令前宕机了,如果重新进行选举,还是会选择zxid最大的节点作为leader,因此,这个日志并不会被丢弃,会在选举出leader之后重新同步到其他节点当中。

 

6、注册中心对比

 

参考:

《Netty、Redis、ZooKeeper高并发实战》

https://cloud.tencent.com/developer/article/1513902

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值