谈点分布式
什么是分布式呢?
起初,我们的应用流量比较小,所有东西全部部署在一个服务器,比如全部丢给一个tomcat来处理,顶多做一个tomcat的多节点部署多分,再挂一台Nginx做一下负载均衡就OK了。但是随着业务功能复杂度上升,访问流程的上升,单体架构就不行了。这个时候就该分布式上场了,将业务模块做一定拆分,各业务组件分布在网络上不同的计算机节点上,同时为了保证高可用性和性能,单个组件模块也会做集群部署。
分布式虽然爽了,但是随之而来的就是分布式带来的复杂性,比如在分布式系统中网络故障问题几乎是必然存在的;事务也不再是数据库帮我们保证了,因为可能每个业务有自己的库,但不同业务之间又有保证事务的需求,这是就需要考虑实现分布式事务了;还有数据一致性问题,集群中副本节点不能及时同步到主节点的数据,会有数据一致性问题需要解决。
分布式理论
实践需要理论的知道,同样地,在分布式软件开发领域内,也是有前辈大神们做了基础的理论研究。下面将要介绍的就是分布式相关的两个基础理论:CAP定理和BASE理论。
CAP定理
在聊CAP定理前,我们先简单了解下分布式事务。数据库的事务我们知道。假如银行转账,转出操作和转入操作在同一个数据库中,就很好实现了,只需要在方法上增加一个@Transactional,剩下的事情数据库会帮我们做好。但是在分布式环境中,我们对比现实中的银行转账,夸行转账,不止是夸库,更是夸不同的银行系统的。在这种场景,我们需要保证ACID的特性,就是需要分布式事务解决方案了。好了,这里只是做个了结。下面说CAP定理。
CAP定理是说,在一个分布式系统中,不可能同时满足一致性(Consistency)、可用性(Availiablity)和分区容错性(Partition tolerance)这三个基本的需求。最多只能满足其中的两项。
一致性
在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保持系统的数据仍然处于一致的状态。如果对第一个节点上的数据更新成功后,第二个节点上的数据并没有得到相应的更新,那么如果从第二个节点读取数据,则获取到的就是旧数据(或者或是脏数据)。这就是典型的分布式数据不一致的场景。
可用性
可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能在有限的时间内返回结果。这里有限的时间就是系统响应时间
分区容错性
分布式系统在遇到任何网络分区故障的时候,仍然需要保证能够对外提供满足一致性和可用性的服务,除非是整个网络环境发生了故障。
网络分区是指在分布式系统中,不同节点分布在不同的子网络中,由于一些特殊的原因导致这些子网络之间出现了网络不连通的情况,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域。
上面说到,一个分布式系统不可能同时满足CAP的特性。但是,需要说明的是,分区容错性P是一个最基本的需求。因为分布式系统中的组件必然部署在不同的网络节点上,网络问题是必然会出现的一个问题。因此就剩下两种选择了,即CP和AP。系统架构需要在C和A之间寻求平衡。
BASE理论
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)的缩写。BASE是对CAP中一致性和可用性权衡的结果。是基于CAP定理逐步演化而来,其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终的一致性。
基本可用
分布式系统在出现不可预知的故障的时候,允许损失部分可用性。比如响应时间上的损失,原来0.2s返回的,现在可能需要2s返回。或者是部分功能上的损失,比如秒杀场景下部分用户可能会被引导到一个降级的页面。
软状态
是和硬状态相对的,是指允许系统中的数据存在中间的状态。但是中间的状态并不会影响系统的整体可用性。
最终一致性
是指系统中的所有数据副本,经过一定时间的同步后,最终能够达到一个一致的状态。而不是要实时保证系统数据的强一致性。
Zookeeper简介
Apache Zookeeper是由Apache Hadoop的子项目发展而来,2011年正式成为Apache的顶级项目。Zookeeper为分布式应用提供了高效且可靠的分布式协调服务。在解决分布式数据一致性方面,Zookeeper并没有使用Paxos算法(一种一致性算法),而是采用了ZAB(Zookeeper Atomic Broadcast)的一致性协议。
分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调、集群管理、Master选举、分布式锁和分布式队列等功能。Zookeeper可以保证如下分布式一致性特性。
顺序一致性
同一个客户端发起的事务请求,最终将会严格的按照其发起的顺序被应用到Zookeeper中去。
原子性
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,也么集群中的所有节点都应用了一个事务,要么都没有应用。
单一视图
无论客户端连接的是哪个Zookeeper服务器,其看到的服务端数据模型都是一致的。
可靠性
一旦服务端成功的应用了一个事务,并完成对客户端的响应那么该事务所引起的服务端状态改变将会被一直保留下来,除非有另一个事务又对其进行了变更。
实时性
Zookeeper仅仅保证在一定的时间段内,客户端最终一定能够从服务端读取到最新的数据状态。
Zookeeper数据模型
Zookeeper中有数据节点的概念,我们称之为ZNode,ZNode是Zookeeper中数据的最小单元。每个ZNode上都可以保存数据,同时还可以挂载子节点,因此就构成了一个层次化的命名空间,我们叫做树。类似于Linux文件系统中的目录树。
看图就明白了。
/是根目录,其他子节点都是从根目录开始。类似于Linux文件系统,不同的是Zookeeper的节点上是可以存储数据的。
Zookeeper事务
Zookeeper中的事务,和数据库中具有ACID特性的事务有所区别。在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,我们叫做事务操作或者更新操作,一般包括数据节点的创建和删除、数据节点内容更新和客户端会话创建与失效操作。对于每一个事务请求,Zookeeper都会为其分配一个全局唯一的事务ID,用ZXID来表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出Zookeeper处理这些更新操作请求的全局顺序。
数据节点的类型
在Zookeeper中,节点类型可以分为持久节点(PERSISTENT),临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)。在具体的节点创建中,通过组合,有下面四种组合节点类型:
持久节点
是最常见的一种节点类型,是指数据节点被创建后,就会一直存在于Zookeeper服务器中,直到有删除操作来主动删除这个节点。
持久顺序节点
和持久节点的特性是一样的,额外的特性表现在顺序性上。在Zookeeper中,每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,Zookeeper会自动为节点名加上一个数字后缀,作为一个新的完整的节点名。数字后缀的上限是整型的最大值。
临时节点
和持久节点不同的是,临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端的会话失效,那么这个节点就会被自动清理掉。这里提到的是客户端会话失效,而非TCP连接断开。
临时有序节点
临时有序节点的特性和临时节点的特性一样,只是增加了有序的特性。
状态信息
在Zookeeper客户端中,我们通过stat命令,可以查看节点的状态信息。
1[zk: localhost:2181(CONNECTED) 6] stat /test/node1
2cZxid = 0x8
3ctime = Sun May 03 21:12:24 CST 2020
4mZxid = 0xb
5mtime = Sun May 03 21:13:08 CST 2020
6pZxid = 0x8
7cversion = 0
8dataVersion = 1
9aclVersion = 0
10ephemeralOwner = 0x0
11dataLength = 5
12numChildren = 0
下面简要介绍下状态信息中各字段含义:
1cZxid: Created ZXID ,表示数据节点被创建时的事务ID。
2ctime: Created Time,表示节点被创建的时间。
3mZxid: Modified ZXID,表示该节点最后一次被更新时的事务ID。
4mtime: Modified Time,表示该节点最后一次被更新的时间。
5pZxid: 表示该节点的子节点列表最后一次被修改时的事务ID,注意只有子节点列表变更了才会变更pZxid,子节点内容的变更不会影响pZxid。
6cversion: 子节点的版本号。
7dataVersion: 数据的版本号。
8aclVersion: acl权限的版本号。
9ephemeralOwner: 创建该临时节点的会话的sessionID,如果该节点是持久节点,那么这个属性值为0。
10dataLength: 数据内容的长度。
11numChildren: 当前节点的子节点个数。
上面状态中的version字段是一种乐观锁机制的保证,保证并发更新数据的安全性。
Zookeeper的Watcher机制
在Zookeeper中,引入了Watcher机制来实现这种分布式的通知功能。Zookeeper允许客户端向服务器注册一个Watcher监听,当服务端的一个特定事件触发了这个Watcher,那么就会向客户端发送一个事件通知来实现分布式的通知功能。这个过程可以看下面的图。
Zookeeper集群节点类型
构成集群的每一台机器都有自己的角色,最典型的集群模式就是Master/Slave模式。在这种集群模式中,Master节点复杂读写操作,Slave负责提供读服务,并以异步的方式从Master同步数据。
而在Zookeeper集群中,没有使用传统的Master/Slave集群模式,而是引入了Leader、Follower和Observer三种角色。ZK集群中的所有机器通过一个Leader选举过程来选定一台机器作为“Leader”的机器。Leader服务器为客户端提供读和写服务。Follower和Observer都能够提供读服务,唯一区别在于Observer机器不参与Leader选举过程。
Leader
Leader服务器是整个zk集群工作机制中的核心,其主要工作是以下两个:
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
- 集群内部各服务器的调度者。
Follower
Follower服务器时集群状态的跟随者,其主要的工作有一下三个。
- 处理客户端非事务请求,转发事务请求给Leader服务器。
- 参与事务请求Proposal的投票。
- 参与Leader选举投票。
Observer
在zk集群中充当了一个观察者的角色,观察集群的最新状态并将这些状态变更同步过来。Observer服务器的工作原理和Follower服务器基本是一致的,对于非事务的请求都可以进行独立的处理,对于事务请求会转发给Leader处理。和Follower唯一的区别在于,Observer不参与任何形式的投票,包括事务请求Proposal投票和Leader选举投票。
Zookeeper的客户端操作
安装好Zookeeper之后,就可以使用zk自带的客户端脚本来进行操作了。进入Zookeeper的bin目录之后,直接执行如下命令:
1sh zkCli.sh
当看到出现下面这句话时,说明已经成功连接到了zkserver。
1WatchedEvent state:SyncConnected type:None path:null
进入客户端后,可以直接使用help命令看下支持哪些命令。
下面说一下在zk客户端中怎么操作节点和数据。
增加节点
使用create命令新建一个节点。命令格式如下:
1create [-s] [-e] [-c] [-t ttl] path [data] [acl]
-s是顺序特性,-e是临时节点。
比如执行下面命令
1[zk: localhost:2181(CONNECTED) 10] create /Java hello
2Created /Java
会在根节点下创建一个/Java节点,并且节点的数据内容是hello。默认创建的是持久节点。
可以继续在/Java节点下创建子节点,比如:
1[zk: localhost:2181(CONNECTED) 11] create /Java/spring 123
2Created /Java/spring
读取
使用ls命令可以看到指定节点下的所有子节点。只能看一级,不能列出子节点树。命令格式为:
1ls [-s] [-w] [-R] path
比如执行 ls / 命令,可以看下根节点下的子节点情况。
1[zk: localhost:2181(CONNECTED) 12] ls /
2[Java, aaa, happy, test, zookeeper]
可以使用get 命令获取节点的数据内容,命令格式为:
1get [-s] [-w] path
比如我们获取一下/Java节点中的数据内容:
1[zk: localhost:2181(CONNECTED) 14] get /Java
2hello
获取节点状态信息
使用stat命令可以获取节点的状态信息,命令格式如下:
1stat [-w] path
例如,我们获取一下/Java节点的状态信息
1[zk: localhost:2181(CONNECTED) 15] stat /Java
2cZxid = 0x14
3ctime = Mon May 11 21:40:38 CST 2020
4mZxid = 0x14
5mtime = Mon May 11 21:40:38 CST 2020
6pZxid = 0x15
7cversion = 1
8dataVersion = 0
9aclVersion = 0
10ephemeralOwner = 0x0
11dataLength = 5
12numChildren = 1
上面这些信息我们在之前聊到Zookeeper数据节点是有讲过是什么意思,可以回顾下。
更新
使用set命令可以更新指定节点的数据内容。命令格式如下:
1 set [-s] [-v version] path data
比如我们将/Java节点的内容更新为 world。
1[zk: localhost:2181(CONNECTED) 16] set /Java world
2[zk: localhost:2181(CONNECTED) 17] get /Java
3world
数据更新完成之后,我们可以使用stat命令,再看一下节点的状态信息,发现dataVersion已经由0变为1了。
1[zk: localhost:2181(CONNECTED) 18] stat /Java
2cZxid = 0x14
3ctime = Mon May 11 21:40:38 CST 2020
4mZxid = 0x16
5mtime = Mon May 11 21:52:19 CST 2020
6pZxid = 0x15
7cversion = 1
8dataVersion = 1
9aclVersion = 0
10ephemeralOwner = 0x0
11dataLength = 5
12numChildren = 1
因为刚才更新数据内容的操作导致数据版本升级。
删除
使用delete命令删除Zookeeper节点,用法如下:
1 delete [-v version] path
比如我们将/Java节点删除掉。不过需要注意的是,要删除的节点必须没有子节点才可以。下面直接删除/Java节点是删除不掉的。因为它下面有子节点。
1[zk: localhost:2181(CONNECTED) 20] delete /Java
2Node not empty: /Java
3[zk: localhost:2181(CONNECTED) 21] ls /
4[Java, aaa, happy, test, zookeeper]
可以删除/Java/spring节点
1[zk: localhost:2181(CONNECTED) 22] ls /Java
2[spring]
3[zk: localhost:2181(CONNECTED) 23] delete /Java/spring
4[zk: localhost:2181(CONNECTED) 24] ls /Java
5[]
ZAB协议
ZAB协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。它并不是Paxos算法的一种实现。
在Zookeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,Zookeeper实现了一种主备模式的系统架构来保证集群模中各副本之间数据的一致性。ZAB协议要满足下面一些核心需求。
- Zookeeper使用一个单一的主进程来接收和处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据状态的变更以事务Proposal的形式广播到所有副本进程上去。
- 要保证事务执行的顺序性。ZAB协议必须保证一个全局的变更系列被顺序地应用。
- 最后就是考虑到主进程(也就是Leader服务器)随时都有可能崩溃或者退出。ZAB协议要做到Leader在出现上述异常的情况下,依然能够正常的工作。
ZAB协议的核心机制
其核心机制是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理方式,即:
所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器称为Leader服务器,而余下的其他服务器是Follower(这里暂不说Observer,因为不参与投票)。Leader服务器复杂将一个客户端的事务请求转换成一个事务提议(Proposal),并将该Proposal分发给集群中所有的Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确地反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。
针对ZAB协议这里只做简要介绍,至于崩溃恢复和消息广播的具体内容不详细展开。
Zookeeper的Leader选举
前面说到在ZK集群中,有一个Leader负责处理事务请求。Leader是通过一种选举算法选出来的一个Zookeeper服务器节点。下面简要介绍下Leader选举的过程。
Leader选举有两个时机:
1、服务器启动时Leader选举
2、运行期Leader节点挂了,需要选举新的Leader。
服务器启动时的Leader选举
这里以3台机器组成的集群为例子。Server1(myid为1)、Server2(myid为2)、Server3(myid为3)。myid是在zk集群中用来标识每一台机器的,不能重复。假设Server1最先启动,然后Server2,再Server3。
1、首先每台Server会发出一个投票
由于是初始的情况,每台机器都会选自己作为Leader。每次投票的最基本信息就是服务器的myid和ZXID,我们用(myid,zxid)这种形式标识。那Server1发出的投票就是(1,0),Server2发出的投票就是(2,0),Server3(3,0)然后将各自的投票发送给集群中剩下的其他所有机器。
2、接收来自各个服务器的投票
每个服务器都会接收来自其他服务器的投票。集群中每个服务器在接收到投票后,首先会判断该投票的有效性,包括检查投票是否是本轮投票,是否来自LOOKING状态的服务器。
3、处理投票
主要是拿自己的投票和其他服务器发送过来的投票做一个PK,PK的规则如下:
- 有限检查ZXID。ZXID比较大的服务器优先作为Leader
- 如果ZXID相同的话,那么就比较myid。myid比较大的服务器作为Leader。
这里对于Server1来说,自己的投票是(1,0),收到的投票是(2,0),经过PK之后,Server1会更新自己的投票(2,0),然后将票重新发出去。而对于Server2来说,不需要更新自己的投票信息。
4、统计投票信息
每次投票后,服务器都会统计所有的投票,判断是否已经有过半的机器收到了相同的投票信息。这里对于Server1,Server2来说,都统计出集群中已经有两台机器接收了(2,0)这个投票信息。此时,就认为已经选举出了Leader
5、改变服务器状态
一旦确定了Leader,每个服务器都会更新自己的状态:如果是Follower,那么就变为FOLLOWING,如果是Leader,那么就变为LEADING。
运行期Leader节点挂了,需要选举新的Leader。
zk集群正常运行的过程中,一旦选出了Leader,那它一直就是Leader,除非这个Leader挂了,才会进入新一轮的Leader选举,也就是下面要说的这种情况。这个过程其实和启动期间Leader选择过程基本是一致的。
1、变更服务器状态
当Leader挂了之后,余下的非Observer服务器都会将自己的服务器状态变为LOOKING,然后开始进入Leader选举流程。
2、每个Server会发出一个投票
这里ZXID可能就会是不一样的,因为是运行期,每台机器上的数据同步情况可能会有差异。
3、接收来自各个服务器的投票
4、处理投票
5、统计投票
6、改变服务器的状态
Zookeeper的典型应用场景
1、使用Zookeeper可以做配置中心
可以将配置信息集中存储在zk的节点中,客户端可以注册一些监听,一旦节点数据发生变更,服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个通知之后,可以主动到服务端获取最新的数据。
2、作为注册中心
Dubbo中就是默认用zk作为注册中心的。将服务的url信息注册到zk的节点上。利用临时节点和watcher机制实现服务的动态感知。
3、作为分布式锁
分布式锁是分布式场景下保证资源同步的一种方式。在zk中,所有客户端都在一个节点(比如/lock)下调用create()方法创建临时子节点,只会有一个创建成功,这个创建成功的就认为是获取了锁。其他客户端就需要在/lock节点上注册一个子节点变更的watcher监听。