RPC框架-Gitee代码(麻烦点个Starred, 支持一下吧)
RPC框架-GitHub代码(麻烦点个Starred, 支持一下吧)
该项目的RPC通信将采用Netty+Zookeeper,所以会在前两章介绍使用方法
ZooKeeper的数据模型是一个类似于**文件系统的层次结构,**被组织成一个树形结构,每个节点称为ZNode
ZNode是ZooKeeper中的基本数据单元,它可以存储数据和子节点。
每个ZNode可以存储一个字节数组作为其数据,可以是任意类型的数据,例如配置信息、状态信息等。
持久节点(Persistent Node):
临时节点(Ephemeral Node):
顺序节点(Sequential Node):
Watcher机制是ZooKeeper中非常重要的概念,它允许客户端在ZooKeeper上设置监视点,以便在节点发生变化时接收通知。Watcher机制使得开发人员可以及时获取关于数据变化的通知,以便采取相应的操作
以下是Watcher机制:
1.注册Watcher:
客户端可以通过在对节点进行操作时注册Watcher来设置监视点
客户端在注册Watcher时需要指定监视的路径和Watcher对象,当指定路径的节点发生变化时,ZooKeeper会将通知发送给客户端
2.Watcher通知:
3.触发Watcher的类型
4.Watcher的触发流程
5.Watcher的注意事项
ZooKeeper提供了一组命令行工具(CLI)来与ZooKeeper集群进行交互。
以下是一些常见的ZooKeeper命令:
1.connect
连接到ZooKeeper集群。
语法: connect
2.ls
列出指定路径下的子节点。
语法: ls
3.create
create
4.get
get
5.set
set
6.delete
delete
7.stat
stat
8.getAcl
getAcl
9.setAcl
setAcl
10.quit/exit
quit
或 exit
1.添加Maven依赖,引入ZooKeeper客户端库
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.8.2version>
dependency>
2.测试用例
连接Zookeeper
private ZooKeeper zooKeeper;
@Before
public void createZooKeeper() throws IOException {
// 定义连接参数
String connectString = "127.0.0.1:2181";
// 定义超时时间 10秒
int sessionTimeout = 10000;
zooKeeper = new ZooKeeper(connectString, sessionTimeout, null);
}
创建永久节点
/**
* 创建永久节点
*/
@Test
public void testCreatePersistentNode(){
try {
String result = zooKeeper.create("/dcyrpc", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("result = " + result);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
try {
if (zooKeeper != null){
zooKeeper.close();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
删除一个永久节点
/**
* 删除一个永久节点
*/
@Test
public void testDeletePersistentNode(){
// version: cas mysql 乐观锁,也可以无视版本号 -1
try {
zooKeeper.delete("/dcyrpc", -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
} finally {
try {
if (zooKeeper != null){
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
设置指定节点的数据
/**
* 设置指定节点的数据
*/
@Test
public void testSetNodeData(){
try {
zooKeeper.setData("/dcyrpc", "hi rpc".getBytes(), -1);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
} finally {
try {
if (zooKeeper != null){
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
检测节点的版本号
/**
* 检测节点的版本号
*/
@Test
public void testCheckNodeVersion(){
try {
// version: cas mysql 乐观锁,也可以无视版本号 -1
Stat stat = zooKeeper.exists("/dcyrpc", null);
// 当前节点的数据版本
int version = stat.getVersion();
System.out.println("version = " + version);
// 当前节点的acl数据版本
int aversion = stat.getAversion();
System.out.println("aversion = " + aversion);
// 当前子节点的数据版本
int cversion = stat.getCversion();
System.out.println("cversion = " + cversion);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
} finally {
try {
if (zooKeeper != null){
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
watcher机制的本质是向zookeeper注册关心的事件,然后在本地存储钩子函数,当事件发生后调用钩子函数
概念
Zookeeper提供了数据的发布/订阅功能。多个订阅者可监听某一特定主题对象(节点)。当主题对象发生改变(数据内容改变,被删除等),会实时通知所有订阅者。该机制在被订阅对象发生变化时,会异步通知客户端,因此客户端不必在注册监听后轮询阻塞。
Watcher机制实际上与观察者模式类似,也可看作观察者模式在分布式场景中给的一种应用。
特性
特性 | 说明 |
---|---|
一次性 | Watcher是一次性的,一旦触发,就会被移除,再次使用需要重新注册 |
轻量级 | WatcherEvent是最小的通信单元,结构上只包含连接状态、事件类型和节点路径,并不会告诉数据 |
时效性 | watcher只有在当前session彻底时效时才会无效,若在session有效期内重新连接成功,则watcher |
ZooKeeper中的读取操作getData、exist、getChildren 等都可以使用指定参数为节点设置监听
Zookeeper监听有三个关键点:
java api中 有三个方法可以注册监听,getData、exist、getChildren
监听器:
节点的事件主要有5种类型:
连接事件类型 Watcher.Event.KeeperState枚举维护
注册事件的方式与节点事件的关系:
方式 | NodeCreate | NodeDeleted | NodeDataChanged | NodeChildrenChanged |
---|---|---|---|---|
exist | 可监控 | 可监控 | 可监控 | 不可监控 |
getData | 不可监控 | 可监控 | 可监控 | 不可监控 |
getChildren | 不可监控 | 可监控 | 不可监控 | 可监控 |
测试用例
创建方法实现Watcher接口,在process方法里实现业务逻辑
/**
* zookeeper 的 watcher 机制
* watcher只编写自己关心的事件
*/
public class WatcherTest implements Watcher {
@Override
public void process(WatchedEvent event) {
// 判断事件类型:连接事件类型/节点事件类型
if (event.getType() == Event.EventType.None) {
// 判断具体的连接事件类型
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("zookeeper连接成功");
} else if (event.getState() == Event.KeeperState.AuthFailed) {
System.out.println("zookeeper身份认证失败");
} else if (event.getState() == Event.KeeperState.Disconnected) {
System.out.println("zookeeper断开连接");
}
} else if (event.getType() == Event.EventType.NodeCreated) {
System.out.println(event.getPath() + " 节点被创建");
} else if (event.getType() == Event.EventType.NodeDeleted) {
System.out.println(event.getPath() + " 节点被删除");
}
}
}
/**
* 检测watcher
*/
@Test
public void testWatcher(){
try {
// 以下三个方法可以注册watcher,可以直接new一个新的watcher
// 也可以使用true,选定默认的watcher
zooKeeper.exists("/dcyrpc", true);
// zooKeeper.getChildren();
// zooKeeper.getData();
while (true) {
Thread.sleep(1000);
}
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
} finally {
try {
if (zooKeeper != null){
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
zookeeper的运行模式有单机模式,伪集群模式,集群模式三种。单个Zookeeper节点是会存在单点故障的,Zookeeper节点部署越多,服务的可靠性越高。通常建议部署奇数个节点,因为zookeeper集群是以宕机个数过半才会让整个集群宕机的。
zookeeper的集群模式下,节点分为leader和follower两种状态,leader负责所有的写操作,follower负责相关的读操作。
主机名 | IP地址 |
---|---|
linux-node1 | 192.168.200.128 |
linux-node2 | 192.168.200.129 |
linux-node3 | 192.168.200.130 |
Zookeeper运行需要java环境,需要安装jdk
注:每台服务器上面都需要安装zookeeper、jdk
修改zookeeper的配置文件,构建集群的基础配置:
dataLogDir=/usr/local/apache-zookeeper-3.8.2-bin/zookeeper/logs
dataDir=/usr/local/apache-zookeeper-3.8.2-bin/zookeeper/data
server.1= 192.168.200.128:2888:3888
server.2= 192.168.200.129:2888:3888
server.3= 192.168.200.130:2888:3888
server.1中的1指代第几个节点,2888端口用来辅助这个服务器与集群中的leader服务器做交换信息的端口,3888端口是在leader挂掉时专门用来进行选举leader所用的端口。
创建日志和持久化目录:
mkdir -p /zookeeper/{logs,data}
创建ServerID标识
除了修改zoo.cfg配置文件外,zookeeper集群模式下还要配置一个myid文件,这个文件需要放在dataDir目录下
在server.1服务器上面创建myid文件,就将他的值设置为1,以此类推
touch myid
each 1 > myid
在每台服务器上都要进行创建目录与ServerID
连接集群
@Before
public void createZooKeeper() throws IOException {
// 定义连接集群,用逗号隔开
String connectString = "192.168.200.129:2181,192.168.200.130:2181,192.168.200.130:2181";
// 定义超时时间 10秒
int sessionTimeout = 10000;
// 构建zookeeper是否需要等待连接
zooKeeper = new ZooKeeper(connectString, sessionTimeout, new WatcherTest());
}
对于任何一个分布式系统而言,数据同步永远都是重中之重。因为一个集群当中会有很多节点,那么客户端每次写数据的时候,是只向一个节点写入,还是向所有节点写入就成了一个问题。
如果向所有节点写入,假设节点个数为 N,那么客户端的一次写请求就会被放大 N 倍,因为每个节点都要写一遍,显然这么做是非常不明智的。因此我们应该让客户端只向一个节点写入,然后该节点再将数据同步给集群内的其它节点。
但这就产生了一个问题,如果某个节点的数据同步还没有完成,就收到了客户端的读请求,那么显然会返回旧数据。如果想让客户端看到的一定是新数据,那么就必须等到数据在所有节点之间都同步完成之后,才能让客户端访问,而这又会造成集群服务出现短暂的不可用。
客户端的每次读操作,不管访问哪个节点,读到的都是同一份最新的数据。不会出现读不同节点,得到的数据不同这种情况。
但集群毕竟不是单机,总会有网络故障的时候,这时候如果要保证一致性,也就是让客户端访问任何一个节点都能看到相同的数据,那么就应该拒绝服务(客户端读取失败),等到数据同步完成之后再提供服务。
一致性强调的不是数据完整,而是各节点之间的数据绝对一致。
任何来自客户端的请求,不管访问哪个节点,都能得到响应数据,但不保证是同一份最新数据。
可用性这个指标强调的是服务可用,但不保证数据的绝对一致。
当节点间出现任意数量的消息丢失或高延迟的时候,系统仍然可以继续提供服务。
分布式系统会告诉客户端:不管我的内部出现什么样的数据同步问题,我会一直运行,提供服务。
这个指标,强调的是集群对分区故障的容错能力。
对于一个分布式系统而言,一致性、可用性、分区容错性 3 个指标不可兼得,只能在 3 个指标中选择两个。
只要有网络交互就一定会有延迟和数据丢失,而这种状况我们必须接受,还必须保证系统不能挂掉。
分区容错性(P)是前提,是必须要保证的
现在就只剩下一致性(C)和可用性(A)可以选择了:**要么选择一致性,保证数据绝对一致;要么选择可用性,保证服务可用。**如果选择 C,那么就是 CP 模型;如果选择 A,那么就是 AP 模型。
选择 CP 一般都是对数据一致性特别敏感,尤其是在支付交易领域,Hbase 等分布式数据库领域,都要优先保证数据的一致性,在出现网络异常时,系统就会暂停服务处理。还有用来分发及订阅元数据的 Zookeeper、Etcd 等等,也是优先保证 CP 的。
比如微博多地部署,如果不同区域出现网络中断,区域内的用户仍然能发微博、相互评论和点赞,但暂时无法看到其它区域用户发布的新微博和互动状态。
还有类似 12306 这种火车购票系统,在节假日高峰期抢票时也会遇到这种情况,明明某车次有余票,但真正点击购买时,却提示说没有余票。就是因为票已经被抢光了,票的可选数量应该更新为 0,但因并发过高导致当前访问的节点还没有来得及更新就提供服务了。因此它返回的是更新之前的旧数据,但其实已经没有票了。
BASE 理论是 CAP 理论中的 AP 的延伸,所以它强调的是可用性,这个理论广泛应用在大型互联网的后台当中。它的核心思想就是基本可用(Basically Available)和最终一致性(Eventually consistent)。
当分布式系统在出现不可预知的故障时,允许损失部分功能的可用性,来保障核心功能的可用性。说白了就是服务降级,在服务器资源不够、或者说压力过大时,将一些非核心服务暂停,优先保证核心服务的运行。比如:
系统中所有的数据副本在经过一段时间的同步后,最终能够达到一致的状态
在数据一致性上,存在一个短暂的延迟,几乎所有的互联网系统采用的都是最终一致性。比如 12306 买票,票明明卖光了,但还是显示有余票,说明此时数据不一致。但当你在真正购买的时候,又会提示你票卖光了,说明数据最终是一致的。
如果业务的某功能无法容忍一致性的延迟(比如分布式锁对应的数据),就需要强一致性;如果能容忍短暂的一致性的延迟(比如APP用户的状态数据),就可以考虑最终一致性。
假设现在有一个写操作,需要在ZooKeeper集群服务中执行写操作,创建一个/dcyrpc节点,其大致流程如
下:
已经执行了写操作还要数据同步吗?
Follower节点执行写操作并返回成功结果给Leader是为了保证写操作的一致性和持久性。数据同步是确保在整个集群中所有节点的数据是最终一致的关键步骤。
但在写操作的结果被确认之前,数据同步的过程是必要的。这是因为ZooKeeper使用了多数原则来决定写操作的最终结果,只有在大多数节点都完成写操作并确认成功后,Leader才会确认写操作成功。此时,数据同步的过程才会开始。
数据同步是在写操作完成后才开始的,这意味着在数据同步期间的某个时间点,集群中的不同节点的数据可能是不一致的。但是一旦数据同步完成,所有节点都将具有相同的数据,并保持一致性。这样做是为了在写操作期间保持高可用性,并在数据同步完成后确保数据的一致性。
数据同步是增量同步还是全量同步?
全量数据同步需要将所有相关节点上的数据进行复制,网络传输和处理的开销都可能会比较大。
为了减小全量数据同步的性能开销,ZooKeeper在设计上采取了一些优化措施:
假设现在有myid为1、2、3的三台ZooKeeper服务器,分别启动1、2、3三台服务,下面是集群启动时的选举过程的详细描述:
1.初始状态:集群中的所有服务器处于LOOKING状态,即正在寻找leader节点
2.服务器1启动:服务器1作为第一台启动的服务器,它将成为Leader选举的候选者,并向其他服务器发送选举请求
3.服务器1的选举投票:服务器1将自己的投票信息(包括标识符myid=1、ZXID和状态LOOKING)发送给服务器2和服务器3。(默认给自己也投一票)
4.服务器2和服务器3收到选举请求:服务器2和服务器3收到来自服务器1的选举请求,并检查自己的状态
5.服务器2和服务器3的选举投票:服务器2和服务器3会分别初始化自己的投票,并将自己的投票信息发送回给服务器1。(默认给自己也投一票)
6.服务器1收集和处理投票:服务器1收到来自服务器2和服务器3的投票,它将根据规则进行投票的计算和比较。如果服务器1得到了大多数的投票(超过半数,即两票),它将更改自己的状态为LEADING,并成为leader。
7.选举结果通知:服务器1成为leader后,它将选举结果通知给服务器2和服务器3。服务器2和服务器3会更新自己的状态,并认可服务器1作为leader
8.集群状态稳定:现在,服务器1成为了leader,而服务器2和服务器3成为了follower。整个集群的状态稳定下来,Leader将开始处理客户端的请求
在Elasticsearch、ZooKeeper这些集群环境中,有一个共同的特点,就是它们有一个“大脑”。比如Elasticsearch集群中有Master节点,ZooKeeper集群中有Leader节点。
集群中的Master或Leader节点往往是通过选举产生的。在网络正常的情况下,可以顺利的选举出Leader。但当两个机房之间的网络通信出现故障时,选举机制就有可能在不同的网络分区中选出两个Leader当网络恢复时,这两个Leader该如何处理数据同步?又该听谁的?这也就出现了“脑裂”现象。
zookeeper集群中的脑裂
在使用zookeeper时,很少遇到脑裂现象,是因为zookeeper已经采取了相应的措施来减少或避免脑裂的发生。现在先假设zookeeper没有采取这些防止脑裂的措施。在这种情况下,看看脑裂问题是如何发生的。
现有6台zkServer服务组成了一个集群,部署在2个机房:
正常情况下,该集群只有会有个Leader,当Leader宕掉时,其他5个服务会重新选举出一个新的Leader。
如果机房1和机房2之间的网络出现故障,暂时不考虑Zookeeper的防止措施,那么就会出现下图的情况:
也就是说机房2的三台服务检测到没有Leader了,于是开始重新选举,选举出一个新Leader来。原本一个集群,被分成了两个集群,同时出现了两个“大脑”,这就是所谓的“脑裂”现象。
由于原本的一个集群变成了两个,都对外提供服务。一段时间之后,两个集群之间的数据可能会变得不一致了。当网络恢复时,就面临着谁当Leader,数据怎么合并,数据冲突怎么解决等问题。
Zookeeper默认采用的是“过半原则”。所谓的过半原则就是:在Leader选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。
以上图6台服务器为例来进行说明:half = 6 / 2 = 3,也就是说选举的时候,要成为Leader至少要有4台机器投票才能够选举成功。那么,针对上面2个机房断网的情况,由于机房1和机房2都只有3台服务器,根本无法选举出Leader。这种情况下整个集群将没有Leader。
在没有Leader的情况下,会导致Zookeeper无法对外提供服务,所以在设计的时候,我们在集群搭建的时候,要避免这种情况的出现。
如果两个机房的部署请求部署3:3这种状况,而是3:2,也就是机房1中三台服务器,机房2中两台服务器:
在上述情况下,先计算half = 5 / 2 = 2,也就是需要大于2台机器才能选举出Leader。那么此时,对于机房1可以正常选举出Leader。对于机房2来说,由于只有2台服务器,则无法选出Leader。此时整个集群只有一个Leader。
通过过半原则可以防止机房分区时导致脑裂现象,但还有一种情况就是Leader假死。
假设某个Leader假死,其余的followers选举出了一个新的Leader。这时,旧的Leader复活并且仍然认为自己是Leader,向其他followers发出写请求也是会被拒绝的。
因为ZooKeeper维护了一个叫epoch的变量,每当新Leader产生时,会生成一个epoch标号(标识当前属于那个Leader的统治时期),epoch是递增的,followers如果确认了新的Leader存在,知道其epoch,就会拒绝epoch小于现任leader epoch的所有请求。
只要集群中有过半的机器是正常工作的,那么整个集群就可对外服务。
列举一些情况,来看看在这些情况下集群的容错性: