面试之敌系列 6 zookeeper

631 阅读15分钟

zookeeper 详解

zookeeper是一种开源的分布式协调服务,通过将分布式一致性服务封装成合适的原语集,提供接口给用户,实现分布式协调。

zookeeper是一种典型的分布式数据一致性解决方案,分布式服务可以基于zookeeper实现数据的发布--订阅,负载均衡,命名服务,分布式协调,通知,集群管理,master选举,分布式锁等一系列的功能。

zookeeper一个最常用的场景就是用于担任服务生产者和服务消费者的注册中心。

zookeeper 奇数个服务器构成,主要是选举的时候的多数,一般是超过一般的时候,就可以表示该提议已经成功,或者该事务已经成功了。而且,只要有半数以上的节点存活的时候,zookeeper就可以提供服务。

ZNode

在谈到分布式的时候,我们通常说的“节点"是指组成集群的每一台机器。然而,在Zookeeper中,“节点"分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点一一ZNode。

在zookeeper中,node可以分为永久节点和临时节点两类。临时节点的话,其生命周期一般和回话绑定,回话结束的时候自动的删除。 此外,节点创建的时候还可以指定是否为sequence类型的。

Watcher

zookeeper提供的一个事件监听器,通过对指定的节点进行特定的时间监听,可以自动在事件发生的时候触发。

zookeeper的特点

  1. 顺序一致性:客户端发起的事务请求,最终会严格地按照其顺序在zookeeper中执行
  2. 原子性:所有的事务的请求最终在集群中的每个机器上的情况是一致的,也就是说事务在所有的机器上成功,或者在所有的机器上均没有执行
  3. 单一镜像:所有的客户端,无论连接到哪个zookeeper服务器上,其看到的数据模型都是一致的
  4. 可靠性:提供持久化。

数据模型

image.png
zookeeper提供了一种和文件系统很类似的层次结构的命名空间。分布式的应用程序可以共享该数据区域,但是由于使用了zookeeper协议,因此所有的操作均有原子保障。
image.png

  1. stst: 包含Znode的各种元数据,比如事务ID、版本号、时间戳、大小等等。
  2. acl: 记录Znode的访问权限,即哪些人或哪些IP可以访问本节点。
  3. child: 当前节点的子节点引用,类似于二叉树的左孩子右孩子。这里需要注意一点,Zookeeper是为读多写少的场景所设计。Znode并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息,每个节点的数据最大不能超过1MB。
  4. data: Znode存储的数据信息。

集群模型

为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。 客户端在使用 ZooKeeper 时,需要知道集群机器列表,通过与集群中的某一台机器建立 TCP 连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。由于每次在处理事务的时候,当以后一般的节点处理完成,就会对外宣布此时事务已经成功,但是此时还有很多的节点处于同步的状态,因此,如果超过一半的节点挂掉的话,那么此次事务其实就可以不成功了。如果不成功的话,那么就不会对外宣布可以继续提供正常的服务了。

顺序访问

对于来自客户端的每个更新的请求,都会分配一个全局唯一的id zxid 也叫时间戳。来表示该事务操作的顺序。

Zookeeper的集群角色

最典型集群模式: Master/Slave 模式(主备模式)。在这种模式中,通常 Master服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。此时,所有的客户端的事务请求其实都会转发给到Master来处理,利用ZAB协议来

image.png
ZooKeeper 集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。

image.png

ZAB 协议介绍

ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。

ZAB协议包括两种基本的模式,分别是 崩溃恢复和消息广播。当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进人恢复模式并选举产生新的Leader服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致。

当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进人消息广播模式了。 当一台同样遵守ZAB协议的服务器启动后加人到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加人的服务器就会自觉地进人数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

崩溃恢复

AB协议所定义的三种节点状态:

  1. Looking :选举状态。
  2. Following :Follower节点(从节点)所处的状态。
  3. Leading :Leader节点(主节点)所处状态。

我们还需要知道最大ZXID的概念: 最大ZXID也就是节点本地的最新事务编号,包含epoch和计数两部分。epoch是纪元的意思,相当于Raft算法选主时候的term。

ZAB协议崩溃恢复要求满足如下2个要求:

  1. 确保已经被leader提交的proposal必须最终被所有的follower服务器提交。
  2. 确保丢弃只在leader服务器提出的proposal。(可能本机先commit)

新选举出来的leader不能包含未提交的proposal,即新选举的leader必须都是已经提交了的proposal的follower服务器节点。同时,新选举的leader节点中含有最高的ZXID。这样做的好处就是可以避免了leader服务器检查proposal的提交和丢弃工作。(下文选主流程能够保证1,同步过程能保证2)

首先,选取最大最新的zdix的时候,已经可以保证已经提价的事务也会存在在最新的leader中论文 其次,如果leader发现了自己没有的事务,那么可能就是proposal提出的时候失败了的,此时由于epoch 小,会被leader 实现回滚。这样就可以保障之前的两个步骤。

广播协议

zookeeper采用ZAB协议的核心就是只要有一台服务器提交了proposal,就要确保所有的服务器最终都能正确提交proposal。这也是CAP/BASE最终实现一致性的一个体现。注意,提出和提交是不一样的。提交是获得了半数以上的同意,提出只是还没发出而已。、

  1. 客户端发起一个写操作请求
  2. Leader服务器将客户端的request请求转化为事物proposql提案,同时为每个proposal分配一个全局唯一的ID,即ZXID。
  3. leader服务器与每个follower之间都有一个队列,leader将消息发送到该队列
  4. follower机器从队列中取出消息处理完(写入本地事物日志中)毕后,向leader服务器发送ACK确认。
  5. leader服务器收到半数以上的follower的ACK后,即认为可以发送commit
  6. leader向所有的follower服务器发送commit消息。

2pc协议,也就是两阶段提交,发现流程2pc和zab还是挺像的,区别在于zab协议没有中断回滚逻辑.二阶段提交的要求协调者必须等到所有的参与者全部反馈ACK确认消息后,再发送commit消息。要求所有的参与者要么全部成功要么全部失败。二阶段提交会产生严重阻塞问题,但paxos和2pc没有这要求。

zookeeper ZAB协议详解

Zookeeper的典型应用场景

配置中心

假如有多个服务器都需要连接同一个数据库,我们可以让这多个数据库读取同一个配置文件,这样如果想要更换数据库的话只需要更改配置文件,然后让服务器使用新的配置信息重新连接就好。 在这种情况下就可以使用发布/订阅模式,让多个服务器共同订阅一个目标,然后当目标将最新的配置文件发布的时候,所有的订阅者都能够收到最新的消息,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。还是假设有多个服务器需要同时更换数据库,可以使用发布/订阅模式来完成,使用ZooKeeper来实现。最终做到只需要简单的更改一个配置文件,从而让多个服务器都能够拿到最新的信息。

其实这也是利用了ZooKeeper的Watcher监听来完成的,先在ZooKeeper中创建一个节点,然后将数据库的配置信息放在这个节点中,让其它的服务器监听这个节点的数据变化,当该节点的内容发生变化时就重新读取。

负载均衡

当服务越来越多,规模越来越大时,对应的机器数量也越来越庞大,单靠人工来管理和维护服务及地址的配置信息,已经越来越困难。并且,依赖单一的硬件负载均衡设备或者使用LVS、Nginx等软件方案进行路由和负载均衡调度,单点故障的问题也开始凸显,一旦服务路由或者负载均衡服务器宕机,依赖其的所有服务均将失效。如果采用双机高可用的部署方案,使用一台服务器“stand by”,能部分解决问题,但是鉴于负载均衡设备的昂贵成本,已难以全面推广。  一旦服务器与ZooKeeper集群断开连接,节点也就不存在了,通过注册相应的watcher,服务消费者能够第一时间获知服务提供者机器信息的变更。利用其znode的特点和watcher机制,将其作为动态注册和获取服务信息的配置中心,统一管理服务名称和其对应的服务器列表信息,我们能够近乎实时地感知到后端服务器的状态(上线、下线、宕机)。Zookeeper集群间通过Zab协议,服务配置信息能够保持一致,而Zookeeper本身容错特性和leader选举机制,能保证我们方便地进行扩容。  Zookeeper中,服务提供者在启动时,将其提供的服务名称、服务器地址、以节点的形式注册到服务配置中心,服务消费者通过服务配置中心来获得需要调用的服务名称节点下的机器列表节点。通过前面所介绍的负载均衡算法,选取其中一台服务器进行调用。当服务器宕机或者下线时,由于znode非持久的特性,相应的机器可以动态地从服务配置中心里面移除,并触发服务消费者的watcher。在这个过程中,服务消费者只有在第一次调用服务时需要查询服务配置中心,然后将查询到的服务信息缓存到本地,后面的调用直接使用本地缓存的服务地址列表信息,而不需要重新发起请求到服务配置中心去获取相应的服务地址列表,直到服务的地址列表有变更(机器上线或者下线),变更行为会触发服务消费者注册的相应的watcher进行服务地址的重新查询。这种无中心化的结构,使得服务消费者在服务信息没有变更时,几乎不依赖配置中心,解决了之前负载均衡设备所导致的单点故障的问题,并且大大降低了服务配置中心的压力。   通过Zookeeper来实现服务动态注册、机器上线与下线的动态感知,扩容方便,容错性好,且无中心化结构能够解决之前使用负载均衡设备所带来的单点故障问题。只有当配置信息更新时服务消费者才会去Zookeeper上获取最新的服务地址列表,其他时候使用本地缓存即可,这样服务消费者在服务信息没有变更时,几乎不依赖配置中心,能大大降低配置中心的压力。

命名服务

命名服务是指通过指定的名字来获取资源或者服务的地址,提供者的信息。利用Zookeeper很容易创建一个全局的路径,而这个路径就可以作为一个名字,它可以指向集群中的集群,提供的服务的地址,远程对象等。简单来说使用Zookeeper做命名服务就是用路径作为名字,路径上的数据就是其名字指向的实体。

基于zookeeper的分布式锁

1. 排它锁

算法思路:利用名称唯一性,加锁操作时,只需要所有客户端一起创建/test/Lock节点,只有一个创建成功,成功者获得锁。解锁时,只需删除/test/Lock节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁。 特点:这种方案的正确性和可靠性是ZooKeeper机制保证的,实现简单。缺点是会产生“惊群”效应,假如许多客户端在等待一把锁,当锁释放时候所有客户端都被唤醒,仅仅有一个客户端得到锁。

2. 共享锁

算法思路:临时顺序节点实现共享锁 客户端调用create()方法创建名为“locknode/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL。 客户端调用getChildren(“locknode”)方法来获取所有已经创建的子节点,同时在这个节点上注册上子节点变更通知的Watcher。 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。 如果在步骤3中发现自己并非是所有子节点中最小的,说明自己还没有获取到锁,就开始等待,直到下次子节点变更通知的时候,再进行子节点的获取,判断是否获取锁。 特点:适合小集群分布式。集群大会耗时严重。

3. 非惊群效应共享锁

方案3: 算法思路:临时顺序节点实现共享锁的改进实现 对于加锁操作,可以让所有客户端都去/lock目录下创建临时顺序节点,如果创建的客户端发现自身创建节点序列号是/lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点),进入等待。 对于解锁操作,只需要将自身创建的节点删除即可。 特点:利用临时顺序节点来实现分布式锁机制其实就是一种按照创建顺序排队的实现。这种方案效率高,避免了“惊群”效应,多个客户端共同等待锁,当锁释放时只有一个客户端会被唤醒。

kafka

kafka详解

分布式一致算法

分布式锁的实现方案