Zookeeper
注册中心
微服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器,维护带来很大问题。常见的注册中心:zookeeper、Eureka、consul、etcd。
- 理解注册中心:服务管理,核心是有个服务注册表,心跳机制动态维护。
- 服务提供者 provider: 启动的时候向注册中心上报自己的网络信息。
- 服务消费者 consumer: 启动的时候向注册中心上报自己的网络信息,拉取 provider 的相关网络信息。
安装 JDK1.8
# 解压
tar -zxvf jdk-8u191-linux-x64.tar.gz
# 移动到/usr/local目录下
mv jdk1.8.0_191 /usr/local/
# 修改/etc/profile,在文件的最后面加上下面几行
vim /etc/profile
JAVA_HOME=/usr/local/jdk1.8.0_191
JRE_HOME=$JAVA_HOME/jre
PATH=$JAVA_HOME/bin:$PATH
export PATH
# 激活配置
source /etc/profile
# 验证
[root@localhost test]# java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
安装 Zookeeper
# 解压
tar -zxvf zookeeper-3.4.12.tar.gz
mv zookeeper-3.4.12 /usr/local/
# 修改配置文件
cp /usr/local/zookeeper-3.4.12/conf/zoo_sample.cfg /usr/local/zookeeper-3.4.12/conf/zoo.cfg
vim /usr/local/zookeeper-3.4.12/conf/zoo.cfg
dataDir=/usr/local/zookeeper-3.4.12/data
# 创建数据目录
mkdir /usr/local/zookeeper-3.4.12/data
useradd zookeeper
chown -R zookeeper:zookeeper /usr/local/zookeeper-3.4.12/
# 启动服务
bash /usr/local/zookeeper-3.4.12/bin/zkServer.sh start
# 使用zkCli连接Zookeeper
.\zkCli.cmd -timeout 5000 -server 192.168.31.220:2181
Connecting to 192.168.31.220:2181
ientCnxn$SendThread@1302] - Session establishment complete on server 192.168.31.220/192.168.31.220:2181, sessionid = 0x10014aa999f0000, negotiated timeout = 5000
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
[zk: 192.168.31.220:2181(CONNECTED) 0] stat /
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
#Zookeeper 的数据模型
- zookeeper 的数据模型类似于 Linux 下的文件目录。
- 每一个节点都叫做 zNode,可以有子节点,也可以有数据。
- 每个节点都能设置相应的权限控制用户的访问(ACL)。
- 每个节点存储的数据不宜过大。
- 每个节点都带有一个版本号,数据变更时,版本号变更(乐观锁)。
- 节点分永久节点跟临时节点(session 断开,临时节点便会自动删除)。
zkCli
# 创建顺序节点soulboy 数据是soulboy_data
[zk: 192.168.31.220:2181(CONNECTED) 1] create -s /soulboy soulboy_data
Created /soulboy0000000000
# 创建临时节点
[zk: 192.168.31.220:2181(CONNECTED) 2] create -e /zbk zbk_data
Created /zbk
# 获取节点的子节点
[zk: 192.168.31.220:2181(CONNECTED) 4] ls /
[zbk, zookeeper, soulboy0000000000]
# 获取节点的数据
[zk: 192.168.31.220:2181(CONNECTED) 13] get /soulboy0000000000
soulboy_data
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x2
mtime = Tue Jul 23 12:07:49 CST 2019
pZxid = 0x2
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 12
numChildren = 0
# 查看节点的状态
[zk: 192.168.31.220:2181(CONNECTED) 12] stat /soulboy0000000000
cZxid = 0x2 # 事务id(创建新节点)
ctime = Tue Jul 23 12:07:49 CST 2019 #节点创建时间
mZxid = 0x2 # 最后一次更新时,事务的id (对节点进行修改)
mtime = Tue Jul 23 12:07:49 CST 2019 # 最后一次更新的时间
pZxid = 0x2 # 该节点的子节点列表最后一次被修改的事务id
cversion = 0 # 子节点列表的版本
dataVersion = 0 # 数据内容的版本
aclVersion = 0 # ACL版本
ephemeralOwner = 0x0 # 用于临时节点,表示创建该临时节点的事务id,如果不是临时节点,该字段为0
dataLength = 12 # 数据内容长度
numChildren = 0 # 子节点的数量
# 创建子节点
[zk: 192.168.31.220:2181(CONNECTED) 4] create -e /soulboy0000000000/test test
Created /soulboy0000000000/test
# 获取节点的子节点以及当前节点的状态
[zk: 192.168.31.220:2181(CONNECTED) 5] ls2 /soulboy0000000000
[test] # 子节点在此,下面是当前节点的状态
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x2
mtime = Tue Jul 23 12:07:49 CST 2019
pZxid = 0x6
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 12
numChildren = 1
# 修改节点数据
[zk: 192.168.31.220:2181(CONNECTED) 7] set /soulboy0000000000 soulboy_data_new
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x7
mtime = Tue Jul 23 12:37:50 CST 2019
pZxid = 0x6
cversion = 1
dataVersion = 1 #原本是0,修改之后就会加1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 16
numChildren = 1
# 删除节点(无法删除有子节点的节点)
[zk: 192.168.31.220:2181(CONNECTED) 8] delete /soulboy0000000000
Node not empty: /soulboy0000000000
# 递归删除整个节点(可以递归删除有子节点的节点)
[zk: 192.168.31.220:2181(CONNECTED) 8] rmr /soulboy0000000000
Zookeeper Session 机制
临时节点的生命周期跟随 session 的生命周期。当 session 过期的时候,该 session 创建的所有临时节点都会被删除。
# 创建临时节点
create -e /test test
# 关闭session
close
# 登录查看刚才创建的临时节点 :发现test节点已经被删除
.\zkCli.cmd -timeout 5000 -server 192.168.31.220:2181
[zk: 192.168.31.220:2181(CONNECTED) 0] ls /
[zookeeper, soulboy0000000000]
Zookeeper Watcher 机制
对节点的 watcher 操作 get stat
针对每一个节点的操作,都可以有一个监控者,当节点数据发生变化,会触发 watcher 事件 zk 中 watcher 是一次性的,触发后立即销毁 所有有监控者的节点的变更操作都能触发 watcher 事件。
子节点的 watcher 操作 ls ls2
监控父节点,当父节点对应的子节点发生变更的时候,父节点上的 watcher 事件会被触发, 增删会触发、修改不会数据则不会触发,如果子节点再去新增子节点,也不会触发(也就是说,触发 watcher 事件一定是直系子节点)。
# 监控当前节点的数据变化:get、stat
[zk: 192.168.31.220:2181(CONNECTED) 3] get /soulboy0000000000 watch
# 监控当前的直系子节点们(增删子节点):ls、ls2
[zk: 192.168.31.220:2181(CONNECTED) 3] ls /soulboy0000000000 watch
Zookeeper ACL 权限控制
针对节点可以设置相关的读写等权限,目的是为了保证数据的安全性,权限 permissions 可以指定不同的权限范围及角色,只有拥有相应权限和角色的用户才可以访问对应的 ZNODE。
- 开发环境跟测试环境,使用 acl 就可以进行分离,开发者无权去操作测试的节点。
- 生产环境上控制指定 ip 的服务可以访问相关的节点。
# scheme授权机制
* acl组成[scheme:id:permissions]
* world下只有一个id,也就是anyone,表示所有人
* world:anyone:permissions
* auth:代表认证登录,需要注册用户有权限才可以
* auth:user:password:permissions
* digest 需要密码加密才能访问
* digest:username:BASE64(SHA1(password)):permissions(跟auth区别在于,auth明文,digest为密文)
* ip:ip:localhost:psermissions
* super:代表超管,拥有所有的权限;
* id:允许访问的用户
* permissions:权限组合字符串
cdrwa
c create 创建子节点
d delete 删除子节点
r read 读取节点的数据
w write 写入数据
a admin 可管理的权限
cdrwa cdr cdw
# 获取节点权限 getAcl
[zk: 192.168.31.220:2181(CONNECTED) 4] getAcl /soulboy0000000000
'world,'anyone
: cdrwa
#设置节点权限 setAcl
[zk: 192.168.31.220:2181(CONNECTED) 6] addauth digest soulboy:soulboy # 添加用户
[zk: 192.168.31.220:2181(CONNECTED) 7] setAcl /soulboy0000000000 auth:soulboy:soulboy:cdrwa
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x7
mtime = Tue Jul 23 12:37:50 CST 2019
pZxid = 0x9
cversion = 2
dataVersion = 1
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 16
numChildren = 0
# 查看权限soulboy0000000000节点权限
[zk: 192.168.31.220:2181(CONNECTED) 8] getAcl /soulboy0000000000
'digest,'soulboy:VYanT576Pu1fFRBjBed+WuMZJJI=
: cdrwa
# 使用zkCli新建终端来验证
[zk: 192.168.31.220:2181(CONNECTED) 1] get /soulboy0000000000
Authentication is not valid : /soulboy0000000000
# 登录指定用户,再次查看soulboy0000000000节点
[zk: 192.168.31.220:2181(CONNECTED) 2] addauth digest soulboy:soulboy
[zk: 192.168.31.220:2181(CONNECTED) 3] get /soulboy0000000000
soulboy_data_new
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x7
mtime = Tue Jul 23 12:37:50 CST 2019
pZxid = 0x9
cversion = 2
dataVersion = 1
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 16
numChildren = 0
# 修改权限:少了d,代表没有删除子节点的权限
[zk: 192.168.31.220:2181(CONNECTED) 4] setAcl /soulboy0000000000 auth:soulboy:soulboy:crwa
Zookeeper Cluster Role
- leader:作为整个 zk 集群写请求的唯一处理者,并负责进行投票的发起和决议(集群中过半节点同意提议,才可以写入),更新系统的状态。
- follower:接收客户端请求,处理读请求,并向客户端返回结果;将写请求转给 Leader;在选举 Leader 过程中参与投票。
- observer:可以理解为无选举投票权的 Flollower,其主要是为了协助 Follower 处理更多的读请求。如果 Zookeeper 集群的读请求负载很高,或者客户端非常非常多,多到跨机房,则可以设置一些 Observer 服务器,以提高读取的吞吐量。ZK Server 节点宕机过半,则整个 Zookeeper Cluster 不可用,而 observer 宕机不会影响 Zookeeper Cluster。
Zookeeper Cluster Mode
Zookeeper Cluster 的核心是广播机制,该机制保证了各个 zk 之间数据同步(数据一致性)。zk 实现的机制为 ZAB 协议。
- 恢复模式: 如果 leader 崩溃,这个时候就会进入恢复模式,使整个 zk 集群恢复到正常的工作状态。(进行选举新 Leader 等一系列操作)
- 同步模式:新的 leader 选举出来后,就乎进入同步模式(各个 follower 会去同步新的 leader 上的数据),当大多数 zkServer 完成了与 leader 的状态同步之后,恢复模式就结束。
- 广播模式:客户端想写入数据,这个时候 leader 发起提议,当 leader 的提议被大多数的 zkServer 统一之后,leader 就会去修改自身的数据,并将修改后的数据广播给其他的 follower。
Zookeeper Cluster Mode of Election
核心概念
# myid
这是Zookeeper Cluster中服务器的唯一标识,称为 myid。例如,有三个 zk 服务器,那么编号分别是 1,2,3。
# zxid
ReentranReadWriteLock 32位
高位 低位
0000000000000000 0000000000000000
epoch xid
00000000000000000000000000000000 00000000000000000000000000000000
zxid 为 Long 类型,其中高 32 位表示 epoch,低 32 位表示 xid。即 zxid 由两部分构成:epoch 与 xid。
每个 Leader 都会具有一个不同的 epoch 值,表示一个时期、时代。新的 Leader 产生,则会更新所有 zkServer 的 zxid 中的 epoch。 而 xid 则为 zk 的事务 id,每一个写操作都是一个事务,都会有一个 xid。每一个写操作都需要由 Leader 发起一个提议,由所有 Follower 表决是否同意本次写操作。
# 逻辑时钟
逻辑时钟,Logicalclock,是一个整型数,该概念在选举时称为 logicalclock,而在 zxid 中则为 epoch 的值。即 epoch 与 logicalclock 是同一个值,在不同情况下的不同名称。
Zookeeper Cluster 选举状态
- LOOKING,选举状态(查找 Leader 的状态)。
- LEADING,领导者状态。处于该状态的服务器称为 Leader。
- FOLLOWING,随从状态,同步 leader 状态。处于该状态的服务器称为 Follower。
- OBSERVING,观察状态,同步 leader 状态。处于该状态的服务器称为 Observer。
Zookeeper Cluster Electioneering
发生时机
整个集群群龙无首的时候
- Zookeeper Cluster 服务首次启动。
- Zookeeper Cluster 运行中,Leader 宕机的时候。
选举机制
集群中,半数 zkServer 同意,则产生新的 leader(搭建集群时,一般都是奇数个),三台服务器,最多允许一台宕机,四台服务器,也是最多允许一台宕机。
选举算法
对比(myid,zxid),先对比 zxid,zxid 大者(大表示数据越新)胜出,成为 leader,如果 zxid 一致,则 myid 大者成为 leader。
Zookeeper Cluster 部署
端口的作用
- 2181:对 client 端提供服务
- 2888:集群内及其通讯使用的端口
- 3888:集群选举 leader
# 修改/etc/hosts文件
192.168.31.220 zkcluster1
192.168.31.240 zkcluster2
192.168.31.250 zkcluster3
# 添加zookeeper用户
useradd zookeeper
# 修改zk配置文件zoo.cfg
dataDir=/usr/local/zookeeper-3.4.12/data #数据文件存放目录
server.1=zkcluster1:2888:3888 #分别添加集群中的每台ZK Server节点。
server.2=zkcluster2:2888:3888
server.3=zkcluster3:2888:3888
# 在/usr/local/zookeeper-3.4.12/data目录下新建myid的文件
cat /usr/local/zookeeper-3.4.12/data myid # zkcluster1
1
cat /usr/local/zookeeper-3.4.12/data myid # zkcluster2
2
cat /usr/local/zookeeper-3.4.12/data myid # zkcluster3
3
# 赋权zookeeper用户
chown -R zookeeper:zookeeper /usr/local/zookeeper-3.4.12/
# 关闭防火墙
systemctl stop firewalld.service
# 启动服务
bash /usr/local/zookeeper-3.4.12/bin/zkServer.sh start
# 删除所有windows风格的启动脚本
rm -rf /usr/local/zookeeper-3.4.12/bin/*.cmd
# 为启动脚本增加执行权限
chmod +x /usr/local/zookeeper-3.4.12/bin/*.sh
# 查看状态
zkServer.sh status
# 查看进程PID
jsp
分布式锁作用及其原理
为什么要有分布式锁
分布式服务中,如果各个服务节点需要去竞争资源,无法使用单机多线程中 JDK 自带的锁来解决此类问题,故此时需要分布式锁来协调。
企业中有哪些常见的手段来实现分布式锁
zookeeper、Redis、memcache
分布式锁的原理
zookeeper
去创建相应的节点,创建成功,则表示获取到相应的锁,创建失败,则表示获取锁失败,释放锁的时候删除节点。
Redis、memcache
对应的去设置一个值做为锁的一标志,每次获取锁的时候,判断对应的值是否存在,存在则无法获取,不存在,则设置相应的值,表示获取到锁。(Redis 使用 setnx,memcache 使用 add)
Zookeeper 实现分布式锁的多种方式
创建节点的时候,一定要创建临时节点,避免应用获取到锁后,宕机,导致锁一致被持有。
- 程序连接上 ZK 之后,尝试去创建节点,如果创建成功则成果获取锁,如果节点创建失败,程序进行短暂的休眠,之后重试。缺点:不断重试,会消耗资源。
- 使用 ZK 的 watcher 机制。缺点:引发同一时刻同时唤醒大批等待程序,引起羊群效应。
- 创建有序的节点。目标节点:/a0 排队进程阻塞在有序节点 /a1 /a2 /an,这样/a0 释放锁之后,每次只需要唤醒一个等待的进程即可,例如:唤醒/a1
基于 Zookeeper 原生的 API 实现分布式锁
- 原生 API 不支持递归创建节点
- 如果是单一应用,尽量不要使用分布式锁
引入 Maven 依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
</dependency>
基于 zk 实现分布式锁
import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import static org.apache.zookeeper.CreateMode.EPHEMERAL;
import static org.apache.zookeeper.ZooDefs.Ids.OPEN_ACL_UNSAFE;
/**
* 基于zk实现分布式锁
*/
public class ZkLock {
private ZooKeeper zooKeeper;
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private ZkLock() {
try {
zooKeeper = new ZooKeeper("192.1.31.220:2181,192.1.31.240:2181,192.1.31.250:2181", 5000, new ZkWatcher());
System.out.println(zooKeeper.getState());
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("与zk建立连接=====>"+zooKeeper.getState());
} catch (IOException e) {
e.printStackTrace();
}
}
public static ZkLock getInstance() {
return Singleton.getInstance();
}
private class ZkWatcher implements Watcher {
@Override
public void process(WatchedEvent event) {
System.out.println("接收到监听事件=====》"+event);
if (Event.KeeperState.SyncConnected == event.getState()) {
countDownLatch.countDown();
}
}
}
public void lock(Integer id) {
String path = "/soulboy-product-lock-" + id;
//创建临时节点,如果创建成功的话,就表示获取锁,如果失败,则不断尝试
try {
zooKeeper.create(path,"".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("成功获取锁");
} catch (Exception e) {
while (true) {
try {
Thread.sleep(500L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
zooKeeper.create(path,"".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (Exception e1) {
continue;
}
break;
}
}
}
/**
* 释放锁,直接删除zk节点
* @param id
*/
public void unLock(Integer id) {
String path = "/soulboy-product-lock-" + id;
try {
zooKeeper.delete(path,-1);
System.out.println("成功释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
private static class Singleton {
private static ZkLock instance;
static {
instance = new ZkLock();
}
private static ZkLock getInstance() {
return instance;
}
}
}
测试代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程安全的操作示例
*/
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
private static ZkLock lock = ZkLock.getInstance();
/**y
* 每次调用对num进行++操作
*/
public static void inCreate() {
lock.lock(1);
Thread th=Thread.currentThread();
System.out.println("当前num数值:" + num + ", Tread name:"+th.getName());
num++;
lock.unLock(1);
}
public static void test() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch
countDownLatch.countDown();
}).start();
}
while (true) {
if (countDownLatch.getCount() == 0) {
System.out.println(num);
break;
}
}
}
}
控制台输出
CONNECTING
接收到监听事件=====》WatchedEvent state:SyncConnected type:None path:null
与zk建立连接=====>CONNECTED
成功获取锁
当前num数值:0, Tread name:Thread-3
成功释放锁
成功获取锁
当前num数值:1, Tread name:Thread-3
成功释放锁
成功获取锁
当前num数值:2, Tread name:Thread-3
成功释放锁
...
成功获取锁
当前num数值:996, Tread name:Thread-7
成功释放锁
成功获取锁
当前num数值:997, Tread name:Thread-7
成功释放锁
成功获取锁
当前num数值:998, Tread name:Thread-7
成功释放锁
成功获取锁
当前num数值:999, Tread name:Thread-7
成功释放锁
1000
使用 Zookeeper 作为注册中心的问题
核心思想
- 在实践中,注册中心不能因为自身的任何原因破坏服务之间本身的可连通性
- 注册中心需要AP,而 Zookeeper 是 CP
使用 Zookeeper 作为服务注册中心
启动Docker本地容器
//获取镜像
docker pull zookeeper:3.5
//运行 Zookeeper 镜像
docker run --name zookeeper -p 2181:2181 -d zookeeper:3.5
Provider 端几乎和使用 Euraka 没有区别
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
bootstrap.properties
spring.application.name=waiter-service
application.properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
info.app.author=DigitalSonic
info.app.encoding=@project.build.sourceEncoding@
server.port=0
spring.cloud.zookeeper.connect-string=localhost:2181
Customer 端几乎和使用 Euraka 没有区别
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
bootstrap.properties
spring.application.name=customer-service
application.properties
server.port=0
spring.cloud.zookeeper.connect-string=localhost:2181