[TOC]
zookeeper 动物管理员全局把控。提供了配置管理、服务发现等服务。其自身也是能够集群化的。实现上是基于观察者模式。不想 eureka/consul 等同类产品须要心跳机制。他自身反对察看与被动触发机制; 千里之行始于足下,咱们曾经摸索了 eureka、consul 两个服务注册的中间件了。明天咱们持续学习另外一个作为服务注册的服务。
本文将从 zookeeper 单机到集群的装置解说;在从集群 leader 选举机制的解说及数据同步的梳理。到最终的基于 zookeeper 实现的配置管理及分布式锁的利用。从点到面在到利用带你领会一把过山车
简介
- Zookeeper 大家都晓得是动物管理员的意思。在大数据全家桶中他的作用也是治理。上面咱们别离从装置到应用来看看 zk 的柔美
中心化
服务 | 特点 | 中心化 | CAP |
---|---|---|---|
eureka | peer to peer 每个 eureka 服务默认都会向集群中其余 server 注册及拉去信息 | 去中心化 | AP |
consul | 通过其中节点病毒式蔓延至整个集群 | 多中心化 | CP |
zookeeper | 一个 leader 多个 followes | 中心化 | CP |
事务
- 下面提到 zookeeper 是一个 leader 多个 followers。
- 除了中心化思维外,zookeeper 还有个重要的个性就是事务。zookeeper 数据操作是具备原子性的。
- 如何了解 zookeeper 的事务呢,其实外部是通过版本治理数据实现事务性的。zookeeper 每个客户端初始化时都会初始化一个 operation。每个 client 连贯是都有个外部的 session 治理。同一个 session 操作都会有对应的版本记录,zxid 这样能保证数据的一个一致性。
downlaod
zookeeper3.5.9
单机装置
- 在下面的地址下载后进行解压
tar -zxvf apache-zookeeper-3.5.9-bin.tar.gz
- 官网提供的相当于是个模板,这个时候间接启动会报错的。
- 依据报错信息咱们晓得缺失 zoo.cfg 默认配置文件。在 conf 目录下官网给咱们提供了 zoo_sample.cfg 模板配置文件。咱们只须要复制改文件为 zoo.cfg 在此基础上进行修该
cp zoo_sample.cfg zoo.cfg
- 咱们批改下 data 门路就能够启动了。
- 启动之后通过 ’zkServer.sh status’ 查看 zookeeper 运行状态。咱们能够看到此时是单机模式启动的。
- 通过 jps 咱们也可能看到 zookeeper 启动胜利了。
集群搭建
- 这里的集群为了不便就演示伪集群版。即在一台服务器上安排三台 zk 服务。
mkdir {zk1,zk2,zk3}
, 首先创立 zk1,zk2,zk3 三个文件夹寄存 zk- 将之前解压好的 zk 文件夹别离复制到三个创立 zk 中
- 在 zookeeper 进群中,每台服务都须要有一个编号。咱们还须要写入每台机器的编号
- 而后反复咱们单机版的过程。在根目录创立 data 文件夹,而后批改 conf 中对应的 data 配置。只不过这里还须要咱们批改一下端口号。因为在同一台机器上所以须要不同的端口号才行。
- 最初新增集群外部通信端口
server.1=192.168.44.130:28881:38881
server.2=192.168.44.130:28882:38882
server.3=192.168.44.130:28883:38883
- 上述的端口只有保障可用就行了。
- 下面是 zk1 的配置,其余读者自行配置。
- 在配置总咱们多了 server 的配置。这个 server 是集群配置的重点
server.1=192.168.44.139:28881:38881
- server 是固定写法
- .1 : 1 就是咱们之前每台 zk 服务写入的 myid 里的数字
- 192.168.44.130:示意咱们 zk 所在服务 ip
- 28881: 在 zookeeper 集群中 leader 和 follewers 数据备份通信端口
- 38881:选举机制的端口
- 下面我是通过 zkServer 启动不同配置文件。你们也能够在不同的 zk 包下别离启动。最终成果是一样的。启动实现之后咱们通过 jps 查看能够看到多了三个 zk 服务。在单机版的时候 jps 咱们晓得 zk 的主启动是 QuorumPeerMain。
- 上图是其中一个 zk 服务的目录构造。咱们能够看到 data 目录有数据产生了。这是 zk 集群启动之后生成的文件。
- 集群启动实现了。然而咱们当初对 zk 集群如同还是没有太大的感知。比如说咱们不晓得谁是 leader。能够通过如下命令查看每台 zk 的角色
- 咱们的 zk2 是 leader 角色。其余是 follower
Cli 连贯
- 在 zookeeper 包中还有一个 zkCli.sh 这个是 zk 的客户端。通过他咱们能够连贯 zookeeper 服务并进行操作。
zkCli.sh -server 192.168.44.131:1181
能够连贯 zk 服务
- 上面咱们测试下对 zk 的操作会不会是集群化的。
- 咱们 zkCli 连贯了 zk1 服务 1181,并且创立的一个 zk 节点名为 node。咱们在去 zk2 服务上同样能够看到这个 node 节点。
- 至此,咱们 zookeeper 集群搭建实现,并且测试也曾经通过了。
集群容错
Master 选举
- 咱们已上述集群启动是为例,简述下集群选举流程。
①、zk1 启动时,这个时候集群中只有一台服务就是 zk1。此时 zk1 给集群投票天然被 zk1 本人获取。此时 zk1 有一票
②、zk2 启动时,zk1,zk2 都会都一票给集群。因为进群中 zk1(myid) 小于 zk2(myid),所以这两票被 zk2 获取。这里为什么会是 zk2 获取到呢。zookeeper 节点都一份坐标 zk=(myid,zxid);myid 是每个 zk 服务配置的惟一项。zxid 是 zk 服务的一个 64 位内容。高 32 没 master 选举一次递增一次并同时清空低 32 位。低 32 位是每产生一次数据事务递增一次。所以 zxid 最高阐明此 zk 服务数据越新。
③、zk2 取得两票后,此时曾经取得了集群半数以上的票数,多数遵从少数此时 zk2 曾经是准 leader 了同时 zk1 切换为 following。此时 zk1 曾经是 zk2 的跟班了
④、zk3 启动时,按情理 zk3 应该会收到三票。然而因为 zk1 曾经站队到 zk2 了。zk2 作为准 leader 是不可能给 zk3 投票的。所以 zk3 最多只有本人一票,zk3 明知 zk2 取得半数以上,曾经是民心所归了。所以 zk3 为了本人的前途也就将本人的一票投给了 zk2.
- zookeeper 的投票选举机制赤裸裸的就是一个官场。充斥的人心
leader 宕机从新选举
- 其实在启动阶段 zk2 获取到两张投票是有一个 PK 的逻辑在外面的。上述启动阶段的投票是集体的一个抽象化了解。
- 在下面说 myid 高的不会给 myid 低的投票实际上是一种全面的了解。实际上是会进行投票的,投票之后会进行两张票 PK,将权重高的一张票投出去选举 leader。有集群管理者进行统计投票并计数。
- 上面咱们来看看从新选举是的逻辑。也是真正的 leader 选举的逻辑。
①、zk2 服务挂了,这个时候 zk1,zk3 立马切换为 looking 状态,并别离对集群内其余服务进行投票
②、zk1 收到本人的和其余服务投过来的票(1,0)、(3,0)。zk1 会斟酌这两张票,基于咱们提到的算法 num=10*zxid+myid,所以 zk1 会将(3,0) 这张票投入计数箱中
③、zk3 收到两种票(3,0)、(1,0),同样会将(3,0) 投入计数箱
④、最终统计 zk3 取得两票胜出。
- 下面的选举才是真正的选举。启动期间咱们只是退出了咱们本人的了解在外面。选举完之后服务会切换成 leader、follower 状态进行工作
数据同步
- 同样先上图
- client 如果间接将数据变更申请发送到 leader 端,则间接从图中第三步开始发送 proposal 申请期待过半机制后再发送 commit proposal。follower 则会开始更新本地 zxid 并同步数据。
- 如果 client 发送的是 follower,则须要 follower 先将申请转发至 leader 而后在反复下面的步骤。
- 在数据同步期间为了保障数据强一致性。leader 发送的 proposal 都是有序的。follower 执行的数据变更也都是有程序的。这样能保证数据最终一致性。
在一段时间内比如说须要对变量 a = 5 和 1 = 1 操作。如果 a = 5 和 a = 1 是两个事物。如果 leader 告诉 follower 进行同步,zk1 先 a = 1 在 a =5。则 zk1 中的 a 为 5.zk3 反之来则 zk3 中 a =1; 这样就会造成数不统一。然而 zookeeper 通过 znode 节点有序排列保障了 follower 数据生产也是有序的。在莫一时刻 zk1 执行了 a =5,这时候 client 查了 zk1 的 a 是 5, 尽管外表上是脏数据实际上是 zk1 未执行完。期待 zk1 执行完 a =1. 这就叫数据 <red> 最终一致性 </red>。 - 上面一张图可能更加的形象,来自于网络图片
特色性能
服务治理
- 在咱们 eureka、consul 章节曾经介绍了 springcloud 注册的细节了。明天咱们还是同样的操作。已 payment 和 order 模块来讲服务注册到 zookeeper 上。看看成果。
<!-- SpringBoot 整合 zookeeper 客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
- 配置文件配置增加 zookeeper
spring:
cloud:
zookeeper:
connect-string: 192.168.44.131:2181
- 和 consul 一样, 这里的注册会将 spring.application.name 值注册过来。eureka 是将 spring.application.name 的大写名称注册过来。这个影响的就是 order 订单中调用的地址区别。
点我看源码
- 还是一样的操作。启动 order、两个 payment 之后咱们调用
http://localhost/order/getpayment/123
能够看到后果是负载平衡了。
配置管理
- 相熟 springcloud 的都晓得,springcloud 是有一个配置核心的。外面次要借助 git 实现配置的实时更新。具体细节咱们前面章节会缓缓开展,本次咱们展现通过 zookeeper 实现配置核心治理。
- 首先咱们在咱们的 payment 模块持续开发,引入 zookeeper-config 模块
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-config</artifactId>
</dependency>
- 而后咱们须要有个储备常识,在 spring 加载配置文件的程序,会先加载 bootstrap 文件而后是 application 文件。这里 bootstrap 咱们也用 yml 格式文件。
spring:
application:
name: cloud-payment-service
profiles:
active: dev
cloud:
zookeeper:
connect-string: 192.168.44.131:2181
config:
enabled: true
root: config
profileSeparator: ','
discovery:
enabled: true
enabled: true
- 下面这部分须要解释下
key | 解释 |
---|---|
spring.application.name | 服务名 |
spring.profiles | 环境名 |
spring.cloud.enabled | 用于激活 config 主动配置 |
spring.cloud.zookeeper.config.root | zookeeper 根门路 |
spring.cloud.zookeeper.profilSeparator | key 分隔符 |
@Component
@ConfigurationProperties(prefix = "spring.datasources")
@Data
@RefreshScope
public class Db {private String url;}
- 零碎中会读取配置文件中的 spring.datasources.url 这个属性值。联合咱们的 bootstrap.yml 文件中。此时会去 zookeeper 零碎中查找 /config/cloud-payment-service,dev/spring.datasources.url 这个值。
- 对于那个 zookeeper 的 key 是如何来的。仔细察看下能够发现他的法则 /${spring.cloud.zookeeper.root}/${spring.application.name}${spring.cloud.zookeeper.profileSeparator}${spring.profiles}/${理论的 key}
- 惟一留神的是须要在类上增加
@RefreshScope
, 这个注解是 cloud 提供的。包含到前面的 config 配置核心都离不开这个注解 - 此时通过前文提到的 zkCli 连贯 zk 服务,而后创立对应节点就能够了。zookeeper 服务须要逐层创立。比方下面提到的
/config/cloud-payment-services/spring.datasources.url
, 咱们须要create /config
而后create /config/cloud-payment-services
在创立最初的内容。 - 咱们能够提前在 zk 中创立好内容。而后
localhost:8001/payment/getUrl
获取内容。而后通过set /config/cloud-payment-services/spring.datasources.url helloworld
在刷新接口就能够看到最新的 helloworld 了。这里不做演示。 - zookeeper 原生的创立命令因为须要一层一层创立这还是很麻烦。还有咱们有 zkui 这个插件。这个提供了 zookeeper 的可视化操作。还反对咱们文件导入。上述的配置内容咱们只须要导入以下内容的文件即可
/config/cloud-payment-services=spring.datasources.url=hello
zkui 装置应用
- 下面咱们提到了一个工具
zkui
,顾名思义他是 zookeeper 可视化工具。咱们间接下载 github 源码。
- 官网装置步骤也很简略,因为他就是一个 jar 服务。
- pom 同级执行 maven clean install 打包 jar 而后
nohup java -jar zkui-2.0-SNAPSHOT-jar-with-dependencies.jar &
后盾启动就行了。默认端口 9090 - 默认用户名明码 官网都给了。admin:manager
- 在 jar 包同级官网提供了一份 zookeeper 配置模板。在外面咱们能够配置咱们的 zookeeper。如果是集群就配置多个就行了。
- 对于这个 zkui 的应用这里不多介绍,就是一个可视化。程序员必备技能应该都会应用的。
- 咱们我的项目里应用的都是单机的 zookeeper, 然而在下面装置的时候咱们也有集群 zookeeper。端口别离是 1181,1182,1183. 咱们在 zkui 的配置文件 config.cfg 中配置集群即可。
分布式锁
分布式锁罕用在共享资源的获取上。在分布式系统的中咱们须要协调每个服务的调度。如果不进行管制的话很大水平会造成资源的节约甚至是资源溢出。常见的就是咱们的库存。
- 之前咱们 springcloud 中有一个 order 和两个 payment 服务。payment 次要用来做领取操作。如果订单胜利之后须要调用 payment 进行扣款。这时候金额相当于资源。这种资源对 payment 两个服务来说是互斥操作。两个 payment 过去操作金额时必须先后顺序执行。
mysql 隔离管制
- 在以前分布式还不是很遍及的时候咱们失常解决这些操作时都是完结数据库的事务隔离级别。
隔离级别 | 隔离级别 | 景象 |
---|---|---|
read uncommit | 读未提交 | 产生脏读 |
read commited | 读已提交 | 幻读(insert、delete)、不可反复读(update) |
repeatable read | 可反复度 | 幻读 |
serializable | 串行化 | 无问题、效率变慢 |
- 基于 mysql 隔离级别咱们能够设置数据库为串行模式。然而带来的问题是效率慢,所有的 sql 执行都会串行。
mysql 锁
- 隔离级别尽管能够满足然而带来的问题的确不可承受。上面就会衍生出 mysql 锁。咱们能够独自建一张表有数据代表上述胜利。否则上锁失败。这样咱们在操作金额扣减时先判断下这张表有没有数据进行上锁。然而如果上锁之后会造成死锁景象。因为程序异样迟迟没有开释锁就会造成程序瘫痪。
- 这个时候咱们能够定时工作革除锁。这样至多保障其余线程可用。
redis 锁
- 因为 redis 自身有生效属性,咱们不用放心死锁问题。且 redis 是内存操作速度比 mysql 快很多。对于 redis 锁的实现能够参考我的其余文章 redis 分布式锁.
zookeeper 锁
- 因为 zookeeper 基于观察者模式,咱们上锁失败后能够监听对应的值直到他生效时咱们在进行咱们的操作这样可能保障咱们有序解决业务,从而实现锁的性能。
- 上图是两个线程上锁的繁难图示。在 zookeeper 中实现分布式锁次要依赖
CuratorFramework
、InterProcessMutex
两个类
CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.44.131:2181", new ExponentialBackoffRetry(1000, 3));
client.start();
InterProcessMutex mutex = new InterProcessMutex(client, "/config/test");
long s = System.currentTimeMillis();
boolean acquire = mutex.acquire(100, TimeUnit.SECONDS);
if (acquire) {long e = System.currentTimeMillis();
System.out.println(e - s+"@@@ms");
System.out.println("lock success....");
}
- 最终 InterProcessMutex 实现加锁。加锁会在指定的 key 上增加一个新的 key 且带有编号。此时线程中的编号和获取的 /config/test 下汇合编号最小值雷同的话则上锁胜利。T2 则上锁失败,此时会想前一个序号的 key 增加监听。即当 00001 生效时则 00002 对应的 T2 就会获取到锁。这样能够保障队列的有序进行。
本文由博客一文多发平台 OpenWrite 公布!