本文 3万多字, 1千多行, 阅读完毕需要花费一些时间, 仅用作个人学习使用!
ZooKeeper 是 Apache 软件基金会的一个软件项目,它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 ZooKeeper 实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。ZooKeeper 现在是一个独立的顶级项目,曾经是 Hadoop 的一个子项目。
集群:将一个任务部署在多个服务器,每个服务器都能独立完成该任务。例如:饭店后厨有三个厨师,他们每个人都会洗菜、切菜和炒菜,即使饭店同时来了很多客人也能轻松应对,这就是集群。
分布式:将一个任务拆分成若干个子任务,由若干个服务器分别完成这些子任务,每个服务器只能完成某个特定的子任务。例如:饭店后厨有三个厨师,洗菜、切菜和炒菜三个子任务分别由每个人独立完成,一个人洗菜,一个人切菜,一个人炒菜,这就是分布式。
从概念上就可以看出两者最主要的区别就是分布式是将一种业务拆分成多个子业务部署在多台服务器上,进而对外提供服务;而集群就是将多台服务器组合在一起提供同一种服务。
集群强调在多台服务器位置集中,并且容易统一管理;而分布式没有具体要求,不论放置在哪个位置,只要通过网络连接起来就行。
集群是一种物理形态,即多台服务器在一起提供一种服务;而分布式是一种工作方式,即一个程序或业务分解到多台服务器分别完成。
总结:集群是通过提高单位时间内执行的任务数来提升效率,分布式是以缩短单个任务的执行时间来提升效率。
通俗地讲,“单体应用(monolith application)”就是将应用程序的所有功能都打包成一个独立的单元。当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心。当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。
PS :
从软件设计的角度上来说,ESB 是一个抽象的间接层,提取了服务调用过程中调用与被调用动态交互中的一些共同的东西,减轻了服务调用者的负担。Java 编程思想里提到:“所有的软件设计的问题都可以通过增加一个抽象的间接层而得到解决或者得到简化!”简单来说 ESB 就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB 做了消息的转化解释和路由工作,让不同的服务互联互通。
微服务就是将一个单体架构的应用按业务划分为一个个的独立运行的程序即服务,它们之间通过 HTTP 协议进行通信(也可以采用消息队列来通信,如 RabbitMQ,Kafaka 等),可以采用不同的编程语言,使用不同的存储技术,自动化部署(如 Jenkins)减少人为控制,降低出错概率。服务数量越多,管理起来越复杂,因此采用集中化管理。例如 Eureka,ZooKeeper 等都是比较常见的服务集中化管理框架。
微服务是一种架构风格,架构就是为了解耦,实际使用的是分布式系统开发。一个大型的复杂软件应用,由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好的完成该任务。
一句话总结:微服务是 SOA 发展出来的产物,它是一种比较现代化的细粒度的 SOA 实现方式。
现如今,对于多数大型互联网应用,主机众多、部署分散,而且现在的集群规模越来越大,节点只会越来越多,所以节点故障、网络故障是常态,因此分区容错性也就成为了一个分布式系统必然要面对的问题。
解决了分区容错性,随之而来又产生了新的问题,那就是如何在保证了数据安全(一致,不易丢失)的同时,又让我们的分布式环境满足可用性呢?这就是著名的 CAP 原则。
CAP 原则又称 CAP 定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
CAP 由 Eric Brewer 在 2000 年 PODC 会议上提出。该猜想在提出两年后被证明成立,成为我们熟知的 CAP 定理。CAP三者不可兼得。
特性 | 定理 |
---|---|
Consistency | 一致性,也叫做数据原子性,系统在执行某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性的。等同于所有节点访问同一份最新的数据副本。 |
Availability | 可用性,每一个操作总是能够在一定的时间内返回结果,这里需要注意的是"一定时间内"和"返回结果"。一定时间内指的是在可以容忍的范围内返回结果,结果可以是成功或者是失败,且不保证获取的数据为最新数据。 |
Partition | |
tolerance | 分区容错性,分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障。这里可以理解为是否可以对数据进行分区,这是考虑到性能和可伸缩性。 |
CAP 三个特性只能满足其中两个,那么取舍的策略就共有三种:
现如今,对于多数大型互联网应用,主机众多、部署分散,而且现在的集群规模越来越大,节点只会越来越多,所以节点故障、网络故障是常态,因此分区容错性也就成为了一个分布式系统必然要面对的问题。那么就只能在 C 和 A 之间进行取舍。但对于传统的项目就可能有所不同,拿银行的转账系统来说,涉及到金钱的对于数据一致性不能做出一丝的让步,C 必须保证,出现网络故障的话,宁可停止服务。而互联网非金融项目普遍都是基于 AP 模式。
总而言之,没有最好的策略,好的系统应该是根据业务场景来进行架构设计的,只有适合的才是最好的。
CAP 理论已经提出好多年了,难道真的没有办法解决这个问题吗?也许可以做些改变。比如 C 不必使用那么强的一致性,可以先将数据存起来,稍后再更新,实现所谓的 “最终一致性”。
这个思路又是一个庞大的问题,同时也引出了第二个理论 BASE 理论。
BASE:全称 Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:
既然无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性)。需要注意的是,基本可用绝不等价于系统不可用。
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本数据同步的延时就是软状态的体现。
系统不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性。从而达到数据的最终一致性。这个时间期限取决于网络延时,系统负载,数据复制方案设计等等因素。
实际上,不只是分布式系统使用最终一致性,关系型数据库在某个功能上,也是使用最终一致性的,比如备份,数据库的复制都是需要时间的,这个复制过程中,业务读取到的值就是旧值。当然,最终还是达成了数据一致性。这也算是一个最终一致性的经典案例。
总的来说,BASE 理论面向的是大型高可用可扩展的分布式系统,和传统事务的 ACID 是相反的,它完全不同于 ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间是不一致的。
Paxos 描述了这样一个场景:
- 有一个叫做 Paxos 的小岛(Island)上面住了一批居民(Islander);
- 岛上面所有的事情由一些特殊的人决定,他们叫做议员(Senator);
- 议员的总数(Senator Count)是确定的,不能更改;
- 岛上每次环境事务的变更都需要通过一个提议(Proposal),每个提议都有一个编号(PID),这个编号是一直增长的,不能倒退;
- 每个提议都需要超过半数((Senator Count)/2 +1)的议员同意才能生效(少数服从多数);
- 每个议员只会同意大于当前编号的提议,包括已生效的和未生效的;
- 如果议员收到小于等于当前编号的提议,他会拒绝,并告知对方:你的提议已经有人提过了。这里的当前编号是每个议员在自己记事本上记录的编号,他会不断更新这个编号;
- 整个议会不能保证所有议员记事本上的编号总是相同的;
- 现在议会有一个目标:保证所有的议员对于提议都能达成一致的看法。
现在议会开始运作,所有议员一开始记事本上面记录的编号都是0。有一个议员发了一个提议:将电费设定为1元/度。他首先看
了一下记事本,嗯,当前提议编号是0,那么我的这个提议的编号就是1,于是他给所有议员发消息:1号提议,设定电费1元/
度。其他议员收到消息以后查了一下记事本,哦,当前提议编号是0,这个提议可接受,于是他记录下这个提议并回复:我接受
你的1号提议,同时他在记事本上记录:当前提议编号为1。发起提议的议员收到了超过半数的回复,立即给所有人发通知:1号
提议生效!收到的议员会修改他的记事本,将1好提议由记录改成正式的法令,当有人问他电费为多少时,他会查看法令并告诉
对方:1元/度。
再看冲突的解决:假设总共有三个议员S1-S3,S1和S2同时发起了一个提议:1号提议,设定电费。S1想设为1元/度, S2想设为
2元/度。结果S3先收到了S1的提议,于是他做了和前面同样的操作。紧接着他又收到了S2的提议,结果他一查记事本,咦,这
个提议的编号小于等于我的当前编号1,于是他拒绝了这个提议:对不起,这个提议先前提过了。于是S2的提议被拒绝,S1正式
发布了提议: 1号提议生效。S2向S1或者S3打听并更新了1号法令的内容,然后他可以选择继续发起2号提议。
如果Paxos岛上的议员人人平等,在某种情况下会由于提议的冲突而产生一个“活锁”(所谓活锁我的理解是大家都没有死,都在
动,但是一直解决不了冲突问题)。Paxos的作者在所有议员中设立一个总统,只有总统有权发出提议,如果议员有自己的提
议,必须发给总统并由总统来提出。
情况一:市民甲(Client)到某个议员(ZK Server)那里询问(Get)某条法令的情况(ZNode的数据),议员毫不犹豫的拿出他的
记事本(local storage),查阅法令并告诉他结果,同时声明:我的数据不一定是最新的。你想要最新的数据?没问题,等
着,等我找总统Sync一下再告诉你。
情况二:市民乙(Client)到某个议员(ZK Server)那里要求政府归还欠他的一万元钱,议员让他在办公室等着,自己将问题反
映给了总统,总统询问所有议员的意见,多数议员表示欠屁民的钱一定要还,于是总统发表声明,从国库中拿出一万元还债,
国库总资产由100万变成99万。市民乙拿到钱回去了(Client函数返回)。
情况三:总统突然挂了,议员接二连三的发现联系不上总统,于是各自发表声明,推选新的总统,总统大选期间政府停业,拒
绝屁民的请求。
Raft算法将Server划分为3种状态,或者也可以称作角色:
Term
RPC
日志复制(Log Replication)
ZAB协议包含两种基本模式,分别是:
1》崩溃恢复之数据恢复
2》消息广播之原子广播
首先将三台虚拟机切换至相互免秘钥快照(keyfree),然后将准备好的 ZooKeeper 安装包上传至服务器
先在 node01 机器执行以下操作
解压
[root@node01 ~]# tar -zxvf apache-zookeeper-3.6.3-bin.tar.gz -C /opt/yjx/
[root@node01 ~]# rm apache-zookeeper-3.6.3-bin.tar.gz -rf
创建数据目录,日志目录
[root@node01 ~]# mkdir -p /var/yjx/zookeeper/data
[root@node01 ~]# mkdir -p /opt/yjx/apache-zookeeper-3.6.3-bin/logs
修改配置文件, ZooKeeper 启动时默认加载的配置文件名为 zoo.cfg
[root@node01 ~]# cd /opt/yjx/apache-zookeeper-3.6.3-bin/
[root@node01 apache-zookeeper-3.6.3-bin]# cp conf/zoo_sample.cfg conf/zoo.cfg
[root@node01 apache-zookeeper-3.6.3-bin]# vim conf/zoo.cfg
主要修改以下内容 (数字表示行号) :
12 dataDir=/var/yjx/zookeeper/data
13 dataLogDir=/opt/yjx/apache-zookeeper-3.6.3-bin/logs
15 clientPort=2181
37 server.1=node01:2888:3888
38 server.2=node02:2888:3888
39 server.3=node03:2888:3888
server.1 中的 1 是 myid 文件中的内容,2888 用于集群内部通信,3888 用于选举 Leader。
接下来将 node01 的 ZooKeeper 所有文件拷贝至 node02 和 node03。推荐从 node02 和 node03 拷贝。
[root@node02 ~]# scp -r root@node01:/opt/yjx/apache-zookeeper-3.6.3-bin /opt/yjx/
[root@node03 ~]# scp -r root@node01:/opt/yjx/apache-zookeeper-3.6.3-bin /opt/yjx/
# 或者使用分发脚本
[root@node01 ~]# yjxrsync /opt/yjx/apache-zookeeper-3.6.3-bin
然后在三台机器的 $ZOOKEEPER_HOME/data 目录下分别创建 myid 文件,内容分别为 1,2,3
node01 :
[root@node01 apache-zookeeper-3.6.3-bin]# echo 1 > data/myid
node02
[root@node02 apache-zookeeper-3.6.3-bin]# echo 2 > data/myid
node03
[root@node03 apache-zookeeper-3.6.3-bin]# echo 3 > data/myid
最后 vim /etc/profile 配置环境变量,环境搭建结束。
export ZOOKEEPER_HOME=/opt/yjx/apache-zookeeper-3.6.3-bin
export PATH=$ZOOKEEPER_HOME/bin:$PATH
配完环境变量后 source /etc/profile 重新加载环境变量。
启动集群
[root@node01 ~]# zkServer.sh start
[root@node02 ~]# zkServer.sh start
[root@node03 ~]# zkServer.sh start
查看状态
[root@node01 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/yjx/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
[root@node02 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/yjx/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
[root@node03 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/yjx/apache-zookeeper-3.6.3-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader
停止集群
[root@node01 ~]# zkServer.sh stop
[root@node02 ~]# zkServer.sh stop
[root@node03 ~]# zkServer.sh stop
环境搭建成功后,删除 ZooKeeper 安装包, shutdown -h now 关机拍摄快照
在 /usr/local/bin 目录下创建对应服务的脚本:
[root@node01 ~]# vim /usr/local/bin/zookeeper
zookeeper 脚本内容如下:
#!/bin/bash
user=$(whoami)
case $1 in
"start")
for i in node01 node02 node03
do
echo -e "\e[1;34m========== $i ZooKeeper 启动 ============ \e[0m"
ssh $user@$i "/opt/yjx/apache-zookeeper-3.6.3-bin/bin/zkServer.sh start"
done
;;
"stop")
for i in node01 node02 node03
do
echo -e "\e[1;34m==================== $i ZooKeeper 停止 ====================\e[0m"
ssh $user@$i "/opt/yjx/apache-zookeeper-3.6.3-bin/bin/zkServer.sh stop"
done
;;
"status")
for i in node01 node02 node03
do
echo -e "\e[1;34m==================== $i ZooKeeper 状态 ====================\e[0m"
ssh $user@$i "/opt/yjx/apache-zookeeper-3.6.3-bin/bin/zkServer.sh status"
done
;;
esac
修改脚本权限为用户读写执行 rwx ,组读执行 r-x ,其他用户无权限 — :
[root@node01 ~]# chmod 750 /usr/local/bin/zookeeper
一、zk服务命令
1. 启动ZK服务: bin/zkServer.sh start
2. 查看ZK服务状态: bin/zkServer.sh status
3. 停止ZK服务: bin/zkServer.sh stop
4. 重启ZK服务: bin/zkServer.sh restart
5. 连接服务器: zkCli.sh -server 127.0.0.1:2181
二、zk客户端命令
1.ls -- 查看某个目录包含的所有文件,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] ls /
ls /path
2.create -- 创建znode,并设置初始内容,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] create /test "test"
Created /test
创建一个新的 znode节点“ test ”以及与它关联的字符串
create /path data 默认创建持久节点
create -s /path data 创建顺序节点
create -e /path data 创建临时节点
create /parent/sub/path /data
4.get -- 获取znode的数据,如下:
[zk: 127.0.0.1:2181(CONNECTED) 1] get /test
get /path
get /path0000000018 访问顺序节点必须输入完整路径
5.set -- 修改znode内容,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] set /test "ricky"
set /path /data
6.delete -- 删除znode,例如:
[zk: 127.0.0.1:2181(CONNECTED) 1] delete /test
delete /path 删除没有子节点的节点
rmr /path 移除节点并且递归移除所有子节点
7 quit --退出客户端
8 help --帮助命令
[zk: localhost:2181(CONNECTED) 10] stat /yjx
666 当前节点的值
cZxid = 0xf00000013
创建这个节点的事务id,ZXID是一个长度64位的数字,
低32位是按照数字递增,即每次客户端发起一个proposal,低32位的数字简单加1。
高32位是leader周期的epoch编号
ctime = Mon Dec 09 17:33:06 CST 2019 创建时间
mZxid = 0xf00000013 最后一次修改节点数据的事务ID
mtime = Mon Dec 09 17:33:06 CST 2019 修改时间
pZxid = 0xf00000014 子节点的最新事务ID
cversion = 1 对此znode的子节点进行的更改次数
dataVersion = 对此znode的数据所作的修改次数
aclVersion = 对此znode的acl更改次数
ephemeralOwner = 0x0 (持久化节点)0x16ee9fc0feb0001(临时节点)
dataLength = 3 数据的长度
numChildren = 1 子节点的数目
持久化节点(PERSISTENT)
临时节点(Ephemral)
序列化节点(Sequential)
语法格式 : addWatch [-m mode] path # optional mode is one of [PERSISTENT,PERSISTENT_RECURSIVE] - default is PERSISTENT_RECURSIVE 。
addWatch 作用针对指定节点添加事件监听,支持两种模式
PERSISTENT 模式案例演示:
# 创建节点
[zk: localhost:2181(CONNECTED) 0] create /bigdata 100
Created /bigdata
# 监听节点,使用 PERSISTENT 模式
[zk: localhost:2181(CONNECTED) 1] addWatch -m PERSISTENT /bigdata
# 修改当前节点会触发监听事件
[zk: localhost:2181(CONNECTED) 2] set /bigdata 200
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata
# 创建子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 3] create /bigdata/zk 300
WATCHER:: # 触发监听事件 NodeChildrenChanged
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/bigdata
Created /bigdata/zk
# 修改子节点不会触发监听事件
[zk: localhost:2181(CONNECTED) 4] set /bigdata/zk 301
# 删除子节点触发监听事件
[zk: localhost:2181(CONNECTED) 5] delete /bigdata/zk
WATCHER:: # 触发监听事件 NodeChildrenChanged
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/bigdata
# 删除当前节点会触发监听事件
[zk: localhost:2181(CONNECTED) 6] delete /bigdata
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted path:/bigdata
PERSISTENT_RECURSIVE 模式案例演示
# 创建节点
[zk: localhost:2181(CONNECTED) 0] create /bigdata 100
Created /bigdata
# 监听节点,不指定模式默认采用 PERSISTENT_RECURSIVE 模式
[zk: localhost:2181(CONNECTED) 1] addWatch /bigdata
# 修改当前节点会触发监听事件
[zk: localhost:2181(CONNECTED) 2] set /bigdata 200
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata
# 创建子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 3] create /bigdata/zk 30
WATCHER:: # 触发监听事件 NodeCreated
WatchedEvent state:SyncConnected type:NodeCreated path:/bigdata/zk
Created /bigdata/zk
# 创建子节点的子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 4] create /bigdata/zk/test 400
WATCHER:: # 触发监听事件 NodeCreated
WatchedEvent state:SyncConnected type:NodeCreated path:/bigdata/zk/test
Created /bigdata/zk/test
# 修改子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 5] set /bigdata/zk 301
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata/zk
# 修改子节点的子节点会触发监听事件
[zk: localhost:2181(CONNECTED) 6] set /bigdata/zk/test 401
WATCHER:: # 触发监听事件 NodeDataChanged
WatchedEvent state:SyncConnected type:NodeDataChanged path:/bigdata/zk/test
# 删除子节点的子节点触发监听事件
[zk: localhost:2181(CONNECTED) 7] delete /bigdata/zk/test
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted path:/bigdata/zk/test
# 删除子节点和当前节点都会触发监听事件
[zk: localhost:2181(CONNECTED) 8] deleteall /bigdata
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted path:/bigdata/zk
WATCHER:: # 触发监听事件 NodeDeleted
WatchedEvent state:SyncConnected type:NodeDeleted path:/bigdata
仔细观察,两种模式的 WatchedEvent 稍微有些不同。
ACL权限控制
ZK的节点有5种操作权限:CREATE、READ、WRITE、DELETE、ADMIN 也就是 增、删、改、查、管理权限,这5种权限简写为
crwda,这5种权限中,delete是指对子节点的删除权限,其它4种权限指对自身节点的操作权限
身份的认证有4种方式:
- world:默认方式,相当于全世界都能访问
- auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
- digest:即用户名:密码这种方式认证,这也是业务系统中最常用的
- ip:使用Ip地址认证
schema
world:只有一个用户anyone,代表所有人(默认)
ip:使用IP地址认证
auth:使用已添加认证的用户认证
digest:使用用户名:密码 方式认证
id
world:只有一个id,anyone
ip:通常是一个ip地址或者地址段
auth:用户名
digest:自定义
权限
create 简写为c,可以创建子节点
delete 简写为d 可以删除子节点
read 简写为r 可以读取节点数据及显示子节点列表
write 简写为w 可以设置节点数据
admin 简写为a 可以设置管理权限
查看ACL
getAcl /parent
设置ACL
setAcl /parent world:anyone:r
添加用户
addauth digest zhangsan:123456
addauth digest lisi:123456
设置权限
setAcl /parent auth:zhangsan:123456:r
setAcl /parent auth:lisi:123456:rcwd
退出当前用户
quit
后续访问 /parent路径,需要先添加用户
addauth digest zhangsan:123456
官方文档:https://zookeeper.apache.org/doc/r3.6.3/zookeeperAdmin.html#sc_4lw
可以通过 nc (NetCat)命令,脱离 ZK 客户端和 ZK 服务交互。
需预先安装 nc 命令,安装命令: yum -y install nc 。
然后在 zoo.cfg 配置文件中配置启用四字命令 4lw.commands.whitelist=* 。
命令格式: echo [commond] | nc [ip] [port] 。
安装nc
pom.xml 信息
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.6.3version>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<version>5.9.2version>
<scope>testscope>
dependency>
ZooKeeperTest.java
/*
@DisplayName:测试类在测试报告中的名称,可以加在类上,也可以加在方法上。
@BeforeAll和@AfterAll:它们定义了整个测试类在开始前以及结束时的操作,只能修饰静态方法,主要用于在测试过程中所
需要的全局数据和外部资源的初始化和清理。
@BeforeEach和@AfterEach:它们所标注的方法会在每个测试用例方法开始前和结束时执行,主要是负责该测试用例所需要的
运行环境的准备和销毁。
*/
@DisplayName("ZooKeeper 测试类")
public class ZooKeeperTest {
/**
* 客户端对象
*/
private ZooKeeper zooKeeper;
@BeforeEach
public void init() {
try {
/*
初始次数为 1,后面要在内部类中使用。
三种写法:
1、写成外部类成员变量,不用加 final;
2、作为函数局部变量,放在 try 外面,写成 final;
3、写在 try 中,不加 final。
*/
CountDownLatch countDownLatch = new CountDownLatch(1);
// zkServer 的 ip、port,如果是集群逗号分隔
String connectString = "192.168.100.101:2181,192.168.100.102:2181,192.168.100.103:2181";
// 超时时间
int sessionTimeout = 5000;
zooKeeper = new ZooKeeper(connectString, sessionTimeout,
// 连接成功后监听
watchedEvent -> {
// 如果状态变成已连接则次数 -1
if (Watcher.Event.KeeperState.SyncConnected.equals(watchedEvent.getState())) {
System.out.println("连接成功");
// 次数 -1
countDownLatch.countDown();
}
});
// 等待,次数为 0 时才会继续往下执行(等待监听器监听到连接成功,才能操作 zk)
countDownLatch.await();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
@AfterEach
public void close() {
// 关闭连接
try {
if (zooKeeper != null) {
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制台打印结果
连接成功
/**
* 操作后,服务端会返回处理结果,返回 void、null也算处理结果。
* 同步指的是当前线程阻塞,等待服务端返回数据,收到返回的数据才继续往下执行;
* 异步回调指的是,把对结果(返回的数据)的处理写在回调函数中,当前线程不等待返回的数据,继续往下执行,收到返回
的数据时自动调用回调函数来处理。
*/
@DisplayName("检测节点是否存在")
@Test
public void testDataIsExists() {
// 同步方式
System.out.println("同步方式");
Stat exists = null;
String znode = "/bigdata";
try {
// 如果存在,返回节点状态 stat;如果不存在,返回 null。
// 第二个参数是 watch,是否监听。
exists = zooKeeper.exists(znode, false);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
if (exists == null) {
System.out.println(znode + "节点不存在");
} else {
System.out.println(znode + "节点存在");
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
zooKeeper.exists(znode, false,
// 第二个是 path znode 路径,第三个是 ctx 后面传入实参,第四个是 znode 的状态
(i, s, o, stat) -> {
// 如果节点不存在,返回的 stat 是 null
if (stat == null) {
System.out.println(znode + "节点不存在");
} else {
System.out.println(znode + "节点存在");
}
// ctx:Object 类型
}, "传给回调的参数");
}
控制台打印结果
连接成功
同步方式
/bigdata节点不存在
========== 华丽的分割线 ==========
异步回调
/bigdata节点不存在
@DisplayName("创建节点")
@Test
public void testCreateNode() {
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
try {
// 数据要写成 byte[],不携带数据写成 null;
// 默认 acl 权限使用 ZooDefs.Ids.OPEN_ACL_UNSAFE;
// 最后一个是节点类型,P 是永久,E 是临时,S 是有序
zooKeeper.create(znode1, "abc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
System.out.println(znode1 + "节点创建成功");
// 如果节点已存在,会抛出异常
} catch (KeeperException | InterruptedException e) {
System.out.println("创建节点" + znode1 + "失败,请检查节点是否已存在");
e.printStackTrace();
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.create(znode2, "abc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT,
// 第二个 path,第三个 ctx,第四个节点状态
(i, s, o, s1, stat) -> {
// 回调方式不抛出异常,返回的 stat 是创建节点的状态,如果节点已存在,返回的 stat 是 null
if (stat == null) {
System.out.println("创建节点" + znode2 + "失败,请检查节点是否已存在");
} else {
System.out.println(znode2 + "节点创建成功");
}
}, null);
}
控制台打印结果
连接成功
同步方式
/bigdata节点创建成功
========== 华丽的分割线 ==========
异步回调
/java节点创建成功
@DisplayName("获取节点数据")
@Test
public void testGetNodeData() {
// 获取节点数据,返回 byte[]
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
byte[] data = null;
try {
// 第二个参数是 watch,第三个是 stat
data = zooKeeper.getData(znode1, false, null);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
// 调用 new String() 时要判断 data 是否为 null,如果是 null 会抛 NPE
if (data == null) {
System.out.println(znode1 + "节点没有数据");
} else {
System.out.println(znode1 + "节点数据:" + new String(data));
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.getData(znode2, false,
// 第二个起依次是:path、ctx、返回的节点数据、节点状态
(i, s, o, bytes, stat) -> {
// 不必判断 bytes 是否是 null,如果节点没有数据,不会调用回调函数;
// 执行到此,说明 bytes 不是 null
System.out.println(znode2 + "节点数据:" + new String(bytes));
}, null);
}
控制台打印结果
连接成功
同步方式
/bigdata节点数据:abc
========== 华丽的分割线 ==========
异步回调
/java节点数据:abc
@DisplayName("更新节点数据")
@Test
public void testUpdateNodeData() {
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
try {
// 最后一个参数是版本号,-1 表示可以是任何版本
zooKeeper.setData(znode1, "123".getBytes(), -1);
System.out.println(znode1 + "节点数据更新成功");
} catch (KeeperException | InterruptedException e) {
System.out.println(znode1 + "节点数据更新失败");
e.printStackTrace();
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.setData("/java", "123".getBytes(), -1,
// 第二个是 path,第三个是 ctx
(i, s, o, stat) -> {
System.out.println(znode2 + "节点数据更新成功");
}, null);
}
控制台打印结果
连接成功
同步方式
/bigdata节点数据更新成功
========== 华丽的分割线 ==========
异步回调
/java节点数据更新成功
获取字节点列表
/**
* 只获取子节点,不获取孙节点。
* watch:可以写 boolean,要添加监听就写 true,不监听写 false;
* 可以写 Watcher 对象,new 一个 Watcher 对象表示要监听,null 表示不监听。
*/
@DisplayName("获取子节点列表")
@Test
public void testGetNodeList() {
// 获取子节点列表,List,比如/bigdata/zk,/bigdata/java,返回的是["zk"、"java"]
// 同步方式
System.out.println("同步方式");
String znode1 = "/";
List<String> children = null;
try {
// 第二个参数是 watch
children = zooKeeper.getChildren(znode1, false);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
System.out.println(znode1 + "子节点列表:" + children);
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/zookeeper";
zooKeeper.getChildren(znode2, false,
// 第二个起依次是:path、ctx、返回的子节点列表
(i, s, o, list) -> System.out.println(znode2 + "子节点列表:" + list), null);
}
控制台打印结果
连接成功
同步方式
/子节点列表:[bigdata, java, zookeeper]
========== 华丽的分割线 ==========
异步回调
/zookeeper子节点列表:[config, quota]
/**
* delete() 只能删除没有子节点的 znode,如果该 znode 有子节点会抛出异常。
* 没有提供递归删除子节点的方法,如果要删除带有子节点的 znode,需要自己实现递归删除。
* 可以先 getChildren() 获取子节点列表,遍历列表依次删除子节点,再删除父节点。
*/
@DisplayName("删除节点")
@Test
public void testDeleteNode() {
// 同步方式
System.out.println("同步方式");
String znode1 = "/bigdata";
try {
// 第二个参数是版本号,-1 表示可以是任何版本
zooKeeper.delete(znode1, -1);
System.out.println(znode1 + "节点删除成功");
} catch (InterruptedException | KeeperException e) {
System.out.println(znode1 + "节点删除失败");
e.printStackTrace();
}
System.out.println("========== 华丽的分割线 ==========");
// 异步回调
System.out.println("异步回调");
String znode2 = "/java";
zooKeeper.delete(znode2, -1,
// 第二个是 path,第三个是 ctx
(i, s, o) -> System.out.println(znode2 + "节点删除成功"), null);
}
控制台打印结果
连接成功
同步方式
/bigdata节点删除成功
========== 华丽的分割线 ==========
异步回调
/java节点删除成功
有一组服务器向客户端提供某种服务,我们希望客户端每次请求服务端都可以找到服务端集群中某一台服务器,这样服务端就可以向客户端提供客户端所需的服务。对于这种场景,我们的程序中一定有一份这组服务器的列表,每次客户端请求时候,都是从这份列表里读取这份服务器列表。那么这分列表显然不能存储在一台单节点的服务器上,否则这个节点挂掉了,整个集群都会发生故障,我们希望这份列表时高可用的。高可用的解决方案是:这份列表是分布式存储的,它是由存储这份列表的服务器共同管理的,如果存储列表里的某台服务器坏掉了,其他服务器马上可以替代坏掉的服务器,并且可以把坏掉的服务器从列表里删除掉,让故障服务器退出整个集群的运行,而这一切的操作又不会由故障的服务器来操作,而是集群里正常的服务器来完成。这是一种主动的分布式数据结构,能够在外部情况发生变化时候主动修改数据项状态的数据机构。,它和javaEE里的JNDI服务很像。
当分布式系统操作数据,例如:读取数据、分析数据、最后修改数据。在分布式系统里这些操作可能会分散到集群里不同的节点上,那么这时候就存在数据操作过程中一致性的问题,如果不一致,我们将会得到一个错误的运算结果,在单一进程的程序里,一致性的问题很好解决,但是到了分布式系统就比较困难,因为分布式系统里不同服务器的运算都是在独立的进程里,运算的中间结果和过程还要通过网络进行传递,那么想做到数据操作一致性要困难的多。Zookeeper提供了一个锁服务解决了这样的问题,能让我们在做分布式数据运算时候,保证数据操作的一致性。
在分布式系统里,我们会把一个服务应用分别部署到n台服务器上,这些服务器的配置文件是相同的(例如:我设计的分布式网站框架里,服务端就有4台服务器,4台服务器上的程序都是一样,配置文件都是一样),如果配置文件的配置选项发生变化,那么我们就得一个个去改这些配置文件,如果我们需要改的服务器比较少,这些操作还不是太麻烦,如果我们分布式的服务器特别多,比如某些大型互联网公司的hadoop集群有数千台服务器,那么更改配置选项就是一件麻烦而且危险的事情。这时候zookeeper就可以派上用场了,我们可以把zookeeper当成一个高可用的配置存储器,把这样的事情交给zookeeper进行管理,我们将集群的配置文件拷贝到zookeeper的文件系统的某个节点上,然后用zookeeper监控所有分布式系统里配置文件的状态,一旦发现有配置文件发生了变化,每台服务器都会收到zookeeper的通知,让每台服务器同步zookeeper里的配置文件,zookeeper服务也会保证同步操作原子性,确保每个服务器的配置文件都能被正确的更新。
集群管理是很困难的,在分布式系统里加入了zookeeper服务,能让我们很容易的对集群进行管理。集群管理最麻烦的事情就是节点故障管理,zookeeper可以让集群选出一个健康的节点作为master,master节点会知道当前集群的每台服务器的运行状况,一旦某个节点发生故障,master会把这个情况通知给集群其他服务器,从而重新分配不同节点的计算任务。Zookeeper不仅可以发现故障,也会对有故障的服务器进行甄别,看故障服务器是什么样的故障,如果该故障可以修复,zookeeper可以自动修复或者告诉系统管理员错误的原因让管理员迅速定位问题,修复节点的故障。大家也许还会有个疑问,master故障了,那怎么办了?zookeeper也考虑到了这点,zookeeper内部有一个“选举领导者的算法”,master可以动态选择,当master故障时候,zookeeper能马上选出新的master对集群进行管理。
如果进程已经看到过数据对象的某个最新值,那么任何后续访问都不会返回在那个值之前的值。
不会读取最旧的数据
秒杀场景
系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以编程了。
按照顺序完成数据的书写
打游戏副本场景