Zookeeper
一、概述
Zookeeper是一个开源的分布式的,为分布式框架提供协调服务的Apache项目。
从设计模式角度来理解,Zookeeper是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,数据发生变化,Zookeeper负责通知已经注册的观察者做出相应反应。
Zookeeper=文件系统+通知机制
二、特点
- Zookeeper是由一个leader,多个follower组成的集群。
- 集群中只要有半数以上的节点存活,Zookeeper集群就能正常提供服务。所以Zookeeper适合安装奇数台服务器。
- 全局数据一致。每个server保存一份数据副本,client无论连接哪个Server数据都是一致的。
- 更新请求顺序执行,来自同一个client的更新请求按其发送的顺序依次执行。
- 数据更新原子性。一次更新,要么全部成功要么全部失败。
- 实时性,在一定时间范围内,client能读到最新数据。
三、数据结构
Zookeeper是树形结构的文件系统。与Unix文件系统类似,整体可以看做是一棵树,每个节点称作一个ZNode。每个ZNode默认最大存储1MB的数据。每个ZNode都可以用唯一路径标识。
不能存储大量数据。
四、应用场景
1.统一命名服务
分布式环境下,经常需要对应用,服务统一命名便于识别。
Nginx也能实现将IP封装成域名的功能。
2.统一配置管理
分布式环境,一般要求一个集群中,所有节点的配置信息是一致的,如Kafka集群。
配置文件修改后,希望能够快速同步到各个节点上。
Zookeeper实现配置管理:
- 将配置信息写入Zookeeper上的一个ZNode。
- 各个服务端服务器监听这个ZNode。
- 一旦ZNode中的数据被修改,Zookeeper将通知各个客户端服务器。
3.统一集群管理
分布式环境中要求掌握每个节点的实时状态,并可根据状态做出一些调整。
Zookeeper实现实时监控节点变化:
- 可将节点信息写入Zookeeper上的一个ZNode。
- 监听这个ZNode可以获取它的实时状态变化。
4.服务器节点上下线
- 服务器启动注册服务信息,在Zookeeper上创建临时节点。
- 客户端获取在线服务列表,并监听列表中的服务信息。
- 服务器节点下线。
- 服务器节点下线事件通知。
- 客户端重新获取服务器列表。
5.软负载均衡
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
五、配置参数解读
Zookeeper中的配置文件zoo.cfg参数含义:
- tickTime=2000:通信心跳时间,单位毫秒。
- initLimit=10:LF初始通信时限。Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)。
- syncLimit=5:LF同步通信时限。Leader和Follower之间的通信时间如果超过syncTime*tickTime,Leader认为Follower死掉,从服务器列表删除follower。
- dataDir:保存zookeeper中的数据。默认的tmp目录是Linux系统的临时目录,会被系统定期删除,所以一般不用默认的tmp目录。
- clientPoint=2181:客户端连接端口,通常不做修改。
六、选举机制
1.第一次启动选举机制
(1)服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(
3票),选举无法完成,服务器1状态保持为
LOOKING;
(2)服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING
(3)服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
(4)服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;
(5)服务器5启动,同4一样当小弟
关键的三个ID:
SID:服务器ID。用来唯一标识一台
ZooKeeper集群中的机器,每台机器不能重
复,和myid一致。
ZXID:事务ID。ZXID是一个事务ID,用来
标识一次服务器状态的变更。在某一时刻,
集群中的每台机器的ZXID值不一定完全一
致,这和ZooKeeper服务器对于客户端“更
新请求”的处理逻辑有关。
Epoch:每个Leader任期的代号。没有
Leader时同一轮投票过程中的逻辑时钟值是
相同的。每投完一次票这个数据就会增加
2.非第一次启动选举机制
( 1)当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举: • 服务器初始化启动。 • 服务器运行期间无法和Leader保持连接。 ( 2)而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态: • 集群中本来就已经存在一个Leader。 对于第一种已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连 接,并进行状态同步即可。 • 集群中确实不存在Leader。 假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻, 3和5服务器出现故障,因此开始进行Leader选举 SID为1、2、4的机器投票情况:
(EPOCH,ZXID,SID ) ( 1,8,1) ( 1,8,2) ( 1,7,4)
选举Leader规则:
①EPOCH大的直接胜出
②EPOCH相同,事务id大的胜出
③事务id相同,服务器id大的胜出
七、启动停止脚本
服务端的启动与停止
开启命令:bin/zkServer.sh start [config]
停止命令:bin/zkServer.sh stop
脚本:
#!bin/bash
case $1 in
"start") {
for i in hadoop102 hadoop103 hadoop104
do
ssh $1 "/opt/module/zookeeper-3.5.7/bin/zkServer start"
done
}
;;
"stop") {
for i in hadoop102 hadoop103 hadoop104
do
ssh $1 "/opt/module/zookeeper-3.5.7/bin/zkServer stop"
done
}
;;
"status"){
for i in hadoop102 hadoop103 hadoop104
do
ssh $1 "/opt/module/zookeeper-3.5.7/bin/zkServer status"
done
}
;;
esac
客户端的命令行操作
指定服务端启动:bin/zkCli.sh -server hadoop102:2181
创建节点命令 create
create /sanguo/simayi "simayi"
删除节点 delete
delete /sanguo
如果sanguo节点下有子节点则无法删除
删除路径上多个节点 deleteall
deleteall /sanguo
删除sanguo节点及其子节点。
监听节点 get -w /路径
查看节点 ls
查看根目录节点:ls /
查看指定路径的节点:ls /sanguo
退出 quit
修改 set /路径 "名称"
八、节点
1.节点信息
- czxid:创建节点的事务ID。(每次修改Zookeeper状态都会产生一个Zookeeper事务ID。事务ID(zxid)是zookeeper中所有修改总的次序。事务ID(zxid)有大小,且全局唯一。)
- ctime:znode被创建的毫秒数(从1970开始计算)。
- mzxid:zNode最后更新的事务zxid。
- mtime:zNode最后修改的毫秒数(从1970开始算起)。
- pZxid:zNode最后更新的子节点的zxid。
- ccversion:zNode子节点的变化号,zNode子节点修改次数。
- dataversion:znode数据变化号
- aclversion:zNode访问控制列表的变化号。
- ephemeralOwner:如果是临时节点,这个是zNode拥有者的sessionId。如果不是临时节点则是0。
- dataLength:zNode的数据长度。
- numChildren:zNode的子节点数量。
2.节点类型
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
( 1)持久化目录节点 客户端与Zookeeper断开连接后,该节点依旧存在。例:create /sanguo/wuguo/limeng "limeng" ( 2)持久化顺序编号目录节点 客户端与Zookeeper断开连接后,该节点依旧存 在,只是Zookeeper给该节点名称进行顺序编号。例:create -s /sanguo/wuguo/lusu "lusu" ( 3)临时目录节点 客户端与Zookeeper断开连接后,该节点被删除。例:create -e /sanguo/weiguo/xunyu "xunyu" ( 4)临时顺序编号目录节点 客户端与 Zookeeper 断开连接后 , 该 节 点 被 删 除 , 只 是 Zookeeper给该节点名称进行顺序编号。例:create -e -s /sanguo/weiguo/caocao "caocao"
说明:创建znode时设置顺序标识,znode名称
后会附加一个值,顺序号是一个单调递增的计数
器,由父节点维护
注意:在分布式系统中,顺序号可以被用于
为所有的事件进行全局排序,这样客户端可以通
过顺序号推断事件的顺序
3.监听器及节点删除
1.监听原理详解
- 首先要有一个main()线程
- 在main线程中创建Zookeeper客户端,这时就会创建两个线 程,一个负责网络连接通信(connet),一个负责监听(listener)。
- 通过connect线程将注册的监听事件发送给Zookeeper。
- 在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
- Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
- listener线程内部调用了process()方法。
2.常见监听
- 监听节点数据的变化 get path [watch]
- 注册监听,监听sanguo节点的数据变化:get -w /sanguo
- 注意:注册一次监听只能监听一次数据变化。想再次监听需要重新注册。
- 监听子节点增减的变化 ls path [watch]
- 注册监听,监听sanguo节点下的节点变化:ls -w /sanguo
- 注意:创建一次,监听一次。同上。
4 .IDEA 环境搭建
1)创建一个工程:zookeeper
2)添加pom文件
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</version>
</dependency>
</dependencies>
3)拷贝log4j.properties文件到项目根目录
需要在项目的 src/main/resources 目录下,新建一个文件,命名为“log4j.properties”,在
文件中填入。
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
4)创建包名com.atguigu.zk
5)创建类名称zkClient
5. 创建 ZooKeeper 客户端
// 注意:逗号前后不能有空格
private static String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private static int sessionTimeout = 2000;
private ZooKeeper zkClient = null;
@Before
public void init() throws Exception {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 收到事件通知后的回调函数(用户的业务逻辑)
System.out.println(watchedEvent.getType() + "--" + watchedEvent.getPath());
// 再次启动监听
try {
List<String> children = zkClient.getChildren("/", true);
for (String child : children) {
System.out.println(child);
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
6.创建节点
// 创建子节点
@Test
public void create() throws Exception {
// 参数 1:要创建的节点的路径; 参数 2:节点数据 ; 参数 3:节点权限 ;参数 4:节点的类型
String nodeCreated = zkClient.create("/atguigu", "shuaige".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
测试:在 hadoop102 的 zk 客户端上查看创建节点情况
[zk: localhost:2181(CONNECTED) 16] get -s /atguigu shuaige
7.获取子节点并监听节点变化
// 获取子节点
@Test
public void getChildren() throws Exception {
List<String> children = zkClient.getChildren("/", true);
for (String child : children) {
System.out.println(child);
}
// 延时阻塞
Thread.sleep(Long.MAX_VALUE);
}
(1)在 IDEA 控制台上看到如下节点:
zookeeper
sanguo
atguigu
(2)在 hadoop102 的客户端上创建再创建一个节点/atguigu1,观察 IDEA 控制台
[zk: localhost:2181(CONNECTED) 3] create /atguigu1 "atguigu1"
(3)在 hadoop102 的客户端上删除节点/atguigu1,观察 IDEA 控制台
[zk: localhost:2181(CONNECTED) 4] delete /atguigu1
8.判断节点是否存在
// 判断 znode 是否存在
@Test
public void exist() throws Exception {
Stat stat = zkClient.exists("/atguigu", false);
System.out.println(stat == null ? "not exist" : "exist");
}
9.写数据原理
-
写请求直接发送给leader节点
- 客户端将写请求发送leader节点,leader自己将数据写入
- leader节点通知逐步通知follower节点写数据
- 收到半数以上的follower节点回复已经写成功了,leader节点回复客户端写完成了
- leader节点继续通知其余节点写操作,并等待其回复
- 所有节点回复leader节点后,写操作完成
-
写请求发送给follower节点
- 客户端将写请求发送给follower节点,follower节点将写请求转发给leader节点
- leader节点收到follower节点的写请求,将数据写入
- leader节点完成自己写操作后,通知follower节点写数据
- 收到半数以上的follower节点回复已经写成功了,leader节点回复发出写请求的follower节点数据已经写完了
- 收到客户端写请求的follower节点回复客户端写操作已完成。
- leader节点继续通知其余节点写操作,并等待其回复
- 所有节点回复leader节点后,写操作完成
九、服务器动态上下线
1.需求分析
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知
到主节点服务器的上下线
2.服务器注册
(1)先在集群上创建/servers 节点
[zk: localhost:2181(CONNECTED) 10] create /servers "servers"
Created /servers
(2)在 Idea 中创建包名:com.atguigu.zkcase1
(3)服务器端向 Zookeeper 注册代码
import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
public class DistributeServer {
private static String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private static int sessionTimeout = 2000;
private ZooKeeper zk = null;
private String parentNode = "/servers";
// 创建到 zk 的客户端连接
public void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
}
});
}
// 注册服务器
public void registServer(String hostname) throws Exception {
String create = zk.create(parentNode + "/server",
hostname.getBytes(),
Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname + " is online " + create);
}
// 业务功能
public void business(String hostname) throws Exception {
System.out.println(hostname + " is working ...");
Thread.sleep(Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
// 1 获取 zk 连接
DistributeServer server = new DistributeServer();
server.getConnect();
// 2 利用 zk 连接注册服务器信息
server.registServer(args[0]);
// 3 启动业务功能
server.business(args[0]);
}
}
3.客户端监听
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
public class DistributeClient {
private static String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private static int sessionTimeout = 2000;
private ZooKeeper zk = null;
private String parentNode = "/servers";
// 创建到 zk 的客户端连接
public void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 再次启动监听
try {
getServerList();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
// 获取服务器列表信息
public void getServerList() throws Exception {
// 1 获取服务器子节点信息,并且对父节点进行监听
List<String> children = zk.getChildren(parentNode, true);
// 2 存储服务器信息列表
ArrayList<String> servers = new ArrayList<>();
// 3 遍历所有节点,获取节点中的主机名称信息
for (String child : children) {
byte[] data = zk.getData(parentNode + "/" + child, false, null);
servers.add(new String(data));
}
// 4 打印服务器列表信息
System.out.println(servers);
}
// 业务功能
public void business() throws Exception {
System.out.println("client is working ...");
Thread.sleep(Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
// 1 获取 zk 连接
DistributeClient client = new DistributeClient();
client.getConnect();
// 2 获取 servers 的子节点信息,从中获取服务器信息列表
client.getServerList();
// 3 业务进程启动
client.business();
}
}
4.测试
1)在 Linux 命令行上操作增加减少服务器
(1)启动 DistributeClient 客户端
(2)在 hadoop102 上 zk 的客户端/servers 目录上创建临时带序号节点
[zk:localhost:2181(CONNECTED)1]create -e -s /servers/hadoop102 "hadoop102"
[zk:localhost:2181(CONNECTED)2]create -e -s /servers/hadoop103 "hadoop103"
(3)观察 Idea 控制台变化
[hadoop102, hadoop103]
(4)执行删除操作
[zk:localhost:2181(CONNECTED)8] delete /servers/hadoop1020000000000
(5)观察 Idea 控制台变化
[hadoop103]
2)在 Idea 上操作增加减少服务器
(1)启动 DistributeClient 客户端(如果已经启动过,不需要重启)
(2)启动 DistributeServer 服务
-
点击 Edit Configurations…
-
在弹出的窗口中(Program arguments)输入想启动的主机,例如,hadoop102
-
回 到 DistributeServer 的 main 方 法 , 右 键 , 在 弹 出 的 窗 口 中 点 击 Run “DistributeServer.main()”
-
观察 DistributeServer 控制台,提示 hadoop102 is working
-
观察 DistributeClient 控制台,提示 hadoop102 已经上线
十、分布式锁
什么叫做分布式锁呢?
比如说"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。
1.需求分析
1)接收到请求后,在/locks节点下创建一个临时顺序节点
2)判断自己是不是当前节点下最小的节点:是,获取到锁;不是,对前一个节点进行监听
3)获取到锁,处理完业务后,delete节点释放锁,然后下面的节点将收到通知,重复第二步判断
2.代码实现
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class DistributedLock {
// zookeeper server 列表
private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
// 超时时间
private int sessionTimeout = 2000;
private ZooKeeper zk;
private String rootNode = "locks";
private String subNode = "seq-";
// 当前 client 等待的子节点
private String waitPath;
//ZooKeeper 连接
private CountDownLatch connectLatch = new CountDownLatch(1);
//ZooKeeper 节点等待
private CountDownLatch waitLatch = new CountDownLatch(1);
// 当前 client 创建的子节点
private String currentNode;
// 和 zk 服务建立连接,并创建根节点
public DistributedLock() throws IOException, InterruptedException, KeeperException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
if (event.getState() == Event.KeeperState.SyncConnected) {
connectLatch.countDown();
}
// 发生了 waitPath 的删除事件
if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
waitLatch.countDown();
}
}
});
// 等待连接建立
connectLatch.await();
//获取根节点状态
Stat stat = zk.exists("/" + rootNode, false);
//如果根节点不存在,则创建根节点,根节点类型为永久节点
if (stat == null) {
System.out.println("根节点不存在");
zk.create("/" + rootNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
// 加锁方法
public void zkLock() {
try {
//在根节点下创建临时顺序节点,返回值为创建的节点路径
currentNode = zk.create("/" + rootNode + "/" + subNode,
null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// wait 一小会, 让结果更清晰一些
Thread.sleep(10);
// 注意, 没有必要监听"/locks"的子节点的变化情况
List<String> childrenNodes = zk.getChildren("/" + rootNode, false);
// 列表中只有一个子节点, 那肯定就是 currentNode , 说明 client 获得锁
if (childrenNodes.size() == 1) {
return;
} else {
//对根节点下的所有临时顺序节点进行从小到大排序
Collections.sort(childrenNodes);
//当前节点名称
String thisNode = currentNode.substring(("/" +
rootNode + "/").length());
//获取当前节点的位置
int index = childrenNodes.indexOf(thisNode);
if (index == -1) {
System.out.println("数据异常");
} else if (index == 0) {
// index == 0, 说明 thisNode 在列表中最小, 当前 client 获得锁
return;
} else {
// 获得排名比 currentNode 前 1 位的节点
this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
// 在 waitPath 上注册监听器, 当 waitPath 被删除时, zookeeper 会回调监听器的 process 方法
zk.getData(waitPath, true, new Stat());
//进入等待锁状态
waitLatch.await();
return;
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 解锁方法
public void zkUnlock() {
try {
zk.delete(this.currentNode, -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}
3.测试
(1)创建两个线程
import org.apache.zookeeper.KeeperException;
import java.io.IOException;
public class DistributedLockTest {
public static void main(String[] args) throws InterruptedException,
IOException, KeeperException {
// 创建分布式锁 1
final DistributedLock lock1 = new DistributedLock();
// 创建分布式锁 2
final DistributedLock lock2 = new DistributedLock();
new Thread(new Runnable() {
@Override
public void run() {
// 获取锁对象
try {
lock1.zkLock();
System.out.println("线程 1 获取锁");
Thread.sleep(5 * 1000);
lock1.zkUnlock();
System.out.println("线程 1 释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// 获取锁对象
try {
lock2.zkLock();
System.out.println("线程 2 获取锁");
Thread.sleep(5 * 1000);
lock2.zkUnlock();
System.out.println("线程 2 释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
(2)观察控制台变化:
线程 1 获取锁
线程 1 释放锁
线程 2 获取锁
线程 2 释放锁
十一、算法基础
1.Paxos算法
解决什么问题:
Paxos算法:一种基于消息传递且具有高度容错特性的一致性算法。 Paxos算法解决的问题:就是如何快速正确的在一个分布式系统中对某个数据值达成一致,并且保证不论发生任何异常, 都不会破坏整个系统的一致性。
Paxos系统描述:
•在一个Paxos系统中,首先将所有节点划分为Proposer(提议者),Acceptor(接受者),和 Learner(学习者)。(注意:每个节点都可以身兼数职)。
一个完整的Paxos算法流程分为三个阶段:
- Prepare准备阶段
- Proposer向多个Acceptor发出Propose请求Promise(承诺)
- Acceptor针对收到的Propose请求进行Promise(承诺)
- Accept接受阶段
- Proposer收到多数Acceptor承诺的Promise后,向Acceptor发出Propose请求
- Acceptor针对收到的Propose请求进行Accept处理
- Learn学习阶段:Proposer将形成的决议发送给所有Learners
Paxos算法流程:
- Prepare: Proposer生成全局唯一且递增的Proposal ID,向所有Acceptor发送Propose请求,这里无需携带提案内容,只携 带Proposal ID即可。
- Promise: Acceptor收到Propose请求后,做出“两个承诺,一个应答”。 ➢ 不再接受Proposal ID小于等于(注意:这里是<= )当前请求的Propose请求。 ➢ 不再接受Proposal ID小于(注意:这里是< )当前请求的Accept请求。 ➢ 不违背以前做出的承诺下,回复已经Accept过的提案中Proposal ID最大的那个提案的Value和Proposal ID,没有则 返回空值。
- Propose: Proposer收到多数Acceptor的Promise应答后,从应答中选择Proposal ID最大的提案的Value,作为本次要发起的 提案。如果所有应答的提案Value均为空值,则可以自己随意决定提案Value。然后携带当前Proposal ID,向所有Acceptor发送 Propose请求。
- Accept: Acceptor收到Propose请求后,在不违背自己之前做出的承诺下,接受并持久化当前Proposal ID和提案Value。
- Learn: Proposer收到多数Acceptor的Accept后,决议形成,将形成的决议发送给所有Learner
2.ZAB协议
2.1.什么是ZAB算法
ZAB协议借鉴了Paxos算法,是特别为Zookeeper设计的支持崩溃恢复的原子广播协议。基于该协议,Zookeeper设计为只有一个leader可以发起提案,有多个follower节点接收并处理。
2.2.ZAB协议内容
ZAB协议包括两种基本模式:消息广播、崩溃恢复。
2.2.1.消息广播
-
客户端发起一个这操作
-
leader服务端将客户端的请求转化为Proposal提案,同时为每个Proposal生成一个全局ID,即zxid。
-
leader为每一个Follower分配一个单独的队列,然后将需要广播的Porposal依次放到队列中去,并根据FIFO策略进行消息发送。
-
Follower接收到Proposal后,首先将其以事务日志的方式写入磁盘,写入成功后向leader反馈一个ACK响应消息。
-
Leader接收到半数以上的Follower返回的ACK,即认为消息发送成功,可以发送commit消息。
-
leader向所有follower广播commit消息,同时自身也会完成事务提交,Follower收到commit消息后,会将上一个事务提交。
-
Zookeeper采用ZAB协议的核心是,只要有一台服务器提交了Porposal,就要确保所有的服务器最终都能正确提交Porposal。
消息广播的过程分为两个阶段:
1.Porposal广播,开启事务。
2.发送commit提交事务。
leader崩溃产生的两种情况:
- Porposal刚发送未收到半数follower的ACK时,崩溃。
- 未发送commit消息时,崩溃。
ZAB协议崩溃恢复需要满足以下两个条件:
- 确保丢弃已经被leader提出的,但没有被提交的proposal。
- 确保已经被leader提交的Proposal,必须最终被所有的Follower服务器提交。
2.2.2.崩溃恢复
即就是新leader选举:
- 新选举出来的leader不能包含未提交的proposal。即leader必须都是已经提交了proposal的follower服务器节点。
- 新选举出的leader节点中含有最大的zxid。这样做的好处是可以避免leader服务器检查proposal的提交和丢弃工作。
新leader的数据同步(ZAB数据同步):
- 完成leader选举之后,正式开始工作之前(接收事务请求,然后提出新的proposal),leader服务器首先会确认事务日志中所有的proposal是否已经被集群中过半的服务器commit。
- leader服务器需要确保所有的follower服务器都能接收到每一条事务的proposal,并且能将已提交的proposal应用到内存数据中。等follower将所有尚未同步的事务proposal都从leader服务器上同步过,并且应用到内存数据以后,leader才会把follower加入到真正可用的follower列表中。
3.CAP理论
CAP理论是分布式系统的一个指标。
- 一致性 C:Consistency
分布式环境中,数据在多个副本之间是否能够保持数据一致的特性。
- 可用性 A:Available
系统提供的服务必须一直处于可用状态,对于用户的请求总能在有限时间内返回结果。
- 分区容错性 P:Partition Tolerance
分布式系统在遇到任何网络分区故障的时候,任然需要保证对外提供满足一致性和可用性的服务。除非所有网络环境全部故障。
#这是一个基本需求,最多只能同时满足两个,因为P是必须的,所以往往选择就在CP或者AP中。
Zookeeper保证的是CP
- Zookeeper不能保证每次服务请求的可用性。(注:极端情况下,Zookeeper可能会丢弃一些请求,消费者需要重新请求才能得到结果)。
- 进行leader选举的时候集群都是不可用的。(没有proposal提出者)