本来想系统回顾下 ZooKeeper的,可是网上没找到一篇合自己胃口的文章,写的差不多的,感觉大部分都是基于《从Paxos到ZooKeeper 分布式一致性原理与实践》写的,所以自己读了一遍,加上项目中的使用,做个整理。加油,奥利给!
面试常常被要求「熟悉分布式技术」,当年搞 “XXX管理系统” 的时候,我都不知道分布式系统是个啥。分布式系统是一个硬件或软件组件分布在不同的网络计算机中上,彼此之间仅仅通过消息传递进行通信和协调的系统。
计算机系统从集中式到分布式的变革伴随着包括分布式网络、分布式事务、分布式数据一致性等在内的一系列问题和挑战,同时也催生了一大批诸如ACID、CAP和 BASE 等经典理论的快速发展。
为了解决分布式一致性问题,涌现出了一大批经典的一致性协议和算法,最为著名的就是二阶段提交协议(2PC),三阶段提交协议(3PC)和Paxos算法。Zookeeper的一致性是通过基于 Paxos 算法的 ZAB 协议完成的。一致性协议之前的文章也由介绍:「走进分布式一致性协议」从2PC、3PC、Paxos 到 ZAB,这里就不再说了。
ZooKeeper 官网是这么介绍的:”Apache ZooKeeper 致力于开发和维护一个支持高度可靠的分布式协调的开源服务器“
ZooKeeper 是 Apache 软件基金会的一个软件项目,它为大型「分布式计算」提供开源的分布式配置服务、同步服务和命名注册。
Zookeeper 最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上,Zookeeper 就这样诞生了。后来捐赠给了Apache ,现已成为 Apache 顶级项目。
关于“ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的Pig项目),雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家 RaghuRamakrishnan 开玩笑地说:“再这样下去,我们这儿就变成动物园了!”此话一出,大家纷纷表示就叫动物园管理员吧一一一因为各个以动物命名的分布式组件放在一起,雅虎的整个分布式系统看上去就像一个大型的动物园了,而 Zookeeper 正好要用来进行分布式环境的协调一一于是,Zookeeper 的名字也就由此诞生了。
ZooKeeper 是用于维护配置信息,命名,提供分布式同步和提供组服务的集中式服务。所有这些类型的服务都以某种形式被分布式应用程序使用。每次实施它们时,都会进行很多工作来修复不可避免的 bug 和竞争条件。由于难以实现这类服务,因此应用程序最初通常会跳过它们,这会使它们在存在更改的情况下变得脆弱并且难以管理。即使部署正确,这些服务的不同实现也会导致管理复杂。
ZooKeeper 的目标是将这些不同服务的精华提炼为一个非常简单的接口,用于集中协调服务。服务本身是分布式的,并且高度可靠。服务将实现共识,组管理和状态协议,因此应用程序不需要自己实现它们。
ZooKeeper 从设计模式角度来理解:就是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,ZK 就将负责通知已经在 ZK 上注册的那些观察者做出相应的反应,从而实现集群中类似 Master/Slave 管理模式。
图片来源:官网wiki
Zookeeper 数据模型的结构与 Unix 文件系统的结构相似,整体上可以看做是一棵树,每个节点称作一个 「ZNode」。每个 ZNode 默认能存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识。
ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能
统一命名服务
在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,进程对象等等——这些我们都可以统称他们为名字(Name)。其中较为常见的就是一些分布式服务框架(如RPC、RMI)中的服务地址列表。通过调用 Zookeeper 提供的创建节点的 API,能够很容易创建一个全局唯一的 path,这个 path 就可以作为一个名称。
阿里巴巴开源的分布式服务框架 Dubbo 就使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表。
数据发布与订阅(配置中心)
发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到 ZooKeeper 节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。
统一集群管理
所谓集群管理无在乎两点:是否有机器退出和加入、选举 Master。
管理节点
Master选举
在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这个master选举便是这种场景下的碰到的主要问题。
利用 Zookeeper 的强一致性,能够很好的保证在分布式高并发情况下节点的创建一定是全局唯一的,即:同时有多个客户端请求创建 /currentMaster 节点,最终一定只有一个客户端请求能够创建成功。Zookeeper 通过这种节点唯一的特性,可以创建一个 Master 节点,其他客户端 Watcher 监控当前 Master 是否存活,一旦 Master 挂了,其他机器再创建这样的一个 Master 节点,用来重新选举。
软负载均衡
分布式系统中,负载均衡是一种很普遍的技术,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。可以是硬件的负载均衡,如 F5,也可以是软件的负载,我们熟知的 Nginx,或者这里介绍的 Zookeeper。
分布式协调/通知
Zookeeper 中特有的 「Watcher」 注册与异步通知机制,能够很好的实现分布式环境下不同机器,甚至不同系统之间的协调和通知,从而实现对数据变更的实时处理。
使用方法通常是不同系统都对 ZK 上同一个 znode 进行注册,监听 znode 的变化(包括 znode 本身内容及子节点的),其中一个系统 update 了 znode,那么另一个系统能够收到通知,并作出相应处理。
分布式锁
分布式锁,这个主要得益于 ZooKeeper 为我们保证了数据的强一致性。
锁服务可以分为两类,一个是保持独占,另一个是控制时序。
个人感觉还是用 Redis 实现分布式锁更加方便。
PS:阿里中间件团队:“其实,ZK 并非天生就是为这些应用场景设计的,都是后来众多开发者根据其框架的特性,利用其提供的一系列API接口(或者称为原语集),摸索出来的典型使用方法。”
ZooKeeper 第三种部署方式:
计划写三篇的,第二篇会实战 coding,运用各种 API,到时候再装集群,本节先来个单机玩~~
2.1.1 安装前准备
2.1.2 配置修改
2.1.3 操作 Zookeeper
/usr/local/bin/java ZooKeeper JMX enabled by default Using config: /home/sync360/test/apache-zookeeper-3.5.7-bin/bin/../conf/zoo.cfg Starting zookeeper ... STARTED
4020 Jps 4001 QuorumPeerMain
/usr/local/bin/java ZooKeeper JMX enabled by default Using config: /home/apache-zookeeper-3.5.7-bin/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Mode: standalone
Connecting to localhost:2181 2020-03-25 15:41:19,112 [myid:] - INFO [main:Environment@109] - Client environment:zookeeper.version=3.5.7-f0fdd52973d373ffd9c86b81d99842dc2c7f660e, built on 02/10/2020 11:30 GMT ... 2020-03-25 15:41:19,183 [myid:] - INFO [main:ClientCnxn@1653] - zookeeper.request.timeout value is 0. feature enabled= Welcome to ZooKeeper! ... WATCHER:: WatchedEvent state:SyncConnected type:None path:null
ls 查看当前 zk 中所包含的内容
[zk: localhost:2181(CONNECTED) 1] ls / [lazyegg, zookeeper]
create 创建一个新的 znode
[zk: localhost:2181(CONNECTED) 2] create /test Created /test
get 查看新的 znode 的值
[zk: localhost:2181(CONNECTED) 4] get /test null
可以看到值为 null,我们刚才设置了一个没有值的节点,也可以通过 create /zoo dog 直接创建有内容的节点
set 对 zk 所关联的字符串进行设置
set /test hello
delete 删除节点
delete /test
在 Zookeeper 的设计中,如果是集群模式,那所有机器上的 zoo.cfg 文件内容应该都是一致的。
Zookeeper 中的配置文件 zoo.cfg 中参数含义解读如下:
这里引入一个简单的例子,逐个介绍一些 ZK 中的概念。
在分布式系统中经常会遇到这种情况,多个应用读取同一个配置。例如:Client1,Client2 两个应用都会读取配置 B 中的内容,一旦 B 中的内容出现变化,就会通知 Client1 和 Client2。
一般的做法是在 Client1,Client2 中按照时钟频率询问 B 的变化,或者使用观察者模式来监听 B 的变化,发现变化以后再更新两个客户端。那么 ZooKeeper 如何协调这种场景?
这两个客户端连接到 ZooKeeper 的服务器,并获取其中存放的 B。保存 B 值的地方在 ZooKeeper 服务端中就称为 ZNode。
3.1 数据节点(Znode)
在谈到分布式的时候,我们通常说的“节点"是指组成集群的每一台机器。然而,在 Zookeeper 中,“节点"分为两类,第一类同样是指构成集群的机器,我们称之为「机器节点」;第二类则是指数据模型中的数据单元,我们称之为「数据节点」一一ZNode。上图中的 A、B 就是一个数据结点。
Zookeeper 将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)进行分割的路径,就是一个 Znode,例如 /Configuration/B。每个 Znode 上都会保存自己的数据内容,同时还会保存一系列属性信息。
在 Zookeeper 中,Znode 可以分为持久节点和临时节点两类。
另外,ZooKeeper 还允许用户为每个节点添加一个特殊的属性:**SEQUENTIAL。**也被叫做顺序结点,一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper 会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。
3.2 事件监听器(Watcher)
上面说了 ZooKeeper 用来存放数据的 ZNode,并且把 B 的值存储在里面。如果 B 被更新了,两个客户端(Client1、Client2)如何获得通知呢?
Zookeeper 允许用户在指定节点上注册一些 Watcher,当 Znode 发生变化时,将触发并删除一个 watch。当 watch 被触发时客户端会收到一个数据包,指示 znode 已经被修改。如果客户端和 ZooKeeper 服务器之间的连接中断,客户端将收到本地通知。该机制是 Zookeeper 实现分布式协调服务的重要特性。
3.6.0中的新增功能:客户端还可以在 znode 上设置永久性的递归监视,这些监视在触发时不会删除,并且会以递归方式触发已注册 znode 以及所有的 znode 的更改。
ZooKeeper 客户端(Client)会在指定的节点(/Configuration/B)上注册一个 Watcher,ZNode 上的 B 被更新的时候,服务端就会通知 Client1 和 Client2。
3.3 版本
有了 Watcher 机制,就可以实现分布式协调/通知了,假设有这样的场景,两个客户端同时对 B 进行写入操作,这两个客户端就会存在竞争关系,通常需要对 B 进行加锁操作,ZK 通过 version 版本号来控制实现乐观锁中的“写入校验”机制。
Zookeeper 的每个 ZNode 上都会存储数据,对应于每个 ZNode,Zookeeper 都会为其维护一个叫作 Stat 的数据结构,Stat 中记录了这个 ZNode 的三个数据版本,分别是 version(当前ZNode的版本)、cversion(当前ZNode 子节点的版本)和 aversion(当前ZNode的ACL版本)。
znode 里都有些啥呢?
3.4 Stat 结构体
Znodes 维护了一个 stat 结构,其中包含数据更改、ACL更改的版本号、时间戳等。
3.5 会话(Session)
Session 指的是 ZooKeeper 服务器与客户端会话。
在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接。客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 Zookeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watch 事件通知。
Session 作为会话实体,用来代表客户端会话,其包括 4 个属性:
3.6 ACL
Zookeeper 采用 ACL(Access Control Lists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。Zookeeper 定义了如下 5 种权限:
其中尤其需要注意的是,CREATE 和 DELETE 这两种权限都是针对子节点的权限控制。
3.7 集群角色
最典型集群模式:Master/Slave 模式(主备模式)。在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和Observer 三种角色。
server 状态
zk-vote
Zookeeper 中最有特色且最不容易理解的是监视(Watches)。
Zookeeper 所有的读操作——getData(),getChildren(), 和 exists() 都可以设置监视(watch),监视事件可以理解为一次性的触发器, 官方定义如下:a watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes。对此需要作出如下理解:
Zookeeper 中的监视是轻量级的,因此容易设置、维护和分发。当客户端与 Zookeeper 服务器端失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说这通常是透明的。只有一种情况会导致监视事件的丢失,即:通过 exists() 设置了某个 znode 节点的监视,但是如果某个客户端在此 znode 节点被创建和删除的时间间隔内与 zookeeper 服务器失去了联系,该客户端即使稍后重新连接 zookeepe r服务器后也得不到事件通知。
图片来源:yht7
从上图可以看到,Watcher 机制包括三个角色:客户端线程、客户端的 WatchManager 以及 ZooKeeper 服务器。Watcher 机制就是这三个角色之间的交互,整个过程分为注册、存储和通知三个步骤: