..
- 要想数据高可用,就得写多份数据
- 写多分数据就会导致数据一致性问题
- 数据一致性问题会引起性能问题
2.一致性模型
- 弱一致性
- 最终一致性(一段时间达到一致性)
- 强一致
- 1、2 异步冗余;3是同步冗余
3. 扩展服务的方案
- 数据分区: uid % 16
- 数据镜像:让多有的服务器都有相同的数据,提供相当的服务(冗余存储,一般3份为好)
4.两种方案的事务问题
- A向B汇钱,两个用户不在一个服务器上
- 镜像:在不同的服务器上对同一数据的写操作如何保证一致性。
5. 解决一致性事务问题的技术
1. Master -Slave
- 读写请求由Master负责
- 写请求写到Master后,由Master同步到Slave上
- 由Master push or Slave pull
- 通常是由Slave 周期性来pull,所以是最终一致性
问题: 若在 pull 周期内(不是期间?),master挂掉,那么会导致这个时间片内的数据丢失
- 若不想让数据丢掉,Slave 只能成为 ReadOnly方式等Master恢复
- 若容忍数据丢失,可以让 Slave代替Master工作
如何保证强一致性?
- Master 写操作,写完成功后,再写 Slave,两者成功后返回成功。若 Slave失败,两种方法
- 标记 Slave 不可用报错,并继续服务(等恢复后,再同步Master的数据,多个Slave少了一个而已)
- 回滚自己并返回失败
2. Master-Master
- 数据同步一般是通过 Master 间的异步完成,所以是最终一致
- 好处: 一台Master挂掉,另外一台照样可以提供读写服务。当数据没有被赋值到别的Master上时,数据会丢失。
- 对同一数据的处理问题:Dynamo的Vector Clock的设计(记录数据的版本号和修改者),当数据发生冲突时,要开发者自己来处理
3.两阶段提交 Two Phase Commit _ 2PC
- 第一阶段:针对准备工作
- 协调者问所有节点是否可以执行提交
- 参与者开始事务,执行准备工作:锁定资源(获取锁操作)
- 参与者响应协调者,如果事务的准备工作成功,则回应"可以提交",否则,拒绝提交
- 第二阶段:
- 若都响应可以提交,则协调者项多有参与者发送正式提交的命令(更新值),参与者完成正式提交,释放资源,回应完成。协调者收到所有节点的完成响应后结束这个全局事务.。若参与者回应拒绝提交,则协调者向所有的参与者发送回滚操作,并释放资源,当收到全部节点的回滚回应后,取消全局事务
- 存在的问题:若一个没提交,就会进行回滚
- 第一阶段:若消息的传递未接收到,则需要协调者作超时处理,要么当做失败,要么重载
- 第二阶段:若参与者的回应超时,要么重试,要么把那个参与者即为问题节点,提出整个集群
- 在第二阶段中,参与者未收到协调者的指示(也许协调者挂掉),则所有参与者会进入“不知所措” 的状态(但是已经锁定了资源),所以引入了三段提交
4. 三段提交:把二段提交的第一阶段 break 成了两段
- 询问
- 锁定资源(获取锁)
- 提交
- 核心理念:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁定
- 好处:当发生了失败或超时时,三段提交可以继续把状态变为Commit 状态,而二段提交则不知所措?
5. Raxos 算法(少数服从多数)
- 解决的问题:在一个可能发生异常的分布式系统中如何就某个值达成一致,让整个集群的节点对某个值的变更达成一致
- 任何一个节点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的节点同意(所以节点数总是单数)—— 版本标记。虽然一致性,但是只能对一个操作进行操作啊??
- 当一个Server接收到比当前版本号小的提案时,则拒绝。当收到比当前大的版本号的提案时,则锁定资源,进行修改,返回OK. 也就是说收到超过一半的最大版本的提案才算成功。
核心思想:
- 在抢占式访问权的基础上引入多个acceptor,也就是说当一个版本号更大的提案可以剥夺版本号已经获取的锁。
- 后者认同前者的原则:
- 在肯定旧epoch 无法生成确定性取值时,新的 epoch 会提交自己的valu
- 一旦 旧epoch形成确定性取值,新的 epoch肯定可以获取到此取值,并且会认同此取值,不会被破坏。
步骤
- P1 请求Acceptor的 #1,Acceptor 这时并没有其他线程获取到锁,所以把锁交给 P1,并返回这时 #1 的值为null
- 然后 P1 向 第一个 Acceptor 提交 #1 的值,Acceptor 接受并返回 OK
- 这个时候,P2向Acceptor请求#1上的锁,因为版本号更大,所以直接抢占了 P1 的锁。这时 Acceptor 返回了 OK并且返回了 #1 的值
- 这时 P1 P向 后面两个 Acceptor 提交 #1 的值,但是由于中间的那个Acceptor 版本号已经更改为 2 了,所以拒绝P1。第三个 Acceptor 接受了,并且返回了 OK
- 由于后者认同前者的原则,这时 P1 已经形成确定性取值了 V1 了,这时新的 P2 会认同此取值,而不是提交自己的取值。所以,P2会选择最新的那个取值 也就是V1 进行提交。这时Acceptor 返回 OK
6.ZAB 协议 ( Zookeeper Atomic Broadcast) 原子广播协议:保证了发给各副本的消息顺序相同
定义:原子广播协议 ZAB 是一致性协议,Zookeeper 把其作为数据一致性的算法。ZAB 是在 Paxos 算法基础上进行扩展而来的。Zookeeper 使用单一主进程 Leader用于处理客户端所有事务请求,采用 ZAB 协议将服务器状态以事务形式广播到所有 Follower 上,由于事务间可能存在着依赖关系,ZAB协议保证 Leader 广播的变更序列被顺序的处理,一个状态被处理那么它所依赖的状态也已经提前被处理
核心思想:保证任意时刻只有一个节点是Leader,所有更新事务由Leader发起去更新所有副本 Follower,更新时用的是 两段提交协议,只要多数节点 prepare 成功,就通知他们commit。各个follower 要按当初 leader 让他们 prepare 的顺序来 apply 事务
协议状态
- Looking:系统刚启动时 或者 Leader 崩溃后正处于选举状态
- Following:Follower 节点所处的状态,Follower与 Leader处于数据同步状态
- Leading:Leader 所处状态,当前集群中有一个 Leader 为主进程
- ZooKeeper启动时所有节点初始状态为Looking,这时集群会尝试选举出一个Leader节点,选举出的Leader节点切换为Leading状态;当节点发现集群中已经选举出Leader则该节点会切换到Following状态,然后和Leader节点保持同步;当Follower节点与Leader失去联系时Follower节点则会切换到Looking状态,开始新一轮选举;在ZooKeeper的整个生命周期中每个节点都会在Looking、Following、Leading状态间不断转换。
- 选举出Leader节点后 ZAB 进入原子广播阶段,这时Leader为和自己同步每个节点 Follower 创建一个操作序列,一个时期一个 Follower 只能和一个Leader保持同步
阶段
- Election: 在 Looking状态中选举出 Leader节点,Leader的LastZXID总是最新的(只有lastZXID的节点才有资格成为Leade,这种情况下选举出来的Leader总有最新的事务日志)。在选举的过程中会对每个Follower节点的ZXID进行对比只有highestZXID的Follower才可能当选Leader
- 每个Follower都向其他节点发送选自身为Leader的Vote投票请求,等待回复;
- Follower接受到的Vote如果比自身的大(ZXID更新)时则投票,并更新自身的Vote,否则拒绝投票;
- 每个Follower中维护着一个投票记录表,当某个节点收到过半的投票时,结束投票并把该Follower选为Leader,投票结束;
- Discovery:Follower 节点向准 Leader推送 FollwerInfo,该信息包含了上一周期的epoch,接受准 Leader 的 NEWLEADER 指令
- Sync:将 Follower 与 Leader的数据进行同步,由Leader发起同步指令,最终保持数据的一致性
- Broadcast:Leader广播 Proposal 与 Commit,Follower 接受 Proposal 与 commit。因为一个时刻只有一个Leader节点,若是更新请求,只能由Leader节点执行(若连到的是 Follower 节点,则需转发到Leader节点执行;读请求可以从Follower 上读取,若是要最新的数据,则还是需要在 Leader上读取)
- 消息广播使用了TCP协议进行通讯所有保证了接受和发送事务的顺序性。广播消息时Leader节点为每个事务Proposal分配一个全局递增的ZXID(事务ID),每个事务Proposal都按照ZXID顺序来处理(Paxos 保证不了)
- Leader节点为每一个Follower节点分配一个队列按事务ZXID顺序放入到队列中,且根据队列的规则FIFO来进行事务的发送。
- Recovery :根据Leader的事务日志对Follower 节点数据进行同步更新
- 同步策略:
- SNAP :如果Follower数据太老,Leader将发送快照SNAP指令给Follower同步数据;
- DIFF :Leader发送从Follolwer.lastZXID到Leader.lastZXID议案的DIFF指令给Follower同步数据;
- TRUNC :当Follower.lastZXID比Leader.lastZXID大时,Leader发送从Leader.lastZXID到Follower.lastZXID的TRUNC指令让Follower丢弃该段数据;(当老Leader在Commit前挂掉,但是已提交到本地)
- Follower将所有事务都同步完成后Leader会把该节点添加到可用Follower列表中;
- Follower接收Leader的NEWLEADER指令,如果该指令中epoch比当前Follower的epoch小那么Follower转到Election阶段
- 同步策略:
7. Raft 算法
- Raft 算法也是一种少数服从多数的算法,在任何时候一个服务器可以扮演以下角色之一:
- Leader:负责 Client 交互 和 log 复制,同一时刻系统中最多存在一个
- Follower:被动响应请求 RPC,从不主动发起请求 RPC
- Candidate : 由Follower 向Leader转换的中间状态
- 在选举Leader的过程中,是有时间限制的,raft 将时间分为一个个 Term,可以认为是“逻辑时间”:
- 每个 Term中至多存在1个 Leader
- 某些 Term由于不止一个得到的票数一样,就会选举失败,不存在Leader。则会出现 Split Vote ,再由候选者发出邀票
- 每个 Server 本地维护 currentTerm
- 选举过程:
- 自增 CurrentTerm,由Follower 转换为 Candidate,设置 votedFor 为自身,并行发起 RequestVote RPC,不断重试,直至满足下列条件之一为止:
- 获得超过半数的Server的投票,转换为 Leader,广播 HeatBeat
- 接收到 合法 Leader 的 AppendEnties RPC,转换为Follower
- 选举超时,没有 Server选举成功,自增 currentTerm ,重新选举
- 当Candidate 在等待投票结果的过程中,可能会接收到来自其他Leader的 AppendEntries RPC ,如果该 Leader 的 Term 不小于本地的 Current Term,则认可该Leader身份的合法性,主动降级为Follower,反之,则维持 candida 身份继续等待投票结果
- Candidate 既没有选举成功,也没有收到其他 Leader 的 RPC (多个节点同时发起选举,最终每个 Candidate都将超时),为了减少冲突,采取随机退让策略,每个 Candidate 重启选举定时器
- 自增 CurrentTerm,由Follower 转换为 Candidate,设置 votedFor 为自身,并行发起 RequestVote RPC,不断重试,直至满足下列条件之一为止:
- 日志更新问题:如果在日志复制过程中,发生了网络分区或者网络通信故障,使得Leader不能访问大多数Follwers了,那么Leader只能正常更新它能访问的那些Follower服务器,而大多数的服务器Follower因为没有了Leader,他们重新选举一个候选者作为Leader,然后这个Leader作为代表于外界打交道,如果外界要求其添加新的日志,这个新的Leader就按上述步骤通知大多数Followers,如果这时网络故障修复了,那么原先的Leader就变成Follower,在失联阶段这个老Leader的任何更新都不能算commit,都回滚,接受新的Leader的新的更新。
- 流程:
- Client 发送command 命令给 Leader
- Leader追加日志项,等待 commit 更新本地状态机,最终响应 Client
- 若 Client超时,则不断重试,直到收到响应为止(重发 command,可能被执行多次,在被执行但是由于网络通信问题未收到响应)
- 解决办法:Client 赋予每个 Command唯一标识,Leader在接收 command 之前首先检查本地log
9. paxos 算法与 raft 算法的差异
- raft强调是唯一leader的协议,此leader至高无上
- raft:新选举出来的leader拥有全部提交的日志,而 paxos 需要额外的流程从其他节点获取已经被提交的日志,它允许日志有空洞
- 相同点:得到大多数的赞成,这个 entries 就会定下来,最终所有节点都会赞成
NWR模型
- N: N个备份
- W:要写入至少 w 份才认为成功
- R : 至少读取 R 个备份
- W+ R > N ——> R > N - W(未更新成功的) ,代表每次读取,都至少读取到一个最新的版本(更新成功的),从而不会读到一份旧数据
- 问题:并非强一致性,会出现一些节点上的数据并不是最新版本,但却进行了最新的操作
- 版本冲突问题:矢量钟 Vector Clock : 谁更新的我,我的版本号是什么(对于同一个操作者的同一操作,版本号递增)
参考资料:
http://www.tuicool.com/articles/IfQR3u3
http://blog.csdn.net/chen77716/article/details/7309915
http://www.infoq.com/cn/articles/distributed-system-transaction-processing/
文章转自:
https://blog.csdn.net/followmyinclinations/article/details/52870418