20201220 分布式理论、架构设计(自定义RPC) - 拉勾教育
分布式理论
分布式架构系统回顾
- 分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统
分布式与集群的区别:
- 集群:多个人在一起作同样的事 。
- 分布式 :多个人在一起作不同的事 。
分布式系统的特点:
- 分布性
- 对等性
- 并发性
- 缺乏全局时钟
- 故障总是会发生
阿里巴巴发起的 "去 IOE" 运动(IOE 指的是 IBM 小型机、Oracle 数据库、EMC 的高端存储)。
为什么要去 IOE ?
- 升级单机处理能力的性价比越来越低
- 单机处理能力存在瓶颈
- 稳定性和可用性这两个指标很难达到
分布式系统的演变:
- 单应用架构
- 应用服务器与数据库服务器分离
- 应用服务器集群
- 应用服务器负载均衡
- 数据库主从复制,读写分离
- 添加搜索引擎缓解读库压力
- 添加缓存集群缓解读库压力
- 数据库拆分,水平/垂直 分库分表
- 应用拆分
- 应用服务化
分布式系统面临的问题:
- 通信异常
- 网络分区
- 网络之间出现了网络不连通,但各个子网络的内部网络是正常的,分布式系统出现局部小集群
- 节点故障
- 三态
- 成功、失败、超时
- 超时的两种情况:
- 由于网络原因,该请求并没有被成功的发送到接收方,而是在发送过程就发生了丢失现象。
- 该请求成功的被接收方接收后,并进行了处理,但在响应反馈给发送方过程中,发生了消息丢失现象。
一致性
-
分布式数据一致性,指的是数据在多份副本中存储时,各副本中的数据是一致的。
-
副本一致性
- 无法找到一种能够满足分布式系统内所有系统属性的分布式一致性解决方案
- 如何既保证数据的一致性,同时又不影响系统运行的性能,是每一个分布式系统都需要重点考虑和权衡的。于是,一致性级别由此诞生
一致性级别:
-
强一致性
这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。但是强一致性很难实现。
-
弱一致性
这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。
- 读写一致性
- 用户读取自己写入结果的一致性,保证用户永远能够第一时间看到自己更新的内容
- 单调读一致性
- 本次读到的数据不能比上次读到的旧。
- 由于主从节点更新数据的时间不一致,导致用户在不停地刷新的时候,有时候能刷出来,再次刷新之后会发现数据不见了,再刷新又可能再刷出来,就好像遇见灵异事件一样
- 因果一致性
- 如果节点 A 在更新完某个数据后通知了节点 B,那么节点 B 之后对该数据的访问和修改都是基于 A 更新后的值。于此同时,和节点 A 无因果关系的节点 C 的数据访问则没有这样的限制
- 最终一致性
- 最终一致性是所有分布式一致性模型当中最弱的。
- 可以认为是没有任何优化的“最”弱一致性,它的意思是说,我不考虑所有的中间状态的影响,只保证当没有新的更新之后,经过一段时间之后,最终系统内所有副本的数据是正确的。
- 它最大程度上保证了系统的并发能力,也因此,在高并发的场景下,它也是使用最广的一致性模型。
- 读写一致性
CAP定理
- CAP 理论含义是,一个分布式系统不可能同时满足一致性(C: Consistency)(这里是强一致性),可用性(A: Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的2个。
选项 | 描述 |
---|---|
C 一致性 | 分布式系统当中的一致性指的是所有节点的数据一致,或者说是所有副本的数据一致 |
A 可用性 | Reads and writes always succeed. 也就是说系统一直可用,而且服务一直保持正常 |
P 分区容错性 | 系统在遇到一些节点或者网络分区故障的时候,仍然能够提供满足一致性和可用性的服务 |
- 舍弃 A (可用性),保留 CP (一致性和分区容错性)
- 一个系统保证了一致性和分区容错性,舍弃可用性。也就是说在极端情况下,允许出现系统无法访问的情况出现,这个时候往往会牺牲用户体验,让用户保持等待,一直到系统数据一致了之后,再恢复服务。
- 舍弃 C (一致性),保留 AP (可用性和分区容错性)
- 这种是大部分的分布式系统的设计,保证高可用和分区容错,但是会牺牲一致性。
- 舍弃 P (分区容错性),保留 CA (一致性和可用性)
- 如果要舍弃 P ,那么就是要舍弃分布式系统, CAP 也就无从谈起了。可以说 P 是分布式系统的前提,所以这种情况是不存在的。
BASE 理论
-
BASE :Basically Available (基本可用), Soft state (软状态),和 Eventually consistent (最终一致性)三个短语的缩写,来自 ebay 的架构师提出
-
BASE 是对 CAP 中一致性和可用性权衡的结果
-
BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
-
Basically Available (基本可用)
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性
-
响应时间上的损失
- 响应时间变长
-
功能上的损失
- 服务降级
-
Soft state (软状态)
- 什么是软状态呢?相对于一致性,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。
- 软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本之间进行数据同步的过程中存在延迟。
-
Eventually consistent (最终一致性)
- 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
分布式事务
事务有4个非常重要的特性:
-
Atomicity(原子性)
-
Consistency(一致性)
-
Isolation(隔离性)
-
Durablity(持久性)
-
分布式事务从实质上看与数据库事务的概念是一致的,既然是事务也就需要满足事务的基本特性(ACID),只是分布式事务相对于本地事务而言其表现形式有很大的不同
一致性协议 2PC
- 2PC ( Two-Phase Commit 缩写)即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(Commit phase),2 是指两个阶段,P 是指准备阶段,C 是指提交阶段。
两个阶段过程:
- 准备阶段(Prepare phase):事务管理器给每个参与者发送 Prepare 消息,每个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志,此时事务没有提交。 (Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件)
- 提交阶段( commit phase ):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚( Rollback )消息;否则,发送提交( Commit )消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
- 二阶段提交就做了2个事情:投票,执行
2PC 执行流程 :成功执行事务事务提交流程
-
阶段一(各个参与者进行投票是否让事务进行):
- 事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
-
执行事务 (写本地的 Undo/Redo 日志)
-
各参与者向协调者反馈事务询问的响应
-
阶段二:
-
发送提交请求:
协调者向所有参与者发出 commit 请求。
-
事务提交:
参与者收到 commit 请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
-
反馈事务提交结果:
参与者在完成事务提交之后,向协调者发送 ACK 信息。
-
完成事务:
协调者接收到所有参与者反馈的 ACK 信息后,完成事务。
-
ACK 确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。
2PC 执行流程 :中断事务流程
- 假如任何一个参与者向协调者反馈了 No 响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务
-
阶段一(各个参与者进行投票是否让事务进行):
-
事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
-
执行事务 (写本地的 Undo/Redo 日志)
-
各参与者向协调者反馈事务询问的响应
-
-
阶段二:
-
发送回滚请求:
协调者向所有参与者发出 Rollback 请求。
-
事务回滚:
参与者接收到 Rollback 请求后,会利用其在阶段一中记录的 Undo 信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
-
反馈事务回滚结果:
参与者在完成事务回滚之后,向协调者发送 ACK 信息。
-
中断事务:
协调者接收到所有参与者反馈的 ACK 信息后,完成事务中断。
-
2PC 优点缺点:
-
优点:
- 原理简单,实现方便
-
缺点:
-
同步阻塞
二阶段提交协议存在最明显也是最大的一个问题就是同步阻塞,在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,无法进行其他操作。这种同步阻塞极大的限制了分布式系统的性能。
-
单点问题
协调者在整个二阶段提交过程中很重要,如果协调者在提交阶段出现问题,那么整个流程将无法运转,更重要的是:其他参与者将会处于一直锁定事务资源的状态中,而无法继续完成事务操作。
-
数据不一致
假设当协调者向所有的参与者发送 commit 请求之后,发生了局部网络异常或者是协调者在尚未发送完所有 commit 请求之前自身发生了崩溃,导致最终只有部分参与者收到了 commit 请求。这将导致严重的数据不一致问题。
-
过于保守
如果在二阶段提交的提交询问阶段中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事务,显然,这种策略过于保守。换句话说,二阶段提交协议没有设计较为完善的容错机制,任意一个节点失败都会导致整个事务的失败。
-
一致性协议 3PC
3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求” 过程一分为二,共形成了由 CanCommit、PreCommit 和 doCommit 三个阶段组成的事务处理协议。
阶段一:canCommit
-
事务询问
协调者向所有的参与者发送一个包含事务内容的 canCommit 请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
-
各参与者向协调者反馈事务询问的响应
参与者在接收到来自协调者的包含了事务内容的 canCommit 请求后,正常情况下,如果自身认为可以顺利执行事务,则反馈 Yes 响应,并进入预备状态,否则反馈 No 响应。
阶段二:preCommit
协调者在得到所有参与者的响应之后,会根据结果有2种执行操作的情况:执行事务预提交,或者中断事务
-
假如所有参与反馈的都是 Yes,那么就会执行事务预提交。
-
发送预提交请求:
协调者向所有参与者节点发出 preCommit 请求,并进入 prepared 阶段。
-
事务预提交:
参与者接收到 preCommit 请求后,会执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中。
-
各参与者向协调者反馈事务执行的结果:
若参与者成功执行了事务操作,那么反馈 ACK
-
-
若任一参与者反馈了 No 响应,或者在等待超时后,协调者尚无法接收到所有参与者反馈,则中断事务
-
发送中断请求:
协调者向所有参与者发出 abort 请求。
-
中断事务:
无论是收到来自协调者的 abort 请求或者等待协调者请求过程中超时,参与者都会中断事务
-
阶段三:doCommit
该阶段做真正的事务提交或者完成事务回滚,所以就会出现两种情况:
-
执行事务提交
-
发送提交请求:
进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的 ACK 响应,那么他将从预提交状态转化为提交状态,并向所有的参与者发送 doCommit 请求。
-
事务提交:
参与者接收到 doCommit 请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行过程中占用的事务资源。
-
反馈事务提交结果:
参与者在完成事务提交后,向协调者发送 ACK 响应。
-
完成事务:
协调者接收到所有参与者反馈的 ACK 消息后,完成事务。
-
-
中断事务
- 发送中断请求:协调者向所有的参与者节点发送 abort 请求。
- 事务回滚:参与者收到 abort 请求后,会根据记录的 Undo 信息来执行事务回滚,并在完成回滚之后释放整个事务执行期间占用的资源。
- 反馈事务回滚结果:参与者在完成事务回滚后,向协调者发送 ACK 消息。
- 中断事务:协调者接收到所有参与者反馈的 ACK 消息后,中断事务。
注意:一旦进入阶段三,可能会出现 2 种故障:
- 协调者出现问题
- 协调者和参与者之间的网络故障
如果出现了任何一种情况,最终都会导致参与者无法收到 doCommit 请求或者 abort 请求,针对这种情况,参与者都会在等待超时之后,继续进行事务提交
2PC 对比 3PC
- 首先对于协调者和参与者都设置了超时机制(在 2PC 中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题:因为参与者自身拥有超时机制,会在超时后,自动进行本地 commit 从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
- 通过 CanCommit 、 PreCommit 、 DoCommit 三个阶段的设计,相较于 2PC 而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的 。
- PreCommit 是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。
- 3PC 协议并没有完全解决数据不一致问题。
一致性算法
一致性算法 Paxos
- Paxos 算法是 Lamport 提出的 一种基于消息传递的分布式一致性算法,使其获得 2013 年图灵奖。
- Paxos 由 Lamport 于 1998 年在《 The Part-Time Parliament 》论文中首次公开,最初的描述使用希腊的一个小岛 Paxos 作为比喻,描述了 Paxos 小岛中通过决议的流程,并以此命名这个算法,但是这个描述理解起来比较有挑战性。后来在 2001 年, Lamport 觉得同行不能理解他的幽默感,于是重新发表了朴实的算法描述版本《 Paxos Made Simple 》
- 自 Paxos 问世以来就持续垄断了分布式一致性算法, Paxos 这个名词几乎等同于分布式一致性。 Google 的很多大型分布式系统都采用了 Paxos 算法来解决分布式一致性问题,如 Chubby 、 Megastore 以及 Spanner 等。开源的 ZooKeeper ,以及 MySQL 5.7 推出的用来取代传统的主从复制的 MySQL Group Replication 等纷纷采用 Paxos 算法解决分布式一致性问题
- 然而,Paxos 的最大特点就是难,不仅难以理解,更难以实现。
- Paxos 解决了 分布式系统一致性 问题。
分布式系统才用多副本进行存储数据,如果对多个副本执行序列不控制, 那多个副本执行更新操作,由于网络延迟、超时等故障到值各个副本的数据不一致;
我们希望每个副本的执行序列 [ op1 op2 op3 .... opn ] 是不变的, 相同的;
Paxos 以此来确定不可变变量 opi 的取值,每次确定完 opi 之后,各个副本执行 opi 操作,以此类推。
结论: Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致;
注:这里某个数据的值并不只是狭义上的某个数,它可以是一条日志,也可以是一条命令(command)。。。根据应用场景不同,某个数据的值有不同的含义
假设有一组可以提出提案的进程集合,那么对于一个一致性算法需要保证以下几点:
- 在这些被提出的提案中,只有一个会被选定
- 如果没有提案被提出,就不应该有被选定的提案。
- 当一个提案被选定后,那么所有进程都应该能学习(learn)到这个被选定的 value
Paxos 相关概念
-
提案 ( Proposal ): Proposal 信息包括提案编号 ( Proposal ID ) 和提议的值 ( Value )
-
在Paxos算法中,有如下角色:
-
Client:客户端
向分布式系统发出请求 ,并等待响应 。
-
Proposer:提案发起者
提案者提倡客户请求,试图说服Acceptor对此达成一致,并在发生冲突时充当协调者以推动协议向前发展
-
Acceptor:决策者,可以批准提案
Acceptor可以接受(accept)提案;如果某个提案被选定(chosen),那么该提案里的 value就被选定了
-
Learners:最终决策的学习者
学习者充当该协议的复制因素
-
推导过程
-
规定:一个提案被选定需要被半数以上的 Acceptor 接受
- 这个规定又暗示了:『一个 Acceptor 必须能够接受不止一个提案!』
-
P1:一个 Acceptor 必须接受它收到的第一个提案。
-
P2:如果某个 value 为 v 的提案被选定了,那么每个编号更高的被选定提案的 value 必须也是 v。
- P2a :如果某个 value 为 v 的提案被选定了,那么每个编号更高的被 Acceptor 接受的提案的 value 必须也是 v 。
- P2b :如果某个 value 为 v 的提案被选定了,那么之后任何 Proposer 提出的编号更高的提案的 value 必须也是 v 。
- P2c :对于任意的 Mn 和 Vn ,如果提案 [Mn, Vn] 被提出,那么肯定存在一个由半数以上的 Acceptor 组成的集合 S ,满足以下两个条件中的任意一个:
- 要么 S 中每个 Acceptor 都没有接受过编号小于 Mn 的提案。
- 要么 S 中所有 Acceptor 批准的所有编号小于 Mn 的提案中,编号最大的那个提案的 value 值为 Vn
从 P1 到 P2c 的过程其实是对一系列条件的逐步增强,如果需要证明这些条件可以保证一致性,那么就可以进行反向推导: P2c => P2b => P2a => P2 ,然后通过 P2 和 P1 来保证一致性
Proposer 生成提案
接下来来学习,在 P2c 的基础上进行提案的生成
这里有个比较重要的思想: Proposer 生成提案之前,应该先去『学习』已经被选定或者可能被选定的 value ,然后以该 value 作为自己提出的提案的 value 。如果没有 value 被选定, Proposer 才可以自己决定 value 的值。这样才能达成一致。这个学习的阶段是通过一个『 Prepare 请求』实现的。
于是我们得到了如下的提案生成算法:
-
Proposer 选择一个新的提案编号 N ,然后向某个 Acceptor 集合(半数以上)发送请求(我们将该请求称为编号为 N 的 Prepare 请求),要求该集合中的每个 Acceptor 做出如下响应( response )
- Acceptor 向 Proposer 承诺保证不再接受任何编号小于 N 的提案。
- 如果 Acceptor 已经接受过提案,那么就向 Proposer 反馈已经接受过的编号小于 N 的,但值为最大编号的提案的值。
-
如果 Proposer 收到了半数以上的 Acceptor 的响应,那么它就可以生成编号为 N , Value 为 V 的提案 [N, V ]。这里的 V 是所有的响应中编号最大的提案的 Value 。如果所有的响应中都没有提案,那么此时 V 就可以由 Proposer 自己选择。
生成提案后, Proposer 将该提案发送给半数以上的 Acceptor 集合,并期望这些 Acceptor 能接受该提案。我们称该请求为 Accept 请求
Acceptor 接受提案
刚刚讲解了 Paxos 算法中 Proposer 的处理逻辑,怎么去生成的提案,下面来看看 Acceptor 是如何批准提案的
根据刚刚的介绍,一个 Acceptor 可能会受到来自 Proposer 的两种请求,分别是 Prepare 请求和 Accept 请求,对这两类请求作出响应的条件分别如下 :
- Prepare 请求: Acceptor 可以在任何时候响应一个 Prepare 请求
- Accept 请求:在不违背 Accept 现有承诺的前提下,可以任意响应 Accept 请求
因此,对 Acceptor 接受提案给出如下约束:
P1a :一个 Acceptor 只要尚未响应过任何编号大于 N 的 Prepare 请求,那么他就可以接受这个编号为 N 的提案。
算法优化
上面的内容中,分别从 Proposer 和 Acceptor 对提案的生成和批准两方面来讲解了 Paxos 算法在提案选定过程中的算法细节,同时也在提案的编号全局唯一的前提下,获得了一个提案选定算法,接下来我们再对这个初步算法做一个小优化,尽可能的忽略 Prepare 请求
如果 Acceptor 收到一个编号为 N 的 Prepare 请求,在此之前它已经响应过编号大于 N 的 Prepare 请求。根据 P1a ,该 Acceptor 不可能接受编号为 N 的提案。因此,该 Acceptor 可以忽略编号为 N 的 Prepare 请求。
通过这个优化,每个 Acceptor 只需要记住它已经批准的提案的最大编号以及它已经做出 Prepare 请求响应的提案的最大编号,以便出现故障或节点重启的情况下,也能保证 P2c 的不变性,而对于 Proposer 来说,只要它可以保证不会产生具有相同编号的提案,那么就可以丢弃任意的提案以及它所有的运行时状态信息
Paxos算法描述
综合前面的讲解,我们来对 Paxos 算法的提案选定过程进行下总结,那结合 Proposer 和 Acceptor 对提案的处理逻辑,就可以得到类似于两阶段提交的算法执行过程
Paxos 算法分为两个阶段。具体如下:
-
阶段一:
- Proposer 选择一个提案编号 N ,然后向半数以上的 Acceptor 发送编号为 N 的 Prepare 请求。
- 如果一个 Acceptor 收到一个编号为 N 的 Prepare 请求,且 N 大于该 Acceptor 已经响应过的所有 Prepare 请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给 Proposer ,同时该 Acceptor 承诺不再接受任何编号小于 N 的提案。
-
阶段二:
- 如果 Proposer 收到半数以上 Acceptor 对其发出的编号为 N 的 Prepare 请求的响应,那么它就会发送一个针对[ N , V ]提案的 Accept 请求给半数以上的 Acceptor 。注意: V 就是收到的响应中编号最大的提案的 value ,如果响应中不包含任何提案,那么 V 就由 Proposer 自己决定。
- 如果 Acceptor 收到一个针对编号为 N 的提案的 Accept 请求,只要该 Acceptor 没有对编号大于 N 的 Prepare 请求做出过响应,它就接受该提案。
当然,实际运行过程中,每一个 Proposer 都有可能产生多个提案,但只要每个 Proposer 都遵循如上所述的算法运行,就一定能够保证算法执行的正确性
Learner 学习被选定的 value
-
方案一: Learner 获取一个已经被选定的提案的前提是,该提案已经被半数以上的 Acceptor 批准,因此,最简单的做法就是一旦 Acceptor 批准了一个提案,就将该提案发送给所有的 Learner
很显然,这种做法虽然可以让 Learner 尽快地获取被选定的提案,但是却需要让每个 Acceptor 与所有的 Learner 逐个进行一次通信,通信的次数至少为二者个数的乘积
-
方案二:另一种可行的方案是,我们可以让所有的 Acceptor 将它们对提案的批准情况,统一发送给一个特定的 Learner (称为主 Learner ), 各个 Learner 之间可以通过消息通信来互相感知提案的选定情况,基于这样的前提,当主 Learner 被通知一个提案已经被选定时,它会负责通知其他的 Learner
在这种方案中, Acceptor 首先会将得到批准的提案发送给主 Learner ,再由其同步给其他 Learner。因此较方案一而言,方案二虽然需要多一个步骤才能将提案通知到所有的 Learner ,但其通信次数却大大减少了,通常只是 Acceptor 和 Learner 的个数总和,但同时,该方案引入了一个新的不稳定因素:主 Learner 随时可能出现故障
-
方案三:
在讲解方案二的时候,我们提到,方案二最大的问题在于主 Learner 存在单点问题,即主 Learner 随时可能出现故障,因此,对方案二进行改进,可以将主 Learner 的范围扩大,即 Acceptor 可以将批准的提案发送给一个特定的 Learner 集合,该集合中每个 Learner 都可以在一个提案被选定后通知其他的 Learner 。这个 Learner 集合中的 Learner 个数越多,可靠性就越好,但同时网络通信的复杂度也就越高
如何保证 Paxos 算法的活性
活性:最终一定会发生的事情:最终一定要选定 value
假设存在这样一种极端情况,有两个 Proposer 依次提出了一系列编号递增的提案,导致最终陷入死循环,没有 value 被选定,具体流程如下:
解决:通过选取主 Proposer ,并规定只有主 Proposer 才能提出议案。这样一来只要主 Proposer 和过半的 Acceptor 能够正常进行网络通信,那么但凡主 Proposer 提出一个编号更高的提案,该提案终将会被批准,这样通过选择一个主 Proposer ,整套 Paxos 算法就能够保持活性
一致性算法 Raft
- Raft 是一种为了管理复制日志的一致性算法。
- Raft 提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同。 Raft 算法更加容易理解并且更容易构建实际的系统。
- Raft 将一致性算法分解成了 3 模块
- 领导人选举
- 日志复制
- 安全性
- Raft 算法分为两个阶段,首先是选举过程,然后在选举出来的领导人带领进行正常操作,比如日志复制等。
- 动画演示
- GitHub
领导人 Leader 选举
- Raft 通过选举一个领导人,然后给予他全部的管理复制日志的责任来实现一致性。
- 在 Raft 中,任何时候一个服务器都可以扮演下面的角色之一,而影响他们身份变化的则是 选举:
- 领导者( leader ):处理客户端交互,日志复制等动作,一般一次只有一个领导者
- 候选者( candidate ):候选者就是在选举过程中提名自己的实体,一旦选举成功,则成为领导者
- 跟随者( follower ):类似选民,完全被动的角色,这样的服务器等待被通知投票
- Raft 使用心跳机制来触发选举。当 server 启动时,初始状态都是 follower 。每一个 server 都有一个定时器,超时时间为 election timeout (一般为 150-300 ms ),如果某 server 没有超时的情况下收到来自领导者或者候选者的任何消息,定时器重启,如果超时,它就开始一次选举
分布式系统设计策略
- 分布式系统本质是通过低廉的硬件攒在一起以获得更好的吞吐量、性能以及可用性等。
- 在分布式环境下,有几个问题是普遍关心的,我们称之为设计策略:
- 如何检测当前节点还活着?
- 如何保障高可用?
- 容错处理
- 负载均衡
心跳检测
- 心跳顾名思义,就是以固定的频率向其他节点汇报当前节点状态的方式。收到心跳,一般可以认为一个节点和现在的网络拓扑是良好的。当然,心跳汇报时,一般也会携带一些附加的状态、元数据信息,以便管理
-
若 Server 没有收到 Node3 的心跳时, Server 认为 Node3 失联。但是失联是失去联系,并不确定是否是 Node3 故障,有可能是 Node3 处于繁忙状态,导致调用检测超时;也有可能是 Server 与 Node3 之间链路出现故障或闪断。所以心跳不是万能的,收到心跳可以确认节点正常,但是收不到心跳也不能认为该节点就已经宣告“死亡”。此时,可以通过一些方法帮助 Server 做决定: 周期检测心跳机制、累计失效检测机制。 通过周期检测心跳机制、累计失效检测机制可以帮助判断节点是否“死亡”,如果判断“死亡”,可以把该节点踢出集群
-
周期检测心跳机制
Server 端每间隔 t 秒向 Node 集群发起监测请求,设定超时时间,如果超过超时时间,则判断“死亡”。
-
累计失效检测机制
在周期检测心跳机制的基础上,统计一定周期内节点的返回情况(包括超时及正确返回),以此计算节点的“死亡”概率。另外,对于宣告“濒临死亡”的节点可以发起有限次数的重试,以作进一步判断。
高可用设计
高可用( High Availability )是系统架构设计中必须考虑的因素之一,通常是指,经过设计来减少系统不能提供服务的时间
系统高可用性的常用设计模式包括三种:主备(Master-Slave)、互备(Active-Active)和集群(Cluster)模式。
-
主备模式
主备模式就是 Active-Standby 模式,当主机宕机时,备机接管主机的一切工作,待主机恢复正常后,按使用者的设定以自动(热备)或手动(冷备)方式将服务切换到主机上运行。在数据库部分,习惯称之为 MS 模式。 MS 模式即 Master/Slave 模式,这在数据库高可用性方案中比较常用,如 MySQL 、 Redis 等就采用 MS 模式实现主从复制。保证高可用,如图所示。
MySQL 之间数据复制的基础是二进制日志文件( binary log file )。一台 MySQL 数据库一旦启用二进制日志后,作为 master ,它的数据库中所有操作都会以“事件”的方式记录在二进制日志中,其他数据库作为 slave 通过一个 I/O 线程与主服务器保持通信,并监控 master 的二进制日志文件的变化,如果发现 master 二进制日志文件发生变化,则会把变化复制到自己的中继日志中,然后 slave 的一个 SQL 线程会把相关的“事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制。
-
互备模式
互备模式指两台主机同时运行各自的服务工作且相互监测情况。在数据库高可用部分,常见的互备是 MM 模式。 MM 模式即 Multi-Master 模式,指一个系统存在多个 master ,每个 master 都具有 read-write 能力,会根据时间戳或业务逻辑合并版本。
我们使用过的、构建过的 MySQL 服务绝大多数都是 Single-Master ,整个拓扑中只有一个 Master 承担写请求。比如,基于 Master-Slave 架构的主从复制,但是也存在由于种种原因,我们可能需要 MySQL 服务具有 Multi-Master 的特性,希望整个拓扑中可以有不止一个 Master 承担写请求
-
集群模式
集群模式是指有多个节点在运行,同时可以通过主控节点分担服务请求。如 Zookeeper。集群模式需要解决主控节点本身的高可用问题,一般采用主备模式。
容错性
- 容错顾名思义就是 IT 系统对于错误包容的能力
- 容错的处理是保障分布式环境下相应系统的高可用或者健壮性,一个典型的案例就是对于缓存穿透 问题的解决方案。
问题描述:
- 我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,或者有人恶意攻击,如频繁发起为id为“-1”的条件进行查询,可能DB就挂掉了。
那这种问题有什么好办法解决呢?
- 一个比较巧妙的方法是,可以将这个不存在的 key 预先设定一个值。比如, key =“ null ”。在返回这个 null 值的时候,我们的应用就可以认为这是不存在的 key ,那我们的应用就可以决定是否继续等待访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个 key ,如果取到的值不再是 null ,则可以认为这时候 key 有值了,从而避免了透传到数据库,把大量的类似请求挡在了缓存之中。
负载均衡
- 负载均衡:其关键在于使用多台集群服务器共同分担计算任务,把网络请求及计算分配到集群可用的不同服务器节点上,从而达到高可用性及较好的用户操作体验。
- 负载均衡器有硬件解决方案,也有软件解决方案。硬件解决方案有著名的 F5,软件有 LVS、HAProxy、Nginx等。
- 以 Nginx 为例,负载均衡有以下几种策略:
- 轮询:即 Round Robin ,根据 Nginx 配置文件中的顺序,依次把客户端的 Web 请求分发到不同的后端服务器。
- 最少连接:当前谁连接最少,分发给谁。
- IP 地址哈希:确定相同 IP 请求可以转发给同一个后端节点处理,以方便 session 保持。
- 基于权重的负载均衡:配置 Nginx 把请求更多地分发到高配置的后端服务器上,把相对较少的请求分发到低配服务器。
分布式架构网络通信
在分布式服务框架中,一个最基础的问题就是远程服务是怎么通讯的,在Java领域中有很多可实现远程通讯的技术,例如:RMI、Hessian、SOAP、ESB 和 JMS 等,它们背后到底是基于什么原理实现的呢
基本原理
要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络 IO 来实现,其中传输协议比较出名的有 tcp 、 udp 等等, tcp 、 udp 都是在基于 Socket 概念上为某类应用场景而扩展出的传输协议,网络 IO ,主要有 BIO、 NIO、 AIO 三种方式,所有的分布式应用通讯都基于这个原理而实现,只是为了应用的易用,各种语言通常都会提供一些更为贴近应用易用的应用层协议。
RPC
- RPC 全称为 remote procedure call,即远程过程调用。
- 借助 RPC 可以做到像本地调用一样调用远程服务,是一种进程间的通信方式
- RPC 并不是一个具体的技术,而是指整个网络远程调用过程
RPC架构
一个完整的 RPC 架构里面包含了四个核心的组件,分别是 Client , Client Stub , Server 以及 Server Stub ,这个 Stub 可以理解为存根。
- 客户端( Client ),服务的调用方。
- 客户端存根( Client Stub ),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
- 服务端( Server ),真正的服务提供者。
- 服务端存根( Server Stub ),接收客户端发送过来的消息,将消息解包,并调用本地的方法。
RPC 调用过程:
- 注意:无论是何种类型的数据,最终都需要转换成二进制流在网络上进行传输,数据的发送方需要将对象转换为二进制流,而数据的接收方则需要把二进制流再恢复为对象。
- 在 java 中 RPC 框架比较多,常见的有 Hessian 、 gRPC 、 Thrift 、 HSF ( High Speed Service Framework )、 Dubbo 等,其实对 于 RPC 框架而言,核心模块 就是通讯和序列化
RMI
Java RMI 指的是远程方法调用 ( Remote Method Invocation ),是 java 原生支持的远程调用 ,采用 JRMP ( Java Remote Messaging Protocol )作为通信协议,可以认为是纯 java 版本的分布式远程调用解决方案, RMI 主要用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上、也可以在同一个主机上,这里的通信可以理解为一个虚拟机上的对象调用另一个虚拟机上对象的方法。
- 客户端:
- 存根 / 桩( Stub ):远程对象在客户端上的代理;
- 远程引用层( Remote Reference Layer ):解析并执行远程引用协议;
- 传输层( Transport ):发送调用、传递远程方法参数、接收远程方法执行结果。l
- 服务端:
- 骨架( Skeleton ):读取客户端传递的方法参数,调用服务器方的实际对象方法,并接收方法执行后的返回值;
- 远程引用层( Remote Reference Layer ):处理远程引用后向骨架发送远程方法调用;
- 注册表( Registry ):以 URL 形式注册远程对象,并向客户端回复对远程对象的引用。
远程调用过程:
- 客户端从远程服务器的注册表中查询并获取远程对象引用。
- 桩对象与远程对象具有相同的接口和方法列表,当客户端调用远程对象时,实际上是由相应的桩对象代理完成的。
- 远程引用层在将桩的本地引用转换为服务器上对象的远程引用后,再将调用传递给传输层( Transport ),由传输层通过 TCP 协议发送调用;
- 在服务器端,传输层监听入站连接,它一旦接收到客户端远程调用后,就将这个引用转发给其上层的远程引用层;
- 服务器端的远程引用层将客户端发送的远程应用转换为本地虚拟机的引用后,再将请求传递给骨架( Skeleton );
- 骨架读取参数,又将请求传递给服务器,最后由服务器进行实际的方法调用。
结果返回过程:
- 如果远程方法调用后有返回值,则服务器将这些结果又沿着 “骨架 -> 远程引用层 -> 传输层” 向下传递;
- 客户端的传输层接收到返回值后,又沿着 “传输层->远程引用层->桩” 向上传递,然后由桩来反序列化这些返回值,并将最终的结果传递给客户端程序
实例
公共调用接口:
public interface IHelloService extends Remote {
//1.定义一个sayHello方法
String sayHello(User user) throws RemoteException;
}
实体类:
public class User implements Serializable {
private String username;
private int age;
public User() {
}
public User(String username, int age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
服务端
接口实现:
public class HelloServiceImpl extends UnicastRemoteObject implements IHelloService {
//手动实现父类的构造方法
public HelloServiceImpl() throws RemoteException {
super();
}
//我们自定义的sayHello
public String sayHello(User user) throws RemoteException {
System.out.println("this is server , say hello to " + user.getUsername());
return "success";
}
}
服务发布:
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
//1.创建HelloService实例
IHelloService service = new HelloServiceImpl();
//2.获取注册表
LocateRegistry.createRegistry(8899);
//3.对象的绑定
//bind方法的参数1: rmi://ip地址:端口/服务名 参数2:绑定的对象
Naming.bind("//127.0.0.1:8899/rmiserver", service);
}
}
客户端
服务调用:
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
//1.从注册表中获取远程对象 , 强转
IHelloService service = (IHelloService) Naming.lookup("//127.0.0.1:8899/rmiserver");
//2.准备参数
User user = new User("laowang", 18);
//3.调用远程方法sayHello
String message = service.sayHello(user);
System.out.println(message);
}
}
同步&异步,阻塞&非阻塞
- 同步(synchronize)、异步(asychronize)是指 应用程序和内核的交互 而言的
- 阻塞和非阻塞是针对于 进程访问数据的时候,根据 IO 操作的就绪状态 来采取不同的方式
老张煮开水。 老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
- 老张把水壶放到火上,站立着等水开。(同步阻塞)
- 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
- 老张把响水壶放到火上,站立着等水开。(异步阻塞)
- 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
Netty
参考: