zookeeper

前言

在工作中经常听到它的名字,总感觉它无处不在,也知道它大概的作用,但具体到每一个项目上,到底依赖它做了些什么?起了哪些作用?有什么特性?都有哪些使用场景?接下来就让我们一起了解一下这个神奇的“宇宙中心”。

由来

ZooKeeper是一个分布式应用程序协调服务,最早起源于雅虎研究院的一个研究小组。在当时,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。 所以,开发人员就试图开发一个通用的无单点问题的分布式协调框架, Zookeeper 就由此诞生了。

并具备以下读写特性:

最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。

可靠性:具有简单、健壮、良好的性能,如果消息被到一台服务器接受,那么它将被所有的服务器接受。

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

等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。

原子性:更新只能成功或者失败,没有中间状态。

顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。

使用场景

ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如 数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列 等功能。

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

服务生产者将自己提供的服务注册到 ZooKeeper 中心,服务的消费者在进行服务调用的时候先到 ZooKeeper 中查找服务,获取到服务生产者的详细信息之后,再去调用服务生产者的内容与数据。

概念介绍

集群

ZooKeeper可以很方便的进行集群部署,可参考文章中部署的章节。客户端通过与集群中的某一台机器建立 TCP 连接来使用服务。zk客户端会打散配置文件中的serverAddress 顺序并随机组成新的list,然后循环按序取一个服务器地址进行连接,直到成功。客户端使用这个 TCP 链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。

ZooKeeper 官方提供的架构图

角色

最典型集群模式:Master/Slave 模式(主备模式)在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三种角色。

角色关系图

集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器。

Leader 可为客户端提供读写服务。Follower 和 Observer 只能提供读服务。

Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。

ZNode

在谈到分布式的时候,我们通常说的“节点"是指组成集群的每一台机器。然而,在 ZooKeeper 中,“节点"分为两类:

第一类同样是指构成集群的机器,我们称之为机器节点。

第二类则是指数据模型中的数据单元,我们称之为数据节点一ZNode。

那数据节点又应该如何理解?如下图所示,ZooKeeper中保存的为节点数据,每个节点可以包含子节点,又包含自身的数据信息,可以理解成既是Unix里的文件,又是Unix里的目录。因为每个 ZNode 不仅本身可以写数据(相当于Unix里的文件),还可以有下一级文件或目录(相当于Unix里的目录)

ZooKeeper 将所有数据存储在内存中,数据模型是一棵树(Znode Tree)

节点又有以下4种类型

  • PERSISTENT持久节点,一直存在的
  • PERSISTENT_SEQUENTIAL持久序号节点,不会重名的节点,可用于分布式锁
  • EPHEMERAL临时节点(不可在拥有子节点),当客户端退出时会删除。可用于心跳监听
  • EPHEMERAL_SEQUENTIAL临时序号节点(不可在拥有子节点)

每个节点除了保存数据以外还会有额外的一些固定属性

cZxid = 0x40 创建时事务ID,持久不变的数值。 
ctime = Sat Sep 28 10:39:45 CST 2019 创建时间 
mZxid = 0x47 当前数据变更时的事务ID,不包含子节点 
mtime = Sat Sep 28 10:43:26 CST 2019 最后修改时间 
pZxid = 0x41 子节点变更的事务ID,不包含子节点的数据变更,只是子节点的数量变化 
cversion = 1 子节点变更版本号(子节点变更次数) 
dataVersion = 1 数据版本号,变更次数 
aclVersion = 0 权限版本号,变更次数 
ephemeralOwner = 0x0 是否为临时节点,有值表示临时节点 
dataLength = 4 当前节点的数据长度 
numChildren = 1 子节点的数量

Watcher

事件监听器是 ZooKeeper 中的一个很重要的特性。

ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。

ACL权限控制

ACL全称为Access Control List(访问控制列表),用于控制资源的访问权限。基于 scheme:id:permission 的方式进行权限控制。scheme表示授权模式、id模式对应值、permission即具体的增删改权限位。

scheme:认证模型分为四种:

world开放模式,world表示全世界都可以访问(这是默认设置)

ip,ip模式,限定客户端IP防问。

auth用户密码认证模式,只有在会话中添加了认证才可以防问

digest与auth类似,区别在于auth用明文密码,而digest 用sha-1+base64加密后的密码。在实际使用中digest 更常见。

permission权限位:

c->CREATE可以创建子节点

d->DELETE可以删除子节点(仅下一级节点)

r->READ可以读取节点数据及显示子节点列表

w->WRITE可以设置节点数据

a->ADMIN可以设置节点访问控制列表权限

原理简述

顺序访问

对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号。这个编号反应了所有事务操作的先后顺序,这个编号也叫做时间戳—zxid(ZooKeeper Transaction Id)。

Leader服务器的选举流程

当集群中不存在Leader服务器时集群会进行Leader服务器的选举,这通常存在于两种情况:1.集群刚启动时 2.集群运行时,但Leader服务器因故退出。集群中的服务器会向其他所有的Follower服务器发送消息,这个消息可以形象化的称之为选票,选票主要由两个信息组成,所推举的Leader服务器的ID(即配置在myid文件中的数字),以及该服务器的事务ID,事务表示对服务器状态变更的操作,一个服务器的事务ID越大,则其数据越新。整个过程如下所述:

  1. Follower服务器投出选票(SID,ZXID),第一次每个Follower都会推选自己为Leader服务器,也就是说每个Follower第一次投出的选票是自己的服务器ID和事务ID。

  2. 每个Follower都会接收到来自于其他Follower的选票,它会基于如下规则重新生成一张选票:比较收到的选票和自己的ZXID的大小,选取其中最大的;若ZXID一样则选取SID即服务器ID最大的。最终每个服务器都会重新生成一张选票,并将该选票投出去。

这样经过多轮投票后,如果某一台服务器得到了超过半数的选票,则其将当前选为Leader。由以上分析可知,Zookeeper集群对Leader服务器的选择具有偏向性,偏向于那些ZXID更大,即数据更新的机器。

数据一致性

Zookeeper采用ZAB(Zookeeper Atomic Broadcast)协议来保证分布式数据一致性。Zab协议有两种模式, 崩溃恢复(选主+数据同步)和消息广播(事务操作)。任何时候都需要保证只有一个主进程负责进行事务操作,而如果主进程崩溃了,就需要迅速选举出一个新的主进程。主进程的选举机制与事务操作机制是紧密相关的。

消息广播(事务操作)

ZooKeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理。Leader 服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议。而如果集群中的其他机器接收到客户端的事务请求,那么这些非 Leader 服务器会首先将这个事务请求转发给 Leader 服务器。leader生成提议并广播给followers,收到半数以上的ACK后,再广播commit消息,同时将事务操作应用到内存中。follower收到提议后先将事务写到本地事务日志,然后反馈ACK,等接到leader的commit消息时,才会将事务操作应用到内存中。

可见,选主只是选出了内存数据是最新的节点,仅仅靠这个是无法保证已经在leader服务器上提交的事务最终被所有服务器都提交。

  • 比如leader发起提议P1,并收到半数以上follower关于P1的ACK后,在广播commit过程中有读操作,内存还 没写入数据,这时将由client与server一起保障一致性。client 会记录它见过的最大的 zxid (在你的场景下,就是这条写入 的 zkid),读取的时候,如果 server 发现 这条 zxid 比 server 端的最大 zxid 大,则拒绝,client 会自动重连到其他server(还在同一个 session) —— 最终会落到有新数据的 server 上,因为半数已经同意;

  • 比如leader发起提议P1,并收到半数以上follower关于P1的ACK后,在广播commit消息之前宕机了,选举产生的新leader之前是follower,未收到关于P1的commit消息,内存中是没有P1的数据。而ZAB协议的设计是需要保证选主后,P1是需要应用到集群中的。这块的逻辑是通过选主后的数据同步(崩溃恢复)来弥补。

崩溃恢复(选主+数据同步)

当整个服务框架在启动过程中,或当 Leader 服务器出现异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。选主后,节点需要切换状态,leader切换成LEADING状态后的流程如下:

  1. 重新加载本地磁盘上的数据快照至内存,并从日志文件中取出快照之后的所有事务操作,逐条应用至内存,并添加到已提交事务缓存commitedProposals。这样能保证日志文件中的事务操作,必定会应用到leader的内存数据库中。

  2. 获取learner发送的FOLLOWERINFO/OBSERVERINFO信息,并与自身commitedProposals比对,确定采用哪种同步方式,不同的learner可能采用不同同步方式(DIFF同步、TRUNC+DIFF同步、SNAP同步)。这里是拿learner内存中的zxid与leader内存中的commitedProposals(min、max)比对,如果zxid介于min与max之间,但又不存在于commitedProposals中时,说明该zxid对应的事务需要TRUNC回滚;如果 zxid 介于min与max之间且存在于commitedProposals中,则leader需要将zxid+1~max 间所有事务同步给learner,这些内存缺失数据,很可能是因为leader切换过程中造成commit消息丢失,learner只完成了事务日志写入,未完成提交事务,未应用到内存。

  3. leader主动向所有learner发送同步数据消息,每个learner有自己的发送队列,互不干扰。同步结束时,leader会向learner发送NEWLEADER指令,同时learner会反馈一个ACK。当leader接收到来自learner的ACK消息后,就认为当前learner已经完成了数据同步,同时进入“过半策略”等待阶段。当leader统计到收到了一半已上的ACK时,会向所有已经完成数据同步的learner发送一个UPTODATE指令,用来通知learner集群已经完成了数据同步,可以对外服务了。

当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播。那么新加入的服务器就会自觉地进人数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

为什么最好使用奇数台服务器构成 ZooKeeper 集群?

ZooKeeper 中 Leader 选举算法采用了 Zab 协议。Zab 核心思想是当多数 Server 写成功,则任务数据写成功:

  • 如果有 3 个 Server,则最多允许 1 个 Server 挂掉。
  • 如果有 4 个 Server,则同样最多允许 1 个 Server 挂掉。
  • 既然 3 个或者 4 个 Server,同样最多允许 1 个 Server 挂掉,那么它们的可靠性是一样的。
  • 所以选择奇数个 ZooKeeper Server 即可,这里选择 3 个 Server。

安装(伪集群模式)

下载并解压:

http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.5.6/
tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz

注意下载 -bin.tar.gz ,这个包才是可执行的,下面的那个是源码包没有二进制文件

配置

为了模拟集群,根据上面所讲,集群数量推荐为奇数,所以我们先将zk包复制成3份

mv apache-zookeeper-3.5.6-bin zookeeper1
cp -r  zookeeper1  zookeeper2
cp -r  zookeeper1  zookeeper3

进行差异化配置(我只列举zookeeper1的配置方法)

cd zookeeper1
mkdir dataDir    - 用于存放zk数据
echo 1 > ./dataDir/myid  - 每个集群节点需要一个唯一标识。这个唯一标识要求是自然数

vi ./conf/zoo.cfg

文尾加入集群配置
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

修改客户端端口号
clientPort=2181 (因为我们是伪集群,所以不同集群节点使用不同端口号,避免冲突)
修改数据目录dataDir=/xxx/xxx/zookeeper1/dataDir (就是刚刚我们创建的文件夹)

各个端口的作用
2181:对cline端提供服务
3888:选举leader使用
2888:集群内机器通讯使用(Leader监听此端口)

启动

./zookeeper1/bin/zkServer.sh start
./zookeeper2/bin/zkServer.sh start
./zookeeper3/bin/zkServer.sh start

查看服务状态

./zookeeper1/bin/zkServer.sh status
./zookeeper2/bin/zkServer.sh status
./zookeeper3/bin/zkServer.sh status

可以根据mode查看各集群节点身份

特性体验

选举

zookeeper的核心特点就是分布式无中心化,我们先将leader (zookeeper2)集群节点关闭
./zookeeper2/bin/zkServer.sh stop


然后查看状态发现集群已经将zookeeper3选为leader

集群数据同步

使用任意一个节点的客户端进行登录,还记得刚才我们安装环境设置的三个clientPort吗?

登录集群节点1
./zookeeper1/bin/zkCli.sh -server 127.0.0.1:2181

创建测试数据
create /foo foo

登录集群节点3 
./zookeeper1/bin/zkCli.sh -server 127.0.0.1:2183

查看数据是否同步
ls /
get /foo

集群数据同步:通过节点1创建的数据在节点3中被正确查询到

数据节点WATCH(重要)

上面已经提到watch是zk的一个重要的特性,基于此才衍生了众多的应用场景,现在用cli模拟一下。

watch监控需要使用 -w 参数,cli中监控只一次有效

值监控
get -w /foo

节点监控
ls -w /foo

1.添加监控 2.改变值 3.接到事件并获取最新的值

数据节点配额

设置某个数据节点的子节点个数和其本身的数据长度

setquota -n|-b val path
-n 限制此数据节点最大可拥有多少个子节点(包含自身)
-b 限制此数据节点能够存储的数据最大是多少个字节

创建数据节点、并增加配额限制

这条日志是在创建/foo/c的时候打印的,/foo/a、/foo/b、/foo/c,再加上/foo,共4个节点,超出限制,所以进行日志报警

ps:zookeeper的quota并没有实际的限制作用,超出了也只是打印WARN级别日志。

查看数据节点的配额限制
listquota path
quota 为限制  stat 为目前使用量

删除配额限制
delquota [-n|-b] path
ps:需要注意的是删除数据节点时并不会自动删除绑定在特定路径上的quota,需要手动删除。

数据节点属性

我们可以通过stat [path]命令来查看节点的属性。

acl权限

上面我们已经对acl做了介绍,忘记了的,往上翻一下。我们来看一下几个权限设置的小例子。

获取数据节点当前权限
getAcl /foo

设置权限-world模式
setAcl /root world:anyone:cdrwa

设置权限-ip模式
setAcl / ip:127.0.0.1:ra

cli help详解

addauth scheme auth 添加用户,语法在后面权限内会详细去说。

close 断开当前客户端和服务端的连接

config [-c] [-w] [-s] 动态加载配置

connect host:port 连接到客户端

create [-s] [-e] [-c] [-t ttl] path [data] [acl] 创建节点 -e 临时节点(不允许有子节点) -s 序列节点 -c 默认节点  (path为路径)  【data为数据】 【acl权限】 -t节点存活时间,我这未成功。有弄明白-t的小伙伴帮我一下,留言就行。

delete [-v version] path 删除节点(不能带有子节点) [-v version] 版本号,一般不用

deleteall path 删除节点(包含其子节点)

delquota [-n|-b] path 删除节点限额 -n 子节点数 -b 字节数

get [-s] [-w] path 取得节点的值 -s 取值和状态,-w 添加监听(监听数据)

getAcl [-s] path取得限权,-s取权限和状态

history 历史操作记录

listquota path 查看节点限额

ls [-s] [-w] [-R] path查看节点  -s查看节点和内容  -w(添加监听是否添加或删除子节点,但不会监听子节点值的变化) -R查到所有节点(包含根节点)

ls2 path [watch] 相当于ls和stat的组合

printwatches on|off 是否打印监听事件

quit 退出当前客户端

reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*] 重新加载配置文件

redo cmdno 重新操作某命令,与history配合使用

removewatches path [-c|-d|-a] [-l] 移除监听

rmr path 删除和deleteAll完全一致(已被deleteAll替换,不建议继续使用)

set [-s] [-v version] path data 给节点赋值 -s返回节点状态

setAcl [-s] [-v version] [-R] path acl 设置节点权限(后面会详细说一下这个)

setquota -n|-b val path 设置节点限额 -n 子节点数 -b 字节数

stat [-w]path 查看节点状态,-w同get -w用法

sync path强制同步

总结

  1. Zookeeper是一个由多个server组成的集群(高可用)
  2. 选举出一个leader,多个follower (去中心化)
  3. 分布式读写 » 更新请求转发,由leader实施 (读写分离)
  4. 每个server保存一份数据副本 » 全局数据一致 (数据一致性)
  5. 更新请求顺序进行,来自同一个client的更新请求按其 发送顺序依次执行(顺序访问)
  6. 数据更新原子性,一次数据更新要么成功,要么失败
  7. 全局唯一数据视图,client无论连接到哪个server,数据 视图都是一致的
  8. 实时性,在一定事件范围内,client能读到最新数据
  9. 支持watch监听 、acl权限、 znode配额限制

至此对于zookeeper的由来、特性、原理、应用场景基本都了解清楚了,也进行了安装与基础特性的体验,对client cli也进行的演示,当真正需要使用它去做一些实际应用时,再去探究一些细节就可以了。再后续的文章也会针对各系统对zookeeper的应用做进一步阐述~

下章预告

经过对zookeeper系统的了解后,下章我们将对其最基本的应用 “服务注册与发现” 进行模拟实现~

posted @ 2019-12-27 17:01  LinPeng_bky  阅读(168)  评论(0编辑  收藏  举报