第一章
系统由集中式到分布式进化。
分布式问题
通信异常:由于网络的不可靠,导致分布式系统各个节点之间通讯伴随着网络不可用风险。
网络分区:网络异常导致部分节点之间延时过大,最终导致只有部分节点之间能正常通信,这种现象就是网络分区---俗称“脑裂”。
从ACID到CAP/BASE
单机系统很容易满足ACID,但是分布式系统对这些数据进行事务处理则面临很大挑战。
CAP
一致性(C):数据在多个副本之间能够保持一致的特性。
可用性(A):服务一直处于可用的状态,对于用户的每个操作总能在有限的时间内返回结果。
分区容错性(P):分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境发生故障。
分区容错性是分布式系统的基本需要。
BASE
BASE是 Basically Avaiable(基本可用)、Soft State(软状态)、Eventually consistent(最终一致性)的简称。是基于CAP理论演化来的,核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式使系统达到最终一致性。
基本可用:系统在出现不可预知故障的时候,允许损失部分可用性(响应时间损失、功能上的损失等)。
软状态:允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
最终一致性:指系统所有的数据副本,在经过一段时间的同步后,最终都能达到一个一致的状态。
第二章
由数据一致性问题,提出一致性协议与算法。阐述了两阶段提交协议和三阶段提交协议,重点讲述Paxos一致性算法。
两阶段提交协议
简单来讲,两阶段提交将一个事务的处理过程分为投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式。
事务提交与事务中断过程如图所示:
优点
原理简单,实现方便。
缺点
同步阻塞,单点问题,脑裂,太过保守。
三阶段提交协议
三阶段提交协议将二阶段提交协议的“提交事务请求”过程一分为二,形成了由Cancommit,Precommit,Docommit三个阶段。
过程如下:
优点
降低了参与者的阻塞范围,并且能在单点故障后继续达成一致。
缺点
如果参与者收到了preCommit消息后出现了网络分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,必然会造成数据的不一致。
Paxos算法
是一种基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
算法解析
参考了一篇文章,写的还是挺容易理解的。
Paxos的主要的两个组件:
Proposer
提议发起者,处理客户端请求,将客户端的请求发送到集群中,以便决定这个值是否可以被批准。
Acceptor
提议批准者,负责处理接收到的提议,他们的回复就是一次投票。会存储一些状态来决定是否接收一个值。
规则如下:
1 一个Acceptor必须接受它收到的第一个提案。
2 一个提案被选定需要被半数以上的Acceptor接受。
3 如果某个value为v的提案被选定了,那么每个编号更高的被选定提案的value必须也是v。
一个提案被选定要经过第一阶段(prepare),如果一半Acceptor同意提案,Proposer才能第二阶段(Accept)。如果一半Acceptor同意提案,提案才能被选定。
第一阶段(prepare)
1)提议者获取一个提案号(proposal number)n;
2)提议者向所有节点广播prepare(n)请求;
3)接收者接收消息此时要分几种情况处理:
a.如果接收者还没有收到任何提议,记录minProposal.并返回提议者OK,认可该提案。
b.如果接收者已有minProposal,比较n,和minProposal,如果n>minPno,表示有更新的提案,更新minProposal=n。
c.如果此时已经有一个认定值(accptedValue),返回提议者已确定的(acceptedProposal,acceptedValue)。
d.如果接收者已有minProposal,比较n,和minProposal,如果n<minProposal.拒绝并且返回minProposal。
4)提议者根据请求返回的响应做处理。只有收到超过一半接收者响应才能发起第二阶段请求。同样也分为以下几种情况:
a.超过一半接收者返回已确定的提案(acceptedProposal,acceptedValue),表示有认可的提议,保存最高acceptedProposal编号的acceptedValue到本地。
b.超过一半接收者同意提议者的提案。
c.未得到一半接收者同意提案,这种请求跳转1,对n进行累加,重新prepare。
第二阶段(Accept)
5)提议者广播accept(n,value)到所有提议者节点;
6) 接收者比较n和minProposal,如果n>=minProposal,则acceptedProposal=minProposal=n,acceptedValue=value,本地持久化后,返回; 否则,拒绝并且返回minProposal。
7) 提议者接收到过半数请求后,如果发现有返回值>n,表示有更新的提议,跳转1(重新发起提议);否则value达成一致,提案被选定
以下通过实例说明:
示例一:
因为P1收到一半Acceptor的响应,所以发送第二阶段请求。
示例二:
P1发送提议【1,a】至A1,A2 , A1,A2因为此时是第一个提议,所以接收提议,返回OK。
P2发送提议【2,b】至A1,A2 , 因为【2,b】大, A1,A2更新本地minProposal 返回OK。
所以拒绝提案,并返回【2,b】给P1,表示有更新提案。
P1更新提案【3,a】继续第一阶段prepare请求。
但是,如果此时P2在进行accept请求【2,b】发现又有更新请求。 如此反复就会出现活锁的情况,解决的方法就是提出主Proposer,并规定只有主Proposer可以提出议案。
这样,只要主Proposer和过半的Acceptor能够正常进行网络通信,但凡主Proposer提出一个编号更高的提案,该提案终将会被批准。整套Paxos算法流程就能保持活性。
第三章
本章主要通过对Google Chubby 和 Hypertable这两款经典的分布式产品中的Paxos算法应用的介绍,阐述了Paxos算法在实际工业实践的应用。在此,不过多赘述。
第四章
本章首先对Zookeeper进行一个整体上的介绍,包括设计目标、由来及基本概念,重点介绍Zookeeper中的ZAB一致性协议。
Zookeeper是什么
是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅,负载均衡,命名服务,分布式协调/通知,集群管理,master选举,分布式锁和分布式队列等功能。
Zookeeper可以保证如下分布式一致性特性:
顺序一致性
从同一个客户端发起的事务请求,最终将会严格按照发起的顺序被应用到Zookeeper中。
原子性
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的。
单一视图
无论客户端链接的是那个Zookeeper服务器,看到的服务端数据模型都是一致的。
可靠性
一旦服务端成功的应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
实时性
Zookeeper仅仅保证在一定的时间段内,客户端最终一定能够从服务端读取到最新的数据状态。
ZAB协议
很多读者认为Zookeeper是Paxos算法的一个实现,事实上,并没有完全采用Paxos算法,而是使用了ZAB协议。ZAB协议是为Zookeeper设计的一种支持崩溃恢复的原子广播协议。
ZAB协议主要包含两种基本模式:崩溃恢复,消息广播。
消息广播
ZAB协议的消息广播过程是使用一个原子广播协议,类似于二阶段提交。针对客户端的请求,Leader服务器会为其生成对应的事务Proposal,并将其发送给集群中其余所有机器,然后在分别收集各自的票选,最后进行事务提交。
如下图所示:
但与二阶段提交不同的是,ZAB在二阶段提交过程中,移除了中断逻辑,所有的Follower要么正常反馈Leader提出的Proposal,要么抛弃Leader。同时,意味着我们可以在过半的Follower服务器已经反馈ACK之后就可以提交事务,不需要等待所有的Follower都反馈。整个消息广播协议是基于具有FIFO特性的TCP协议进行网络通讯的,因此保证了消息的有序性。
但是,这样的二阶段提交不能保证Leader崩溃后数据的一致性,因此还需要崩溃恢复模式。
崩溃恢复
在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader服务器。因此,ZAB协议需要一个高效且可靠的Leader选举算法,从而确保能快速的选举出新的Leader。同时,Leader选举算法不仅仅需要让Leader自己知道其自身被选举为Leader,还需要让集群中的其他机器知道。
数据同步
所有正常运行的服务器,要么成为Leader,要么成为Follower并和Leader保持同步。 Leader服务器需要确保所有的Follower服务器能够接收到每一条事务Proposal,并且能 够正确地将所有已经提交了的事务Proposal应用到内存数据库中去。具体的,Leader服 务器会为每一个Follower服务器都准备一个队列,并将那些没有被各Follower服务器同 步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每一个Proposal消息 后面紧接着再发送一个Commit消息,以表示该事务已经被提交。等到Follower服务器 将所有其尚未冋步的事务Proposal都从Leader服务器上同步过来并成功应用到本地数 据库中后,Leader服务器就会将该Follower服务器加入到真正的可用Follower列表中, 并开始之后的其他流程。
ZAB和Paxos算法的联系和区别
联系:
- 两者都存在一个类似与Leader进程的角色,由其负责协调多个Follower进程的运行。
- Leader进行都会等待超半数的Follower做出正确反馈后才提交事务。
- 在ZAB协议中,每个Proposal中都包含一个epoch值用来代表当前leader的周期,在Paxos中,同样有这样的标识,名字改为Ballot。
区别:
两者设计目标不太一样,ZAB用于构建一个分布式数据主备系统,而Paxos用于构建一个分布式一致性状态机系统。
第五章
本章主要介绍如何使用Zookeeper,包括部署与运行,以及Java客户端的调用,都是一些比较基础的使用方法,在此不过多赘述。
第六章
本章主要介绍Zookeeper的典型应用场景以及实现。
数据发布、订阅
数据发布、订阅系统,即所谓的配置中心,顾名思义就是发布者将数据发布到Zookeeper的一个或一系列节点上,供订阅者进行数据订阅,从而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布、订阅系统一般有两种设计模式,分别是推(PUSH)模式和拉(PULL)模式。在推模式中,服务器主动将数据更新发送给所有订阅的客户端;而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮训拉取的方式。
Zookeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Wather事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
Zookeeper实现配置管理的几个步骤:
配置存储
选取一个数据节点用于配置的存储,例如 /app1/database_config(以下简称“配置节点”),如下图:
我们需要将配置信息写入该数据节点中,例如:
master0.jdbc.driverclass=com.mysql.jdbc.Driver
master0.jdbc.url=jdbc:mysql://xx.xx.xx.xx:3306/xx
master0.jdbc.username=xxxxxx
master0.jdbc.password=xxxxxx
slave0.jdbc.driverclass=com.mysql.jdbc.Driver
slave0.jdbc.url=jdbc:mysql://xx.xx.xx.xx:3306/xx
slave0.jdbc.username=xxxxxx
slave0.jdbc.password=xxxxxx
配置获取 集群中每台机器在启动初始化阶段,首先会从这个数据节点中读取数据库的信息,同时,客户端还需要在该配置节点上注册一个数据变更的Watcher监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知。
配置变更 系统运行过程中,可能出现需要进行数据库切换的情况,这个时候就需要进行配置变更。我们只需要对节点上的内容进行更新,Zookeeper就能够帮我们将数据变更通知发送到各个客户端。
命名服务
分布式服务的名字类似于数据库中的唯一主键。利用Zookeeper节点创建的API可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。利用这个特性,可以借助Zookeeper来生成全局唯一ID。
如图所示,会有以下步骤:
1.所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接口创建一个顺序节点,例如创建“job-”节点。
2.节点创建完毕后,create()接口返回完整节点名,例如“job-00000001”。
3.客户端拿到这个返回值后,拼接上type,例如“type2-job-0000001”,这就可以作为全局唯一ID。
分布式锁
使用Zookeeper来实现分布式锁,主要实现排它锁和共享锁两种。
排它锁
又称写锁或独占锁,核心是保证当前有且只有一个事务获得锁。
定义
通过在Zookeeper上建立一个数据节点,例如/exciusive_lock/lock节点就被定义为一个锁,如图:
获取
所有客户端试图创建临时节点,创建成功的任务该客户端获取了锁。同时,没有获取到锁的客户端在该节点上注册一个子节点变更的Watcher监听。
释放
因为是个临时节点,因此在两种情况下会删除此节点:
1.获取锁的客户端宕机
2.获取锁的客户端完成逻辑,主动删除
无论这两种哪种方式释放锁,都会通知其他注册了的监听的客户端去重新获取锁。
因此,整个流程如下图所示:
共享锁
又称读锁,如果事务1对对象A加锁,那么其他的事务只能对A读,不能写。
定义
利用Zookeeper创建临时节点,名称类似为“/shared_lock/[Hostname]-请求类型-序号”。
如下图:
获取
在需要获取锁时,所有请求都在该节点下创建临时有序节点,如果是读的话就创建请求类型为R的,如果是写的就创建请求类型为W的。
锁的获取顺序
由于共享锁的定义,可以同时读,但是写时不能读也不能写的。锁的获取顺序分为如下四步:
1.创建节点后,获取/shared_lock节点下的所有子节点,并注册监听。
2.确定自己的序号在所有节点中的顺序。
3.对于读请求:
如果没有比自己小的节点,或是比自己小的节点都是读节点的话,就获得锁。
如果比自己小的节点中有写节点,则继续等待。
对于写请求:
如果自己不是最小的节点,则继续等。
4.接收到监听的通知,继续1步骤。
释放
释放逻辑和排它锁一致,不多赘述。
整体流程如图:
问题:羊群效应
上述共享锁的实现能满足10台机器以内的集群模型,如果规模扩大之后,会产生什么问题呢?
上述共享锁在竞争过程中,存在大量的“watcher通知”和“子节点列表获取”两个重复操作,并且绝大数的运行结果都是判断自己不是最小的节点,从而继续等待。如果在规模较大的集群中,如果同一时间有多个节点对应的客户端完成事务或是事务中断引起节点消失,Zookeeper服务器就会在短时间向客户端发送大量的通知---这就是羊群效应。
升级版共享锁
这里只需要改动:每个锁竞争者,只需关注/shared_lock节点下序号比自己小的节点是否存在即可,具体实现如下:
1.客户端调用create()方法创建一个类似于“/shared_lock/[hostname]-请求类型-序号”的**临时有序节点**。
2.客户端调用getChildren()接口来获取所有已经创建的子节点列表,注意,这里不注册任何Watcher。
3.如果无法获取共享锁,那么就调用exist()来对比自己小的那个节点注册Watcher。
注意,这里比自己小是个笼统的说法,具体对于读请求和写请求不一样。
读请求:向比自己序号小的最后一个写请求节点注册。
写请求:向比自己小的最后一个节点注册。
4.等待Watcher通知,继续进入步骤2。
流程如图所示: