主题概要
- zookeeper概要、背景及作用
- 部署与常规配置
- 节点类型
一、zookeeper概要、背景及作用
zookeeper产生背景:
项目从单体到分布式转变之后,将会产生多个节点之间协同的问题。如:
- 每天的定时任务由谁哪个节点来执行?
- RPC调用时的服务发现?
- 如何保证并发请求的幂等
- ....
这些问题可以统一归纳为多节点协调问题,如果靠节点自身进行协调这是非常不可靠的,性能上也不可取。必须由一个独立的服务做协调工作,它必须可靠,而且保证性能。
zookeeper概要:
ZooKeeper是用于分布式应用程序的协调服务。它公开了一组简单的API,分布式应用程序可以基于这些API用于同步,节点状态、配置等信息、服务注册等信息。其由JAVA编写,支持JAVA 和C两种语言的客户端。
znode 节点
zookeeper 中数据基本单元叫节点,节点之下可包含子节点,最后以树级方式程现。每个节点拥有唯一的路径path。客户端基于PATH上传节点数据,zookeeper 收到后会实时通知对该路径进行监听的客户端。
二、部署与常规配置
zookeeper 基于JAVA开发,下载后只要有对应JVM环境即可运行。其默认的端口号是2181运行前得保证其不冲突。
版本说明:
2019年5月20日发行的3.5.5是3.5分支的第一个稳定版本。此版本被认为是3.4稳定分支的后续版本,可以用于生产。基于3.4它包含以下新功能
- 动态重新配置
- 本地会议
- 新节点类型:容器,TTL
- 原子广播协议的SSL支持
- 删除观察者的能力
- 多线程提交处理器
- 升级到Netty 4.1
- Maven构建
另请注意:建议的最低JDK版本为1.8 文件说明:
- apache-zookeeper-xxx-tar.gz 代表源代码
- apache-zookeeper-xxx-bin.tar.gz 运行版本
下载地址:zookeeper.apache.org/releases.ht… 具体部署流程:
#下载
wget https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/current/apache-zookeeper-3.5.5-bin.tar.gz
#解压
tar -zxvf apache-zookeeper-3.5.5-bin.tar.gz
#拷贝默认配置
cd {zookeeper_home}/conf
cp zoo_sample.cfg zoo.cfg
#启动
{zookeeper_home}/bin/zkServer.sh
常规配置文件说明:
# zookeeper时间配置中的基本单位 (毫秒)
tickTime=2000
# 允许follower初始化连接到leader最大时长,它表示tickTime时间倍数 即:initLimit*tickTime
initLimit=10
# 允许follower与leader数据同步最大时长,它表示tickTime时间倍数
syncLimit=5
#zookeper 数据存储目录
dataDir=/tmp/zookeeper
#对客户端提供的端口号
clientPort=2181
#单个客户端与zookeeper最大并发连接数
maxClientCnxns=60
# 保存的数据快照数量,之外的将会被清除
autopurge.snapRetainCount=3
#自动触发清除任务时间间隔,小时为单位。默认为0,表示不自动清除。
autopurge.purgeInterval=1
客户端命令:
基本命令列表 **close ** 关闭当前会话 **connect host:port ** 重新连接指定Zookeeper服务 create [-s] [-e] [-c] [-t ttl] path [data] [acl] 创建节点 delete [-v version] path 删除节点,(不能存在子节点) deleteall path 删除路径及所有子节点 setquota -n|-b val path 设置节点限额 -n 子节点数 -b 字节数 listquota path 查看节点限额 delquota [-n|-b] path 删除节点限额 get [-s] [-w] path 查看节点数据 -s 包含节点状态 -w 添加监听 getAcl [-s] path ls [-s] [-w] [-R] path 列出子节点 -s状态 -R 递归查看所有子节点 -w 添加监听 printwatches on|off 是否打印监听事件 **quit ** 退出客户端 **history ** 查看执行的历史记录 redo cmdno 重复 执行命令,history 中命令编号确定 removewatches path [-c|-d|-a] [-l] 删除指定监听 set [-s] [-v version] path data 设置值 setAcl [-s] [-v version] [-R] path acl 为节点设置ACL权限 stat [-w] path 查看节点状态 -w 添加监听 sync path 强制同步节点
node数据的增删改查
# 列出子节点
ls /
#创建节点
create /luban "luban is good man"
# 查看节点
get /luban
# 创建子节点
create /luban/sex "man"
# 删除节点
delete /luban/sex
# 删除所有节点 包括子节点
deleteall /luban
三、Zookeeper节点介绍
知识点:
- 节点类型
- 节点的监听(watch)
- 节点属性说明(stat)
- 权限设置(acl)
zookeeper 中节点叫znode存储结构上跟文件系统类似,以树级结构进行存储。不同之外在于znode没有目录的概念,不能执行类似cd之类的命令。znode结构包含如下:
- path:唯一路径
- childNode:子节点
- stat:状态属性
- type:节点类型
节点类型
| 类型 | 描述 |
|---|---|
| PERSISTENT | 持久节点 |
| PERSISTENT_SEQUENTIAL | 持久序号节点 |
| EPHEMERAL | 临时节点(不可在拥有子节点) |
| EPHEMERAL_SEQUENTIAL | 临时序号节点(不可在拥有子节点) |
- PERSISTENT(持久节点)
持久化保存的节点,也是默认创建的
#默认创建的就是持久节点
create /test
- PERSISTENT_SEQUENTIAL(持久序号节点)
创建时zookeeper 会在路径上加上序号作为后缀,。非常适合用于分布式锁、分布式选举等场景。创建时添加 -s 参数即可。
#创建序号节点
create -s /test
#返回创建的实际路径
Created /test0000000001
create -s /test
#返回创建的实际路径2
Created /test0000000002
- EPHEMERAL(临时节点)
临时节点会在客户端会话断开后自动删除。适用于心跳,服务发现等场景。创建时添加参数-e 即可。
#创建临时节点, 断开会话 在连接将会自动删除
create -e /temp
- EPHEMERAL_SEQUENTIAL(临时序号节点)
与持久序号节点类似,不同之处在于EPHEMERAL_SEQUENTIAL是临时的会在会话断开后删除。创建时添加 -e -s
create -e -s /temp/seq
节点属性
# 查看节点属性
stat /luban
其属性说明如下表:
#创建节点的事物ID
cZxid = 0x385
#创建时间
ctime = Tue Sep 24 17:26:28 CST 2019
#修改节点的事物ID
mZxid = 0x385
#最后修改时间
mtime = Tue Sep 24 17:26:28 CST 2019
# 子节点变更的事物ID
pZxid = 0x385
#这表示对此znode的子节点进行的更改次数(不包括子节点)
cversion = 0
# 数据版本,变更次数
dataVersion = 0
#权限版本,变更次数
aclVersion = 0
#临时节点所属会话ID
ephemeralOwner = 0x0
#数据长度
dataLength = 17
#子节点数(不包括子子节点)
numChildren = 0
节点的监听:
客户添加 -w 参数可实时监听节点与子节点的变化,并且实时收到通知。非常适用保障分布式情况下的数据一至性。其使用方式如下:
| 命令 | 描述 |
|---|---|
| ls -w path | 监听子节点的变化(增,删) |
| get -w path | 监听节点数据的变化 |
| stat -w path | 监听节点属性的变化 |
| printwatches on|off | 触发监听后,是否打印监听事件(默认on) |
acl权限设置
ACL全称为Access Control List(访问控制列表),用于控制资源的访问权限。ZooKeeper使用ACL来控制对其znode的防问。基于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 | 可以设置节点访问控制列表权限 |
acl 相关命令:
| 命令 | 使用方式 | 描述 |
|---|---|---|
| getAcl | getAcl | 读取ACL权限 |
| setAcl | setAcl | 设置ACL权限 |
| addauth | addauth | 添加认证用户 |
world权限****示例 语法: setAcl world:anyone:<权限位> 注:world模式中anyone是唯一的值,表示所有人
- 查看默认节点权限:
#创建一个节点
create -e /testAcl
#查看节点权限
getAcl /testAcl
#返回的默认权限表示 ,所有人拥有所有权限。
'world,'anyone: cdrwa
- 修改默认权限为 读写
#设置为rw权限
setAcl /testAcl world:anyone:rw
# 可以正常读
get /testAcl
# 无法正常创建子节点
create -e /testAcl/t "hi"
# 返回没有权限的异常
Authentication is not valid : /testAcl/t
IP权限示例: 语法: setAcl ip:<ip地址|地址段>:<权限位>
auth模式示例: 语法:
- setAcl auth:<用户名>:<密码>:<权限位>
- addauth digest <用户名>:<密码>
digest 权限示例: 语法:
- setAcl digest :<用户名>:<密钥>:<权限位>
- addauth digest <用户名>:<密码>
注1:密钥 通过sha1与base64组合加密码生成,可通过以下命令生成
echo -n <用户名>:<密码> | openssl dgst -binary -sha1 | openssl base64
注2:为节点设置digest 权限后,访问前必须执行addauth,当前会话才可以防问。
- 设置digest 权限
#先 sha1 加密,然后base64加密
echo -n luban:123456 | openssl dgst -binary -sha1 | openssl base64
#返回密钥
2Rz3ZtRZEs5RILjmwuXW/wT13Tk=
#设置digest权限
setAcl /luban digest:luban:2Rz3ZtRZEs5RILjmwuXW/wT13Tk=:cdrw
- 查看节点将显示没有权限
#查看节点
get /luban
#显示没有权限访问
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /luban
- 给当前会话添加认证后在次查看
#给当前会话添加权限帐户
addauth digest luban:123456
#在次查看
get /luban
#获得返回结果
luban is good man
ACL的特殊说明: 权限仅对当前节点有效,不会让子节点继承。如限制了IP防问A节点,但不妨碍该IP防问A的子节点 /A/B。
**主要内容:**
- 客户端
- zookeeper客户端简介,C客户端
- 客户端连接参数说明
- 客户端CRUD
- 客户端监听
- 集群
- 集群架构说明
- 集群配置及参数说明
- 选举投票机制
- 主从复制机制
一、客户端API常规应用
zookeeper 提供了java与C两种语言的客户端。我们要学习的就是java客户端。引入最新的maven依赖:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.5</version>
</dependency>
知识点:
- 初始连接
- 创建、查看节点
- 监听节点
- 设置节点权限
- 第三方客户端ZkClient
初始连接: 常规的客户端类是 org.apache.zookeeper.ZooKeeper,实例化该类之后将会自动与集群建立连接。构造参数说明如下: | 参数名称 | 类型 | 说明 | |:----|:----|:----|:----:| | connectString | String | 连接串,包括ip+端口 ,集群模式下用逗号隔开 192.168.0.149:2181,192.168.0.150:2181 | | sessionTimeout | **int ** | 会话超时时间,该值不能超过服务端所设置的 minSessionTimeout 和maxSessionTimeout | | watcher | Watcher | 会话监听器,服务端事件将会触该监听 | | sessionId | **long ** | 自定义会话ID | | sessionPasswd | byte[] | 会话密码 | | canBeReadOnly | **boolean ** | 该连接是否为只读的 | | hostProvider | HostProvider | 服务端地址提供者,指示客户端如何选择某个服务来调用,默认采用StaticHostProvider实现 |
2.创建、查看节点
创建节点 通过org.apache.zookeeper.ZooKeeper#create()即可创建节点,其参数说明如下:
| 参数名称 | 类型 | 说明 |
|---|---|---|
| path | String | |
| data | byte[] | |
| acl | List | |
| createMode | CreateMode | |
| cb | StringCallback | |
| ctx | Object |
查看节点: 通过org.apache.zookeeper.ZooKeeper#getData()即可创建节点,其参数说明如下:
| 参数名称 | 类型 | 说明 |
|---|---|---|
| path | String | |
| watch | **boolean ** | |
| watcher | Watcher | |
| cb | DataCallback | |
| ctx | Object |
查看子节点: 通过org.apache.zookeeper.ZooKeeper#getChildren()即可获取子节点,其参数说明如下:
| 参数名称 | 类型 | 说明 |
|---|---|---|
| path | String | |
| watch | **boolean ** | |
| watcher | Watcher | |
| cb | Children2Callback | |
| ctx | Object |
3.监听节点
在getData() 与getChildren()两个方法中可分别设置监听数据变化和子节点变化。通过设置watch为true,当前事件触发时会调用zookeeper()构建函数中Watcher.process()方法。也可以添加watcher参数来实现自定义监听。一般采用后者。 注:所有的监听都是一次性的,如果要持续监听需要触发后在添加一次监听。
4.设置节点ACL权限
ACL包括结构为scheme:id:permission(有关ACL的介绍参照第一节课关于ACL的讲解) 客户端中由org.apache.zookeeper.data.ACL 类表示,类结构如下:
- ACL
- Id 1. scheme // 对应权限模式scheme 2. id // 对应模式中的id值
- perms // 对应权限位permission
关于权限位的表示方式: 每个权限位都是一个唯一数字,将其合时通过或运行生成一个全新的数字即可
@InterfaceAudience.Public
public interface Perms {
int READ = 1 << 0;
int WRITE = 1 << 1;
int CREATE = 1 << 2;
int DELETE = 1 << 3;
int ADMIN = 1 << 4;
int ALL = READ | WRITE | CREATE | DELETE | ADMIN;
}
5.第三方客户端ZkClient
zkClient 是在zookeeper客户端基础之上封装的,使用上更加友好。主要变化如下:
- 可以设置持久监听,或删除某个监听
- 可以插入JAVA对象,自动进行序列化和反序列化
- 简化了基本的增删改查操作。
二、Zookeeper集群
知识点:
- 集群部署
- 集群角色说明
- 选举机制
- 数据提交机制
- 集群配置说明
zookeeper集群的目的是为了保证系统的性能承载更多的客户端连接设专门提供的机制。通过集群可以实现以下功能:
- 读写分离:提高承载,为更多的客户端提供连接,并保障性能。
- 主从自动切换:提高服务容错性,部分节点故障不会影响整个服务集群。
半数以上运行机制说明: 集群至少需要三台服务器,并且强烈建议使用奇数个服务器。因为zookeeper 通过判断大多数节点的存活来判断整个服务是否可用。比如3个节点,挂掉了2个表示整个集群挂掉,而用偶数4个,挂掉了2个也表示其并不是大部分存活,因此也会挂掉。
- 集群部署
配置语法: server.<节点ID>=:<数据同步端口>:<选举端口>
- 节点****ID:服务id手动指定1至125之间的数字,并写到对应服务节点的 {dataDir}/myid 文件中。
- **IP地址:**节点的远程IP地址,可以相同。但生产环境就不能这么做了,因为在同一台机器就无法达到容错的目的。所以这种称作为伪集群。
- **数据同步端口:**主从同时数据复制端口,(做伪集群时端口号不能重复)。
- **远举端口:**主从节点选举端口,(做伪集群时端口号不能重复)。
配置文件示例:
tickTime=2000
dataDir=/var/lib/zookeeper/
clientPort=2181
initLimit=5
syncLimit=2
#以下为集群配置,必须配置在所有节点的zoo.cfg文件中
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
集群配置流程:
- 分别创建3个data目录用于存储各节点数据
mkdir data
mkdir data/1
mkdir data/3
mkdir data/3
- 编写myid文件
echo 1 > data/1/myid
echo 3 > data/3/myid
echo 2 > data/2/myid
3、编写配置文件 conf/zoo1.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=data/1
clientPort=2181
#集群配置
server.1=127.0.0.1:2887:3887
server.2=127.0.0.1:2888:3888
server.3=127.0.0.1:2889:3889
conf/zoo2.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=data/2
clientPort=2182
#集群配置
server.1=127.0.0.1:2887:3887
server.2=127.0.0.1:2888:3888
server.3=127.0.0.1:2889:3889
conf/zoo3.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=data/3
clientPort=2183
#集群配置
server.1=127.0.0.1:2887:3887
server.2=127.0.0.1:2888:3888
server.3=127.0.0.1:2889:3889
4.分别启动
./bin/zkServer.sh start conf/zoo1.cfg
./bin/zkServer.sh start conf/zoo2.cfg
./bin/zkServer.sh start conf/zoo3.cfg
5.分别查看状态
./bin/zkServer.sh status conf/zoo1.cfg
Mode: follower
./bin/zkServer.sh status conf/zoo2.cfg
Mode: leader
./bin/zkServer.sh status conf/zoo3.cfg
Mode: follower
检查集群复制情况: 1、分别连接指定节点 zkCli.sh 后加参数-server 表示连接指定IP与端口。
./bin/zkCli.sh -server 127.0.0.1:2181
./bin/zkCli.sh -server 127.0.0.1:2182
./bin/zkCli.sh -server 127.0.0.1:2183
- 任意节点中创建数据,查看其它节点已经同步成功。
注意: -server参数后同时连接多个服务节点,并用逗号隔开 127.0.0.1:2181,127.0.0.1:2182
- 集群角色说明
zookeeper 集群中总共有三种角色,分别是leader(主节点)follower(子节点) observer(次级子节点)
| 角色 | 描述 |
|---|---|
| leader | 主节点,又名领导者。用于写入数据,通过选举产生,如果宕机将会选举新的主节点。 |
| follower | 子节点,又名追随者。用于实现数据的读取。同时他也是主节点的备选节点,并用拥有投票权。 |
| observer | 次级子节点,又名观察者。用于读取数据,与fllower区别在于没有投票权,不能选为主节点。并且在计算集群可用状态时不会将observer计算入内。 |
observer配置: 只要在集群配置中加上observer后缀即可,示例如下:
server.3=127.0.0.1:2889:3889:observer
3.选举机制
通过 ./bin/zkServer.sh status <zoo配置文件> 命令可以查看到节点状态
./bin/zkServer.sh status conf/zoo1.cfg
Mode: follower
./bin/zkServer.sh status conf/zoo2.cfg
Mode: leader
./bin/zkServer.sh status conf/zoo3.cfg
Mode: follower
可以发现中间的2182 是leader状态.
投票机制说明: 第一轮投票全部投给自己 第二轮投票给myid比自己大的相邻节点 如果得票超过半数,选举结束。
选举触发: 当集群中的服务器出现已下两种情况时会进行Leader的选举
- 服务节点初始化启动
- 半数以上的节点无法和Leader建立连接
当节点初始起动时会在集群中寻找Leader节点,如果找到则与Leader建立连接,其自身状态变化follower或**observer。**如果没有找到Leader,当前节点状态将变化LOOKING,进入选举流程。 在集群运行其间如果有follower或observer节点宕机只要不超过半数并不会影响整个集群服务的正常运行。但如果leader宕机,将暂停对外服务,所有follower将进入LOOKING 状态,进入选举流程。
- 数据同步机制
zookeeper 的数据同步是为了保证各节点中数据的一至性,同步时涉及两个流程,一个是正常的客户端数据提交,另一个是集群某个节点宕机在恢复后的数据同步。
客户端写入请求:
写入请求的大至流程是,收leader接收客户端写请求,并同步给各个子节点。 但实际情况要复杂的多,比如client 它并不知道哪个节点是leader 有可能写的请求会发给follower ,由follower在转发给leader进行同步处理 客户端写入流程说明:
- client向zk中的server发送写请求,如果该server不是leader,则会将该写请求转发给leader server,leader将请求事务以proposal形式分发给follower;
- 当follower收到收到leader的proposal时,根据接收的先后顺序处理proposal;
- 当Leader收到follower针对某个proposal过半的ack后,则发起事务提交,重新发起一个commit的proposal
- Follower收到commit的proposal后,记录事务提交,并把数据更新到内存数据库;
- 当写成功后,反馈给client。
服务节点初始化同步: 在集群运行过程当中如果有一个follower节点宕机,由于宕机节点没过半,集群仍然能正常服务。当leader 收到新的客户端请求,此时无法同步给宕机的节点。造成数据不一至。为了解决这个问题,当节点启动时,第一件事情就是找当前的Leader,比对数据是否一至。不一至则开始同步,同步完成之后在进行对外提供服务。 如何比对Leader的数据版本呢,这里通过ZXID事物ID来确认。比Leader就需要同步。 ZXID说明: ZXID是一个长度64位的数字,其中低32位是按照数字递增,任何数据的变更都会导致,低32位的数字简单加1。高32位是leader周期编号,每当选举出一个新的leader时,新的leader就从本地事物日志中取出ZXID,然后解析出高32位的周期编号,进行加1,再将低32位的全部设置为0。这样就保证了每次新选举的leader后,保证了ZXID的唯一性而且是保证递增的。
思考题: 如果leader 节点宕机,在恢复后它还能被选为leader吗?
5.四字运维命令
ZooKeeper响应少量命令。每个命令由四个字母组成。可通过telnet或nc向ZooKeeper发出命令。 这些命令默认是关闭的,需要配置4lw.commands.whitelist来打开,可打开部分或全部示例如下:
#打开指定命令
4lw.commands.whitelist=stat, ruok, conf, isro
#打开全部
4lw.commands.whitelist=*
安装Netcat工具,已使用nc命令
#安装Netcat 工具
yum install -y nc
#查看服务器及客户端连接状态
echo stat | nc localhost 2181
命令列表
-
conf:3.3.0中的新增功能:打印有关服务配置的详细信息。
-
缺点:3.3.0中的新增功能:列出了连接到该服务器的所有客户端的完整连接/会话详细信息。包括有关已接收/已发送的数据包数量,会话ID,操作等待时间,最后执行的操作等信息。
-
crst:3.3.0中的新增功能:重置所有连接的连接/会话统计信息。
-
dump:列出未完成的会话和临时节点。这仅适用于领导者。
-
envi:打印有关服务环境的详细信息
-
ruok:测试服务器是否以非错误状态运行。如果服务器正在运行,它将以imok响应。否则,它将完全不响应。响应“ imok”不一定表示服务器已加入仲裁,只是服务器进程处于活动状态并绑定到指定的客户端端口。使用“ stat”获取有关状态仲裁和客户端连接信息的详细信息。
-
srst:重置服务器统计信息。
-
srvr:3.3.0中的新功能:列出服务器的完整详细信息。
-
stat:列出服务器和连接的客户端的简要详细信息。
-
wchs:3.3.0中的新增功能:列出有关服务器监视的简要信息。
-
wchc:3.3.0中的新增功能:按会话列出有关服务器监视的详细信息。这将输出具有相关监视(路径)的会话(连接)列表。请注意,根据手表的数量,此操作可能会很昂贵(即影响服务器性能),请小心使用。
-
dirs:3.5.1中的新增功能:以字节为单位显示快照和日志文件的总大小
-
wchp:3.3.0中的新增功能:按路径列出有关服务器监视的详细信息。这将输出具有关联会话的路径(znode)列表。请注意,根据手表的数量,此操作可能会很昂贵(即影响服务器性能),请小心使用。
-
mntr:3.4.0中的新增功能:输出可用于监视集群运行状况的变量列表。
课程概要:
1. 分布式集群管理
2. 分布式注册中心
3. 分布式JOB
4. 分布式锁
一、 分布式集群管理****
分布式集群管理的需求:****
1. 主动查看线上服务节点
2. 查看服务节点资源使用情况
3. 服务离线通知
4. 服务资源(CPU、内存、硬盘)超出阀值通知
架构设计:****
节点结构:
1. tuling-manger // 根节点
a. server00001 : //服务节点 1
b. server00002 ://服务节点 2
c. server........n ://服务节点 n
服务状态信息:
a. ip
b. cpu
c. memory
d. disk
功能实现:****
数据生成与上报:
1. 创建临时节点:
2. 定时变更节点状态信息:
主动查询:
1、实时查询 zookeeper 获取集群节点的状态信息。
被动通知:
1. 监听根节点下子节点的变化情况,如果CPU 等硬件资源低于警告位则发出警报。
关键示例代码:
package com.tuling;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tuling.os.CPUMonitorCalc;
import com.tuling.os.OsBean;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Tommy
* Created by Tommy on 2019/9/22
**/
public class Agent {
private String server = "192.168.0.149:2181";
ZkClient zkClient;
private static Agent instance;
private static final String rootPath = "/tuling-manger";
private static final String servicePath = rootPath + "/service";
private String nodePath;
private Thread stateThread;
List list = new ArrayList<>();
public static void premain(String args, Instrumentation instrumentation) {
instance = new Agent();
if (args != null) {
instance.server = args;
}
instance.init();
}
// 初始化连接
public void init() {
zkClient = new ZkClient(server, 5000, 10000);
System.out.println("zk连接成功" + server);
buildRoot();
createServerNode();
stateThread = new Thread(() -> {
while (true) {
updateServerNode();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "zk_stateThread");
stateThread.setDaemon(true);
stateThread.start();
}
// 构建根节点
public void buildRoot() {
if (!zkClient.exists(rootPath)) {
zkClient.createPersistent(rootPath);
}
}
// 生成服务节点
public void createServerNode() {
nodePath = zkClient.createEphemeralSequential(servicePath, getOsInfo());
System.out.println("创建节点:" + nodePath);
}
// 监听服务节点状态改变
public void updateServerNode() {
zkClient.writeData(nodePath, getOsInfo());
}
// 更新服务节点状态
public String getOsInfo() {
OsBean bean = new OsBean();
bean.lastUpdateTime = System.currentTimeMillis();
bean.ip = getLocalIp();
bean.cpu = CPUMonitorCalc.getInstance().getProcessCpu();
MemoryUsage memoryUsag = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
bean.usableMemorySize = memoryUsag.getUsed() / 1024 / 1024;
bean.usableMemorySize = memoryUsag.getMax() / 1024 / 1024;
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(bean);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public void updateNode(String path, Object data) {
if (zkClient.exists(path)) {
zkClient.writeData(path, data);
} else {
zkClient.createEphemeral(path, data);
}
}
public static String getLocalIp() {
InetAddress addr = null;
try {
addr = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
return addr.getHostAddress();
}
}
实现效果图:
二 、分布式注册中心****
在单体式服务中,通常是由多个客户端去调用一个服务,只要在客户端中配置唯一服务节点地址即可,当升级到分布式后,服务节点变多,像阿里一线大厂服务节点更是上万之多,这么多节点不可能手动配置在客户端,这里就需要一个中间服务,专门用于帮助客户端发现服务节点,即许多技术书籍经常提到的服务发现。
一个完整的注册中心涵盖以下功能特性:
l 服务注册: 提供者上线时将自提供的服务提交给注册中心。
l 服务注销: 通知注册心提供者下线。
l 服务订阅:动态实时接收服务变更消息。
l 可靠:注册服务本身是集群的,数据冗余存储。避免单点故障,及数据丢失。
l 容错:当服务提供者出现宕机,断电等极情况时,注册中心能够动态感知并通知客户端服务提供者的状态。
Dubbo 对zookeeper的使用****
阿里著名的开源项目Dubbo 是一个基于JAVA的RCP框架,其中必不可少的注册中心可基于多种第三方组件实现,但其官方推荐的还是Zookeeper做为注册中心服务。
Dubbo Zookeeper注册中心存储结构:****
节点说明:
| 类别 | 属性 | 说明 |
|---|---|---|
| Root | 持久节点 | 根节点名称,默认是 "dubbo" |
| Service | 持久节点 | 服务名称,完整的服务类名 |
| type | 持久节点 | 可选值:providers(提供者)、consumers(消费者)、configurators(动态配置)、routers |
| URL | 临时节点 | url名称 包含服务提供者的 IP 端口 及配置等信息。 |
流程说明:
1. 服务提供者启动时: 向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址
2. 服务消费者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址
3. 监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。
示例演示:****
服务端代码:
package com.tuling.zk.dubbo;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.ServiceConfig;
import java.io.IOException;
/**
* @author Tommy
* Created by Tommy on 2019/10/8
**/
public class Server {
public void openServer(int port) {
// 构建应用
ApplicationConfig config = new ApplicationConfig();
config.setName("simple-app");
// 通信协议
ProtocolConfig protocolConfig = new ProtocolConfig("dubbo", port);
protocolConfig.setThreads(200);
ServiceConfig serviceConfig = new ServiceConfig();
serviceConfig.setApplication(config);
serviceConfig.setProtocol(protocolConfig);
serviceConfig.setRegistry(new RegistryConfig("zookeeper://192.168.0.149:2181"));
serviceConfig.setInterface(UserService.class);
UserServiceImpl ref = new UserServiceImpl();
serviceConfig.setRef(ref);
//开始提供服务 开张做生意
serviceConfig.export();
System.out.println("服务已开启!端口:"+serviceConfig.getExportedUrls().get(0).getPort());
ref.setPort(serviceConfig.getExportedUrls().get(0).getPort());
}
public static void main(String[] args) throws IOException {
new Server().openServer(-1);
System.in.read();
}
}
客户端代码:
package com.tuling.zk.dubbo;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import java.io.IOException;
/**
* @author Tommy
* Created by Tommy on 2018/11/20
**/
public class Client {
UserService service;
// URL 远程服务的调用地址
public UserService buildService(String url) {
ApplicationConfig config = new ApplicationConfig("young-app");
// 构建一个引用对象
ReferenceConfig referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(config);
referenceConfig.setInterface(UserService.class);
// referenceConfig.setUrl(url);
referenceConfig.setRegistry(new RegistryConfig("zookeeper://192.168.0.149:2181"));
referenceConfig.setTimeout(5000);
// 透明化
this.service = referenceConfig.get();
return service;
}
static int i = 0;
public static void main(String[] args) throws IOException {
Client client1 = new Client();
client1.buildService("");
String cmd;
while (!(cmd = read()).equals("exit")) {
UserVo u = client1.service.getUser(Integer.parseInt(cmd));
System.out.println(u);
}
}
private static String read() throws IOException {
byte[] b = new byte[1024];
int size = System.in.read(b);
return new String(b, 0, size).trim();
}
}
查询zk 实际存储内容:
/dubbo
/dubbo/com.tuling.zk.dubbo.UserService
/dubbo/com.tuling.zk.dubbo.UserService/configurators
/dubbo/com.tuling.zk.dubbo.UserService/routers
/dubbo/com.tuling.zk.dubbo.UserService/providers
/dubbo/com.tuling.zk.dubbo.UserService/providers/dubbo://192.168.0.132:20880/com.tuling.zk.dubbo.UserService?anyhost=true&application=simple-app&dubbo=2.6.2&generic=false&interface=com.tuling.zk.dubbo.UserService&methods=getUser&pid=11128&side=provider&threads=200×tamp=1570518302772
/dubbo/com.tuling.zk.dubbo.UserService/providers/dubbo://192.168.0.132:20881/com.tuling.zk.dubbo.UserService?anyhost=true&application=simple-app&dubbo=2.6.2&generic=false&interface=com.tuling.zk.dubbo.UserService&methods=getUser&pid=12956&side=provider&threads=200×tamp=1570518532382
/dubbo/com.tuling.zk.dubbo.UserService/providers/dubbo://192.168.0.132:20882/com.tuling.zk.dubbo.UserService?anyhost=true&application=simple-app&dubbo=2.6.2&generic=false&interface=com.tuling.zk.dubbo.UserService&methods=getUser&pid=2116&side=provider&threads=200×tamp=1570518537021
/dubbo/com.tuling.zk.dubbo.UserService/consumers
/dubbo/com.tuling.zk.dubbo.UserService/consumers/consumer://192.168.0.132/com.tuling.zk.dubbo.UserService?application=young-app&category=consumers&check=false&dubbo=2.6.2&interface=com.tuling.zk.dubbo.UserService&methods=getUser&pid=9200&side=consumer&timeout=5000×tamp=1570518819628
三、分布式JOB****
分布式JOB需求:****
1. 多个服务节点只允许其中一个主节点运行JOB任务。
2. 当主节点挂掉后能自动切换主节点,继续执行JOB任务。
架构设计:****
node结构:
1. tuling-master
a. server0001:master
b. server0002:slave
c. server000n:slave
选举流程:
服务启动:
1. 在tuling-maste下创建server子节点,值为slave
2. 获取所有tuling-master 下所有子节点
3. 判断是否存在master 节点
4. 如果没有设置自己为master节点
子节点删除事件触发:
1. 获取所有tuling-master 下所有子节点
2. 判断是否存在master 节点
3. 如果没有设置最小值序号为master 节点
四、分布式锁****
锁的的基本概念:****
开发中锁的概念并不陌生,通过锁可以实现在多个线程或多个进程间在争抢资源时,能够合理的分配置资源的所有权。在单体应用中我们可以通过 synchronized 或ReentrantLock 来实现锁。但在分布式系统中,仅仅是加synchronized 是不够的,需要借助第三组件来实现。比如一些简单的做法是使用 关系型数据行级锁来实现不同进程之间的互斥,但大型分布式系统的性能瓶颈往往集中在数据库操作上。为了提高性能得采用如Redis、Zookeeper之内的组件实现分布式锁。
共享锁: 也称作只读锁,当一方获得共享锁之后,其它方也可以获得共享锁。但其只允许读取。在共享锁全部释放之前,其它方不能获得写锁。
排它锁: 也称作读写锁,获得排它锁后,可以进行数据的读写。在其释放之前,其它方不能获得任何锁。
锁的获取:****
某银行帐户,可以同时进行帐户信息的读取,但读取其间不能修改帐户数据。其帐户ID为:888
l 获得读锁流程:
1、基于资源ID创建临时序号读锁节点
/lock/888.R0000000002 Read
2、获取 /lock 下所有子节点,判断其最小的节点是否为读锁,如果是则获锁成功
3、最小节点不是读锁,则阻塞等待。添加lock/ 子节点变更监听。
4、当节点变更监听触发,执行第2步
数据结构:
l 获得写锁:
1、基于资源ID创建临时序号写锁节点
/lock/888.R0000000002 Write
2、获取 /lock 下所有子节点,判断其最小的节点是否为自己,如果是则获锁成功
3、最小节点不是自己,则阻塞等待。添加lock/ 子节点变更监听。
4、当节点变更监听触发,执行第2步
l 释放锁:
读取完毕后,手动删除临时节点,如果获锁期间宕机,则会在会话失效后自动删除。
关于羊群效应:****
在等待锁获得期间,所有等待节点都在监听 Lock节点,一但lock 节点变更所有等待节点都会被触发,然后在同时反查Lock 子节点。如果等待对例过大会使用Zookeeper承受非常大的流量压力。
为了改善这种情况,可以采用监听链表的方式,每个等待对列只监听前一个节点,如果前一个节点释放锁的时候,才会被触发通知。这样就形成了一个监听链表。
示例演示:****
package com.tuling.zookeeper.lock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Tommy
* Created by Tommy on 2019/9/23
**/
public class ZookeeperLock {
private String server = "192.168.0.149:2181";
private ZkClient zkClient;
private static final String rootPath = "/tuling-lock";
public ZookeeperLock() {
zkClient = new ZkClient(server, 5000, 20000);
buildRoot();
}
// 构建根节点
public void buildRoot() {
if (!zkClient.exists(rootPath)) {
zkClient.createPersistent(rootPath);
}
}
public Lock lock(String lockId, long timeout) {
Lock lockNode = createLockNode(lockId);
lockNode = tryActiveLock(lockNode);// 尝试激活锁
if (!lockNode.isActive()) {
try {
synchronized (lockNode) {
lockNode.wait(timeout);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (!lockNode.isActive()) {
throw new RuntimeException(" lock timeout");
}
return lockNode;
}
public void unlock(Lock lock) {
if (lock.isActive()) {
zkClient.delete(lock.getPath());
}
}
// 尝试激活锁
private Lock tryActiveLock(Lock lockNode) {
// 判断当前是否为最小节点
List list = zkClient.getChildren(rootPath)
.stream()
.sorted()
.map(p -> rootPath + "/" + p)
.collect(Collectors.toList());
String firstNodePath = list.get(0);
if (firstNodePath.equals(lockNode.getPath())) {
lockNode.setActive(true);
} else {
String upNodePath = list.get(list.indexOf(lockNode.getPath()) - 1);
zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
// 事件处理 与心跳 在同一个线程,如果Debug时占用太多时间,将导致本节点被删除,从而影响锁逻辑。
System.out.println("节点删除:" + dataPath);
Lock lock = tryActiveLock(lockNode);
synchronized (lockNode) {
if (lock.isActive()) {
lockNode.notify();
}
}
zkClient.unsubscribeDataChanges(upNodePath, this);
}
});
}
return lockNode;
}
public Lock createLockNode(String lockId) {
String nodePath = zkClient.createEphemeralSequential(rootPath + "/" + lockId, "lock");
return new Lock(lockId, nodePath);
}
}