ps.本文为《从Paxos到Zookeeper 分布式一致性原理与实践》笔记之一
1 ZooKeeper
ZooKeeper曾是Apache Hadoop的一个子项目,是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、master选举、分布式锁和分布式队列等。
ZooKeeper是Google的Chubby一个开源的实现,由雅虎创建,是Hadoop和Hbase的重要组件。
ZooKeeper没有直接采用paxos算法,而是采用了一种被称为ZAB(Zookeeper Atomic Broadcast)的一致性协议
ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
1.1 ZooKeeper可以保证如下分布式一致性特性
顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中;
原子性:所有事务的请求结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么在整个集群中所有机器上都成功应用了某一个事务,要么都没有应用,没有中间状态;
单一视图:无论客户端连接的是哪个Zookeeper服务器,其看到的服务端数据模型都是一致的。
可靠性:一旦服务端成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
实时性:Zookeeper仅仅保证在一定的时间内,客户端最终一定能够从服务端上读到最终的数据状态。
1.2 ZooKeeper的四个设计目标
zk致力于提供一个高性能、高可用、且具有严格的顺序访问控制能力(主要是写操作)的分布式协议,最后者能使得zk能实现一些复杂的同步语义。
简单的数据模型:能够通过一个共享的、树型结构的名字空间来进行相互协调。这里是树形结构的名字空间是指zk服务器内存中的一个数据结构,由其一系列的ZNode数据节点组成,他们的层级关系就像文件系统的目录结构,不过zk将其全量数据存储在内存中,以达到高吞吐。
可以构建集群:zk集群通常由一组机器组成,集群中的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都保持着通信。集群中只要超过一半的机器可以正常工作,zk就可以对外提供服务。zk客户端会选择和集群中任意一台机器共同来创建一个tcp链接,如果连接断开,客户端会自动连接到服务机器的其他机器。
顺序访问:对于来自客户端的每个更新请求,Zookeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序。
高性能:Zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景。
2 ZooKeeper的基本概念
2.1 集群角色
- 最典型的集群就是Master/Slave模式(主备模式),此情况下把所有能够处理写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器为Slave机器。
- Zookeeper不采用主备模式,它引入了Leader、Follower、Observer三种角色,Zookeeper集群中的所有机器通过Leaser选举过程来选定一台被称为Leader的机器,Leader服务器为客户端提供读和写服务,Follower和Observer提供读服务,但是Observer不参与Leader选举过程,不参与写操作的”过半写成功”策略,Observer可以在不影响写性能的情况下提升集群的性能。
leader: 是整个集群工作机制中的核心,其主要工作有:
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
- 集群内部各服务器的调度者。
follower:是zookeeper集群状态的跟随者,其主要工作是:
- 处理客户端的非事务请求,转发事务请求给leader服务器。
- 参与事务请求proposal的投票
- 参与leader选举投票
observer:和follower唯一的区别在于,observer服务器只提供非事务服务,不参与任何形式的投票,包括事务请求proposal的投票和leader选举投票。
- 通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。
2.2 会话
- 指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP长连接,Zookeeper对外的服务端口默认为2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知。
2.3 数据节点
- 我们常说的节点指的是集群中的机器节点,zk中节点有两类,第一类指构成集群的机器,称为机器节点,第二类是指数据模型中的数据单元,称为数据节点-Znode,Zookeeper将所有数据存储在内存中,数据模型是一棵树,由斜杠/进行分割的路径,就是一个ZNode,如/foo/path1,每个ZNode都会保存自己的数据内存,同时还会保存一些列属性信息。
- ZNode分为持久节点和临时节点两类,持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上,而临时节点的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
- 另外,Zookeeper还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点后面追加一个整形数字,其是由父节点维护的自增数字。
- 临时,持久和顺序,如此,数据节点一共有四种类型:
- 数据节点中除了有数据内容外,还有一个stat对象来记录节点的状态信息:
2.5 版本
- 对于每个ZNode,Zookeeper都会为其维护一个叫作Stat的数据结构,Stat记录了这个ZNode的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)、aversion(当前ZNode的ACL版本)。
- 类似于乐观锁和cas,保证原子性操作
2.6 事务操作
- 在ZooKeeper中,能改变ZooKeeper服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作。对应每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用ZXID表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些事务操作请求的全局顺序。
2.7 watcher
- watcher是事件监听器,Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端。该机制是zk实现分布式协调服务的重要特性。
- 其逻辑如下图:
- 当客户端向服务端注册watcher时,也会将watcher对象存储在客户端的watcherManager中,当服务端出发watcher时间后,向客户端发送通知,客户端从watcherManager中去处watcher对象来执行回调逻辑。
2.8 ACL
Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,类似于unix文件系统的权限控制, - 权限模式
- ip模式
- 通过ip地址粒度来进行权限控制。
- digest
- 类似于username:password形式配置权限标识来控制,
- world
- 即任何人都可以访问
- super
- 一种特殊的digest模式,只不过权限是超级用户。
- ip模式
- 权限:
- CREATE:创建子节点的权限。
- READ:获取节点数据和子节点列表的权限。
- WRITE:更新节点数据的权限。
- DELETE:删除子节点的权限。
- ADMIN:设置节点ACL的权限。
- 值得注意的是,CREATE和READ都是针对子节点的权限控制。
3 ZAB协议
见文章《ZAB协议分析》一文
4 Zookeeper应用场景
4.1 数据发布/订阅
发布订阅系统,即所谓的配置中心,发布者将数据发布到zk的一个或者一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的。
常规的发布订阅系统有push和pull两种模式,在push模式中,服务端主动将数据更新发送给所有订阅的客户端。pull模式中,客户端通常采用盯视轮询拉取的方式发送请求来服务端获取。
zk的发布订阅系统采用推拉结合的模式,客户端向服务端祖册自己需要关注的数据节点,一旦该节点的数据(也就是配置信息)发生变更,服务端会发送watcher时间通知客户端来获取数据。
4.2 负载均衡
- 这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。
- 消息中间件中发布者和订阅者的负载均衡,linkedin开源的KafkaMQ和阿里开源的metaq都是通过zookeeper来做到生产者、消费者的负载均衡。我们以kafka为例子。
4.2.1 kafka概念:
- 消息生产者:producer
- 消息消费者:consumer
- 主题:即Topic,由用户定义并配置在Kafka服务端,用于建立生产者和消费者之间的订阅关系:生产者发送消息到指定Topic下,消费者从这个Topic下消费消息。
- Broker:即Kafka服务器,用于存储消息,在消息中间件中通常被称为Broker
消费者分组:Group - Offset:消息存储在Kafka的Broker上,消费者拉取消息数据的过程中需要知道消息在文件中的偏移量,这个偏移量就是所谓的Offset
- ZooKeeper负责管理所有Broker服务器列表,并且建立了对应路径来对其进行管理/brokers/ids
每个Broker服务器在启动时,都会到ZooKeeper上进行注册,其节点路径为/broker/ids/[0…N] - Topic注册:Kafka当中,会将同一个Topic的消息分成多个区,分布到多个Broker上,这些分区信息和Broker的对应关系由ZooKeeper来维护
4.2.2 ZooKeeper负载均衡实现
- 每当一个Broker启动时,会首先完成Broker注册过程,在ZooKeeper的节点列表里保存Broker。
- Kafka的生产者会对ZooKeeper上的“Broker的新增与减少”、“Topic的新增和减少”和“Broker和Topic关联关系的变化”等事件注册Watcher监听
- 通过ZooKeeper的Watcher通知能够让生产者动态的获取Broker和Topic的变化情况
- Kafka有消费者分组的概念,每个消费者分组包含了若干个消费者,每一条消息只会发送给分组内的一个消费者,不同消费者分组消费自己特定的Topic下面的消息,互不干扰
- Kafka会为每个消费者分配全局唯一的Consumer ID,采用“Hostname:UUID”形式来表示
- 每个消费者一旦确定了对一个消息分区的消费权利,ZooKeeper会将其Consumer ID写入到对应消息分区的临时节点上
- 消费进度管理:Kafka需要定时地将分区消息的消费进度,即Offset记录到ZooKeeper上去
4.3 命名服务
- 命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。
- 其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。
- 阿里巴巴集团开源的分布式服务框架Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表,在Dubbo实现中
- 服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。
- 服务消费者启动的时候,订阅/dubbo/${serviceName}/providers目录下的提供者URL地址, 并向/dubbo/${serviceName} /consumers目录下写入自己的URL地址。
- 注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。
- 另外,Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName}目录下所有提供者和消费者的信息。
4.4 分布式通知/协调
- ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理。
- 心跳检测式通知/协调:检测系统和被检测系统之间并不直接关联起来,而是通过zk上某个节点关联,比如两个客户端都在一个节点下创建各自的临时节点,并定时心跳去检测对方节点的存在,达到心跳检测的目的。
- 系统调度式通知/协调:有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了ZK上某些节点的状态,而ZK就把这些变化通知给他们注册Watcher的客户端,即推送系统,于是,作出相应的推送任务。
- 工作汇报式通知/协调:一些类似于任务分发系统,子任务启动后,到zk来注册一个临时节点,并且定时将自己的进度进行汇报(将进度写回这个临时节点),这样任务管理者就能够实时知道任务进度。
- 总之,使用zookeeper来进行分布式通知和协调能够大大降低系统之间的耦合
4.5 集群管理
- 通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器,或者每个机器自己定时向监控系统汇报“我还活着”。 这种做法可行,但是存在两个比较明显的问题:
- 集群中机器有变动的时候,牵连修改的东西比较多。
- 有一定的延时。
- 利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统:
- 客户端在节点x上注册一个Watcher,那么如果 x的子节点变化了,会通知该客户端。
- 创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,那么该节点就会消失。
- 例如,监控系统在 /clusterServers 节点上注册一个Watcher,以后每动态加机器,那么就往 /clusterServers 下创建一个 EPHEMERAL类型的节点:/clusterServers/{hostname}. 这样,监控系统就能够实时知道机器的增减情况,至于后续处理就是监控系统的业务了。
4.6 Master选举
- 在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这个master选举便是这种场景下的碰到的主要问题。
- 利用ZooKeeper的强一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建同名的如名字为 /currentMaster的节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选取了。
4.6.1 动态master选举
- 上述场景演化一下,就是动态Master选举。这就要用到EPHEMERAL_SEQUENTIAL类型节点的特性了。上文中提到,所有客户端创建请求,最终只有一个能够创建成功。在这里稍微变化下,创建EPHEMERAL_SEQUENTIAL节点,于是所有的请求最终在ZK上创建结果的一种可能情况是这样
- /currentMaster/{sessionId}-1 ,/currentMaster/{sessionId}-2 ,/currentMaster/{sessionId}-3
- 每次选取序列号最小的那个机器作为Master,如果这个机器挂了,由于他创建的节点会马上消失,那么之后最小的那个机器就是Master了。
4.7 分布式锁
- 分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
- 所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
- 控制时序,就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL来指定)。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
4.8 分布式队列
- 队列方面,简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。对于第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致,这里不再赘述。
- 第二种队列其实是在FIFO队列的基础上作了一个增强。通常可以在 /queue 这个znode下预先建立一个/queue/num 节点,并且赋值为n(或者直接给/queue赋值n),表示队列大小,之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。
- 这种用法的典型场景是,分布式环境中,一个大任务Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去 /taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。