一、分布式技术相关的理论
CAP理论
CAP定理(CAP theorem),⼜被称作布鲁尔定理(Eric Brewer),1998年第⼀次提出.
最初提出是指分布式数据存储不可能同时提供以下三种保证中的两种以上:
(1) ⼀致性(Consistency): 每次读取收到的信息都是最新的;
(2) 可⽤性(Availability): 每个请求都会收到(⾮错误)响应;
(3) 分区容错性(Partition tolerance): 尽管节点之间的⽹络不通导致分区,系统仍继续运⾏.
事实上,不仅仅是分布式数据存储应⽤,所有分布式系统都必须在CAP这三点之间进⾏权衡.
分区容错性
在分布式系统中,分区容错性是指系统能够继续正常工作,即使网络分区(即网络中的一部分节点无法与其他节点通信)发生。分布式系统通常使用复制和容错技术来实现分区容错性。例如,在分布式数据库系统中,数据可以复制到多个节点上,当一个节点无法与其他节点通信时,系统仍然可以使用其他节点上的数据进行操作。
BAS理论
数据⼀致性模型
如果数据的读取、写⼊、更新的结果是可预测的,称之为遵循数据⼀致性模型.
(1) 严格⼀致性(Strict Consistency)(强)
不论在哪个节点,看到的资源都是统⼀的结果;
(2) 顺序⼀致性(Sequential Consistency)(弱)
节点的数据变动和操作的顺序保持⼀致;
(3) 最终⼀致性(Eventual Consistency)(弱)
所有数据副本最终都会变得⼀致.
⼀致性算法
- Paxos : Zookeeper
- Raft : ETCD
BASE理论
BASE是Basically Available(基本可⽤)、 Soft state(软状态)和Eventual Consistency(最终⼀致性)三个短语的缩写.
(1) 基本可⽤: 可能是部分功能不可⽤或者是响应时间延⻓;
(2) 软状态: 不同系统/节点之间,数据存在过渡状态;
(3) 最终⼀致: 经过系统内部协调机制,最终所有的节点保持⼀致(分布式系统中的⼀致并不⼀定指数据保持⼀样).
两条系统设计的原则
墨菲定律
墨菲定律(Murphy's law)是⼀种⼼理学效应,由爱德华·墨菲(Edward A. Murphy)提出,亦称墨菲法则.
墨菲定律: 如果有两种或两种以上的⽅式去做某件事情,⽽其中⼀种选择⽅式将导致灾难,则必定有⼈会做出这种选择.
本质: 如果事情有变坏的可能,不管这种可能性有多⼩,它总会发⽣.
康威定律
“设计系统的架构受制于⽣产这些设计的组织的沟通结构
二、zookeeper
2.1概述
zookeeper是一个分布式协调服务。所谓分布式协调主要是来解决分布式系统中多个进程之间的同步限制,防止出现脏读,例如我们常说的分布式锁。
补充安装 测试
import sys
from kazoo.client import KazooClient, KazooState
def main():
zk = KazooClient(hosts='118.25.185.49:3306',timeout=100)
zk.start()
data, stat = zk.get("/")
print(data)
print(stat)
chilen = zk.get_children("/")
print(chilen)
zk.stop()
zk.close()
if __name__ == "__main__":
main()
2.2应用案例
zookeeper的同类产品
- Consul
- ETCD
- Doozer
ZooKeeper -server host:port cmd args
# 连接到指定主机和端口的 ZooKeeper 服务器,并执行指定的命令和参数。
stat path [watch]
# 获取指定路径节点的状态,可以设置监视器以接收更改通知。
set path data [version]
# 将指定路径节点的数据设置为给定数据,可选进行版本检查。
ls path [watch]
# 列出指定路径节点的子节点,可以设置监视器以接收更改通知。
delquota [-n|-b] path
# 删除指定路径的配额,可以是命名空间(-n)或字节(-b)。
ls2 path [watch]
# 列出指定路径节点的子节点,可以设置监视器以接收更改通知。
setAcl path acl
# 为指定路径节点设置 ACL(访问控制列表)。
setquota -n|-b val path
# 为指定路径设置配额,可以是命名空间(-n)或字节(-b),并指定值。
history
# 显示命令历史记录。
redo cmdno
# 重做指定的命令编号。
printwatches on|off
# 打印监视器状态,可以开启或关闭。
delete path [version]
# 删除指定路径的节点,可以指定版本。
sync path
# 同步指定路径。
listquota path
# 列出指定路径的配额信息。
rmr path
# 递归删除指定路径。
get path [watch]
# 获取指定路径节点的数据,可以设置监视器。
create [-s] [-e] path data acl
# 创建具有指定数据和 ACL 的节点,可以选择顺序(-s)或临时(-e)。
addauth scheme auth
# 添加指定方案和凭证的认证信息。
quit
# 退出 ZooKeeper 客户端。
getAcl path
# 获取指定路径节点的 ACL。
close
# 关闭 ZooKeeper 客户端连接。
connect host:port
# 连接到指定主机和端口的 ZooKeeper 服务器。
zookeeper中的数据是存储在内存当中的,因此它的效率十分高效。它内部的存储方式十分类似于文件存储结构,采用了分层存储结构。但是它和文件存储结构的区别是,它的各个节点中是允许存储数据的,需要注意的是zk的每个节点存储数据不能超过1M。它的内存数据结果如下图:
我们可以通过不同的路径访问到不同的节点,因为它是分层结构,我们也可以通过某一个父节点,获取到该节点下的所有子节点信息。
1)create:创建一个新节点,通过指定路径的方式创建节点,例如创建路径为/A/A1/demo,则会在A1节点下创建一个demo节点;
2)delete:删除节点,通过路径的方式删除节点,如果删除路径为/A/A1/demo,则会删除A1节点下的demo节点;
3)exists:判断指定路径下的节点是否存在,例如判断路径为/A/A1/demo,则会判断A1节点下的demo节点是否存在;
4)get:获取指定路径下某个节点的值是什么,例如获取路径为/A/A1/demo,则会获取A1节点下的demo节点的值什么;
5)set:为指定路径的节点进行赋值操作,例如修改路径为/A/A1/demo,则会修改A1节点下的demo节点的值;
6)get children:获取指定路径节点下的子节点信息,例如获取路径为/A,则会获取A节点下的A1和A2节点;
7)sync:获取到同步数据,这个涉及到了zk的原理,zk集群属于最终一致性,调用该方法,可以获取到最终的结果值,如果不使用该方法,在查询的时候可能获取到的值是中间值;
zk中创建的节点分为两种:永久性节点和临时性节点。永久性节点即创建以后,在不执行delete命令的前提下,该节点是永久存在的;而临时节点与session有关,每个客户端与zk建立链接的时候会生成一个session,这个session不会因为链接zk服务器节点的变化而变化,只有当客户端断开连接以后,该session才会消失,而临时节点会随着session的消失而消失。
2.3Zookeeper核⼼概念
- Session会话
- 数据模型
- Watch
Session会话
- ⼀个客户端连接⼀个会话,由Zookeeper分配唯⼀的会话ID;
- 客户端以特定的时间间隔发送⼼跳以保持会话有效,tickTime;
- 超过会话超时时间未收到客户端的⼼跳,则判定客户端“死”了,默认是两倍的tickTime;
- 会话中的请求按FIFO顺序执⾏.
数据模型
Znode命名规范
Znode节点类型
顺序节点
Znode数据构成
Znode 元数据stat结构
ACL: 访问控制列表
ACL权限:
上面的属性中我们说到了个ACL权限,这里的权限指的是对节点的操作权限,一共分为5个权限C(Create,节点创建权限)、D(Delete,删除节点权限)、R(Read,读取节点的权限)、W(Write,更新节点的权限)、A(Admin,管理员权限)。
create [-s] [-e] path data acl
setAcl path acl
getAcl path
Zookeeper中的时间
Watch监听机制
Zookeeper具有发布订阅功能,这就要求订阅的主题发生变化时需要通知左右的订阅者并且获取新的主题信息,Zookeeper就是通过Watcher来实现的。
客户端可以创建并向服务段注册一个Watcher监听,监听在客户端是由WatchManager来管理的,当主题发生变化时会通过对应的Watcher来通知客户端。
Watch重要特性
⼀次性触发
- Watch触发后即被删除,要持续监控变化,则需要持续设置watch.
有序性
- 客户端先得到watch通知,之后才会看到变化结果.
Watch注意事项
- Watch是⼀次性触发器,如果你获得⼀个watch事件,并且希望得到关于未来更改的通知,则必须设置另⼀个watch;
- 因为watch是⼀次性触发器,并且在获取事件和发送获取watch的新情求之间存在延迟,所以不能可靠地得到节点发⽣的每个更改;
- ⼀个watch对象只会被特定的通知触发⼀次。如果⼀个watch对象同时注册了exists、getData,当节点被删除时,删除事件对exists、getData都有效,但只会调⽤watch⼀次.
Zookeeper特性
2.4Zookeeper集群
组⽹⽅式
三台虚拟机
192.168.31.241
192.168.31.242
192.168.31.243
通过映射到主机端
2181/2888/3888
容器⽹络⽅案: OpenVSwitch
docker run -d --rm -p 2181:2181 -p 2888:2888 -p 3888:3888 -e ZOO_MY_ID=1 -e ZOO_SERVERS="server.1=0.0.0.0:2888:3888 server.2=192.168.31.242:2888:3888 server.3=192.168.31.243:2888:3888" zookeeper:3.4.11
docker run -d --rm -p 2181:2181 -p 2888:2888 -p 3888:3888 -e ZOO_MY_ID=2 -e
ZOO_SERVERS="server.1=192.168.31.241:2888:3888 server.2=0.0.0.0:2888:3888 server.3=192.168.31.243:2888:3888"
zookeeper:3.4.11
docker run -d --rm -p 2181:2181 -p 2888:2888 -p 3888:3888 -e ZOO_MY_ID=3 -e
ZOO_SERVERS="server.1=192.168.31.241:2888:3888 server.2=192.168.31.242:2888:3888 server.3=0.0.0.0:2888:3888" zookeeper:3.4.11
2.5zk选主流程
zk的设计目标就是高可用性,那么也就意味着,在使用zk的时候一般都是使用集群而不是单点模式。首先来看一下zk的集群模式,如下图:
该图为zk集群的可用状态,从上图中可以看到,zk的集群是主从集群,客户端可以随意与任何zk服务节点进行连接,并且各个客户端都可以进行读写操作,这是一个和redis主从集群的区别,redis的主从集群,如果客户端是写操作,那么只能连接redis的主节点才可以。
zk的每个客户端是随机连接到zk服务节点的,并且每个客户端都可以进行读写操作,读操作都是在客户端连接的zk节点进行操作;而写操作是有区别的,如果该客户端连接的是leader节点,那么直接进行写操作;如果该客户端连接的是follower节点,那么zk的服务节点会自动将该写操作转到leader节点进行。
zk的集群为主从集群,那么也就意味着主节点只有一个,那么当主节点挂了以后,该zk集群则会处于不可用状态,既然zk的设计目的是高可用,也就意味着当主节点挂了以后,zk会有一定的方式来快速的选出主节点,让服务恢复可用状态,zk的官方文档中给出的压测报告,7台zk服务,选主耗时大概200ms。
介绍zk的选举流程之前需要先解释两个概念:zxid以及myid。zxid指的是当前节点的事物id,通俗点说就是当前节点完成的数据同步情况,该值越大,越能说明该节点的数据同步情况越完整,丢失数据的情况越小或者丢失数据越少。myid是在创建zk集群的时候,我们给它的赋值。
zk的follower节点和leader节点是通过心跳,来查看服务是否可用。在这其中,只要有有一台follower节点发现主节点挂掉,他就开始向其它follower节点发送选主请求,整个集群进入选主流程,不再向外提供服务。
先假设现在有4个zk节点,分别为node1,node2,node3,node4,他们的myid分别为1,2,3,4选主流程主要分为以下两种情况:
1.初始启动,在启动阶段时,此时各个服务节点的zxid都为0,只与myid有关。假设启动顺序为node1->node2->node3->node4,当启动动1和2的时候,该zk集群是不可用状态,因为zk的选主必须是过半服务节点同意(包含自己),最低需要启动三个节点才可以进行选举,因此只有node1和node2启动的时候,此时只有两台服务,不满足条件,当第三台节点启动以后,才满足了选主的最低条件,然后进入到选举流程,因为node3的myid最大,所以此时3号节点为leader,然后启动node4,由于此时已经选举出3位leader节点并且过半通过,则不再选取新的主节点。则该集群的leader节点为node3。
2.运行过程中,初始启动过程中的leader(node3)节点挂掉,假设此时只有node4节点发现leader已经挂掉,node1和node2的Zxid都是10,node4的Zxid为9,选主的时候需要比较zxid和myid,需要注意他们的优先级,zxid为第一优先级,myid为第二优先级,选举流程大致分为以下几步:
1)node4节点给自己投票,然后将自己的zxid和myid发送给node1和node2节点:
2)node1和node2通过比较zxid和myid,发现node4不能成为leader节点,将各自的zxid和myid发送给node4,然后node4接收到以后,发现node1和node2都比自己时候成为leader节点,会给它们进行投票
3)node1和node2反驳完node4的选主请求以后,开始进行各自的选主流程,起过程与node4的过程一致,通过上面的优先级,我们可以知道最终node2会成为leader节点,那么以node2为例说一下接下来的流程。node2首先给自己投票,然后将自己zxid和myid推送给node1和node4,此时会发现node2适合成为主节点,则会给node2节点进行投票,最终选出node2成为主节点,zk集群恢复成可用状态。
2.6zk数据一致性
zk服务一般是以集群状态提供服务,多个zk节点之间的数据一致性是通过zap(原子广播)协议来保证的。zk的数据一致性为最终一致性,需要注意的是他不是实时的,比如node1,node2,node3,其中node3为leader,node1和node2为follower,当node1进行节点创建以后,leader节点肯定为实时更新,但是follower节点不一定为实时更新,因为只要过半通过就算节点已经创建成功,可能会有的节点当前的数据还不是最终态,但是它的更新指令是存在,只是可能还没执行。我们的客户端如果想要读取最终态的数据,那么可以通过使用上面的sync命令,来获取最终数据。
先看一下下面的流程图,然后再进行详细解释:
1)首先由客户端发送创建节点的指令给到zk节点,假设这个zk节点为follower1节点;
2)follower1节点发现是写操作节点,则将该指令通过2888端口转发到leader节点执行;
3)leader节点更新自己zxid信息,也就是事务id信息;
4)leader节点先将创建节点信息同步到log日志中,然后再follower1和follower2各自的队列中放入创建节点写日志的指令,当follower节点接收到指令以后,执行写日志操作,写入日志成功以后,告诉leader写入完成;leader会判断目前是否已经有过半的节点(包含自己)已经写入完成,如果完成,则先在自己的内存中创建节点,然后将在follower对应的节点中加入在内存中创建节点的指令,然后follower接收到指令以后进行内存操作,操作完成以后告诉leader写入完成,同样需要过半完成;
5)将创建结束的消息返回给调用的follower,然后返回给客户端,节点创建结束。
上面步骤中的第四步其实就是对原子广播协议的一个大致解释,原子广播协议可以看成两部分,首先原子就代表这只有成功或者失败,没有中间状态;而广播就是并不意味着所有节点都完成相关操作才算完成,只要过半节点是成功的,那么本次操作就算成功完成了。在第四步中提到的队列就是对最终一致性的一个解释,leader会将所有指令按照顺序放入每个follower对应的队列中,每个follower按顺序去执行队列中的指令,达到一个最终一致性的结果。
三、RPC原理
3.1概述
Remote procedure call - RPC
远程过程调⽤
过程是什么?
过程就是业务处理、计算任务,更直⽩理解,就
是程序;
像调⽤本地⽅法⼀样调⽤远程的过程.
熟悉的Webserveice、restful接⼜调⽤时都是RPC,仅消息的组织⽅式以及消息协议不同.
远程过程调⽤较本地调⽤有何不同?
- 速度相对慢;
- 可靠性减弱.
3.2RPC流程
3.3RPC协议
3.3RPC框架
封装好参数编组、消息解组、底层⽹络通信的RPC程序开发框架,带来的便捷是可以直接在其
基础上只专注过程代码编写
为什么要⽤RPC
- 服务化;
- 可重⽤;
- 系统间交互调⽤.
3.4 RPC核⼼概念术语
3.5基于RPC的分布式服务注册与服务发现架构
流程图
步骤
1. 创建Zookeeper集群2. 制作Kazoo镜像3. 实现服务注册代码4. 实现服务发现代码
创建Zookeeper集群
. 制作Kazoo镜像
实现服务注册
实现服务发现代码
四、实战
4.1maven创建zookeeper的增删改查
maven创建zookeeper的增删改查
4.2Curator操作Zookeeper
pom文件配置
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
创建会话
//创建一个重试策略,连接失败重试三次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
//创建客户端连接
CuratorFramework client =CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //连接地址
.sessionTimeoutMs(50000) //会话超时时间
.connectionTimeoutMs(30000) //连接超时时间
.retryPolicy(retryPolicy) //重试机制
.build();
//启动客户端
client.start();
创建节点
创建节点需要一个path,这个path是可以是不存在的文件路径,框架会自动创建上父节点。
public static void createNode(CuratorFramework client) throws Exception {
String path = "/testClient/test002";
client.create()
.creatingParentsIfNeeded() //如果父节点不存在则自动创建
.withMode(CreateMode.EPHEMERAL) //创建临时节点
.forPath(path); //创建的节点路径
}
查询节点
public static void getNode(CuratorFramework client) throws Exception {
String path = "/testClient/test002";
Stat stat = new Stat();
byte[] bytes = client.getData().storingStatIn(stat).forPath(path);
System.out.println(new String(bytes));
}
更新节点
更新是有个乐观锁控制的,分为两种情况,一种是更新不传入版本默认更新最新版本,另一种更新需要传入版本,先查询一下版本号再更新,咱看一下指定版本号修改:
public static void updateNode(CuratorFramework client) throws Exception {
String path = "/testClient/test002";
Stat stat = new Stat();
//更新并且获取新的版本号
int version = client.setData().withVersion(stat.getVersion()).forPath(path).getVersion();
}
注意:Stat只能使用一次,下次再使用版本就过期了会报错
删除节点
删除节点同样是可以通过指定版本删除的,咱看一下不通过版本删除的就行。
public static void deleteNode(CuratorFramework client) throws Exception {
String path = "/testClient";
client.delete()
.deletingChildrenIfNeeded() //删除节点的同时递归删除子节点
.forPath(path);
}
咱创建节点后延时两秒删除看一下效果:
监听
Curator实现的监听方式有两种,最常用的是通过NodeCache节点缓存实现的监听。NodeCache监听分为三种:Node Cache用于节点自身的监听;Path Cache用于子节点的监听;Tree Cache既能监听子节点也能监听自身。
看一下简单的Node Cache实现方式:
需要引入对应的pom文件,这里面包含了Zookeeper的一些典型应用场景
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
看一下代码实现:
public static void nodeTreeCache(CuratorFramework client) throws Exception {
String path = "testClient";
//如果path存在,删除路径
Stat stat = client.checkExists().forPath("/" + path);
if (stat != null) {
client.delete().guaranteed().deletingChildrenIfNeeded().forPath("/" + path);
}
//创建path
client.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/" + path, path.getBytes());
//参数:true代表缓存数据到本地
PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/" + path, true);
//BUILD_INITIAL_CACHE 代表使用同步的方式进行缓存初始化。
pathChildrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
//监听事件,监听节点的变化
pathChildrenCache.getListenable().addListener((cf, event) -> {
PathChildrenCacheEvent.Type eventType = event.getType();
switch (eventType) {
case CONNECTION_RECONNECTED:
pathChildrenCache.rebuild();
break;
case CONNECTION_SUSPENDED:
break;
case CONNECTION_LOST:
System.out.println("链接丢失---------");
break;
case CHILD_ADDED:
System.out.println("增加子节点------");
break;
case CHILD_UPDATED:
System.out.println("更新子节点-------");
break;
case CHILD_REMOVED:
System.out.println("删除子节点-------");
break;
default:
}
});
}
4.3Zookeeper应用场景
数据发布订阅
发布订阅是将数据发布到Zookeeper的节点上,使用的应用通过订阅节点类动态的获取数据,实现配置数据的几种管理和动态更新。例如使用Zookeeper做配置中心,将yml中的数据库配置或其他一些外部配置发布到Zookeeper对应的节点路径下面,通过节点路径判断当前服务的配置位置,然后再获取子节点中对应的配置信息加载到代码中,实现动态配置。
命名服务
命名服务是分布式项目中常见的一种场景,可以通过指定的名字来获取对应的资源或地址。Zookeeper会在自己的文件系统上创建一个以路径为名称的节点,它可以指向提供的服务的地址,远程对象等。简单来说使用Zookeeper做命名服务就是用路径作为名字,路径上的数据就是其名字指向的实体,例如常见的Zookeeper在Dobbo中的使用。
分布式协调
在分布式项目中我们很多数据是通过MQ来传递操作的,我们通过MQ只能知道当前数据被操作了,但是不清楚执行的结果,到底是执行成功了还是执行失败了,这时候可以通过Zookeeper来通知操作的结果。
设置一个分布式系统中不只是两步操作,有可能是一系列的操作,就可以通过Zookeeper中的内容一直更新,来通知各个节点走到了哪一步了。
分布式锁
在分布式项目中假设我们需要加锁,因为是多个JVM环境一个机器上加的锁对另一个机器上是无效的,所以可以通过zookeeper来实现。例如我们多个机器在zookeeper上创建一个节点,谁先创建出来该节点谁获取到锁,当其他机器创建的时候因为节点已经存在了,所以就报错了,当然这个错try了,然后循环一直创建,锁用完之后会删除节点,省下的机器继续看谁先创建出节点来。
Zookeeper的应用场景还有很多,都是应用其类似于共享文件的属性来设计的,例如用顺序节点来做数据库主键自增等。