课程目录
- 分布式概述
- 系统模型
- 理论基础
- 分布式事务
- 共识协议
- 分布式实践
分布式概述
分布式:分布式系统是计算机程序的集合,这些程序利用跨多个独立计算节点的计算资源来实现共同的目标
优势
- 去中心化
- 低成本
- 弹性
- 资源共享
- 可靠性高
挑战
- 普遍的节点故障
- 不可靠的网络
- 异构的机器与硬件环境
- 安全
分布式存储
- Google File System(GFS): google 分布式文件系统
- Ceph:统一的分布式存储系统
- Hadoop HDFS:基于 GFS 架构的开源分布式文件系统
- Zookeeper:高可用的分布式数据管理与系统协调框架
分布式数据库
- Google Spanner:google 可扩展的、全球分布式的数据库
- TiDB:开源分布式关系型数据库
- HBase:开源 Nosql 数据库
- MongoDB:文档数据库
分布式计算
- Hadoop:基于 MapReduce 分布式计算框架
- Spark:在 Hadoop 基础之上,使用内存来存储数据
- YARN:分布式资源调度
系统模型
拜占庭将军问题
两将军问题:两只军队只能派信使穿越地方领土互相通信,以此约定进攻时间,该问题希望求解如何在两名将军排除的任何信使都可能被俘虏的情况下,就进攻时间达成共识。
结论是:两将军问题被证实是无解的电脑通信问题,两支军队理论上永远无法达成共识
共识与消息传递的不同:即使保证了消息传递成功,也不能保证达成共识(不能确定消息是否被篡改)
TCP 三次握手是在两个方向确认包的序列号,增加了超时重试,是两将军问题的一个工程解。
思考(待补充)
- 为何需要 3 次握手而不是 2 次或者 4 次?
除了从防止历史连接的影响角度分析,也可以从 socket 连接角度分析,第一次 tcp 连接是将客户端的控制信息保存到服务器端的套接字中;第二次 tcp 连接一方面是告诉客户端已经收到控制信息,同时也将自己的控制信息保存到客户端的套接字中。客户端需要通过第三次 tcp 连接告诉服务器端自己也获得了服务器的控制信息,做好了通信前的准备,可以正式开始数据收发了。
当有 3f + 1 个将军时,其中有 f 个叛徒时,可以增加 f 轮协商,最终达成一致
比特币是拜占庭容错的系统 (使用 pow(工作量证明)解决了拜占庭问题)
共识和一致性
最终一致性
线性一致性
时间和事件顺序
分布式论文鼻祖(Time, Clocks, and the Ordering of Events in a Distributed System, Leslie Lamport)
定义了 "happened before" 关系,记为"->"。满足如下三个条件
- 如果 a 和 b 是相同节点上的两个事件,a 在 b 之前发生,则定义:a -> b
- 如果事件 a 表示某个节点发送某条消息,b 是另一个节点接收这条消息,则有 a -> b
- 如果有 a -> b 且 b -> c,则有 a -> c;
当且仅当 a /-> b 且 b /-> a 时,我们称两个事件为并发的(concurrent)
下图中的 p1 -> r4,可由 p1 -> q2 -> q4 -> r3 -> r4 推导而来
Lamport 逻辑时钟
对于每一个节点 Pi 我们定义时钟 Ci 为一个函数,它为任意的事件 a 赋值编号为 Ci(a)
- 如果 a 和 b 是在相同节点 Pi 上的两个事件,a 在 b 之前发生,则有 Ci(a) < Ci(b)
- 如果事件 a 表示节点 Pi 发送某条消息,b 表示节点 Pj 接受这条消息,则有 Ci(a) < Cj(b)
图中虚线所示的称为"tick line"
在同一节点内的连续两个事件之间,至少要有一条 tick line
利用逻辑时钟,我们可以对整个系统中的事件进行全序排序
理论基础
CAP 理论
C(Consistence):一致性,数据在多个副本之间能够保持一致的特性(严格的一致性)
A(Available):可用性,指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应---但是不保证获取的数据为最新数据
P(Network partitioning):分区容错性,分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障
CAP 理论往往运用于数据库领域,同样可以适用于分布式存储方向
CA:放弃分区容错性,加强一致性和可用性(传统单机数据库的选择)
AP:放弃一致性(指的是强一致性),追求分区容错性和可用性,例如一些注重用户体验的系统
CP:放弃可用性,追求一致性和分区容错性,例如与钱财安全相关的系统
上图左侧是保证了可用性,放弃了一致性,当读取 P1 的数据时,得到 a = 1;读取 P2 的数据时,得到 a = 0 (即使 P2 并未成功同步最新的数据,也可以正常读取数据);
上图右侧是保证了一致性,放弃了可用性,当 P2 未成功同步最新的数据,读取 P2 数据就会被拒绝(保证了用户读取数据的一致性)
在网络发生分区的情况下,我们必须在可用性和一致性之间做出选择,近似解决办法:把故障节点的负载转移给备用节点负责,当 Master 节点出现故障,将请求转移给 Backup 节点进行处理(换主),以此保证高可用。如果 Master 节点和 Backup 节点同时出现故障,此时就丢失了故障点后的最新数据,Master 重启后可以同步最新的数据,达成状态一致性。
ACID 理论
事务:数据库管理系统执行过程中的一个逻辑单元,它能够保证一个事务中的所有操作要么全部执行,要么全部不执行。
数据库事务拥有四个特性 ACID
- 原子性(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚;
- 一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。(指的是事务的一致性,CAP 中的一致性指的是线性一致性)
- 隔离性(Isolation):隔离性是当多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability):持久性是指一个事务一旦被提交了,那么对数据库中的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失事务的操作。
BASE 理论
BASE 理论是对 CAP 中一致性和可用性权衡的结果。
Basically Available(基本可用):假设系统出现了不可预知的故障,但还是能用,相比较正常的系统而言:响应时间上的损失或功能上的损失都可以接受。
Soft sate(软状态):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent(最终一致性):系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值。
分布式事务
二阶段提交
二阶段提交(Two-phase Commit):为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(prepare 阶段和 commit 阶段)
三个假设
- 引入协调者(Coordinator)和参与者(Participants),互相进行网络通信
- 所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上
- 所有节点不会永久性损坏,即使损坏后仍然可以恢复
可能出现的情况
情况 1:Coordinator 不宕机,Participant 宕机,需要进行回滚操作
情况 2:Coordinator 宕机,Participant 不宕机。可以起新的协调者,待查询状态后,重复二阶段提交
情况 3:Coordinator 宕机,Participant 宕机、无法确认状态,需要数据库管理员的介入,防止数据库进入一个不一致的状态。
回滚:在 Prepare 阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。
两阶段提交需要注意的问题
- 性能问题:两阶段提交需要多次节点间的网络通信,耗时过大,资源需要进行锁定,徒增资源等待时间。
- 协调者单点故障问题:如果事务协调者节点宕机,需要另起新的协调者,否则参与者处于中间状态无法完成事务。
- 网络分区带来的数据不一致:一部分参与者收到了 Commit 消息,另一部分参与者并没有收到,会导致了节点之间数据不一致(可以使用锁来处理)
思考
- 日志被保存在可靠的存储设备上,可靠如何保证?
通常在传统的单机数据库中,我们使用高可用的硬件来保存。(IOE,IBM 的服务器、Oracle的数据库、EMC 的存储(保证数据库数据保存在可靠的设备上))。
在分布式时代,往往会建立一层分布式文件系统、分布式块存储或分布式 KV 系统来达到可靠存储的目的;
- 参与者 Commit 了,但 Ack 信息协调者没收到,怎么办?
回滚后重新进行一遍流程
三阶段提交
将两阶段提交中的 Prepare 阶段,拆分成两部分: CanCommit 和 PreCommit
解决了两个问题
- 单点故障的问题
- 阻塞的问题
另外引入了超时机制,在等待超时后,会继续进行事务的提交。
例子
在两阶段提交中,如果 A 要给 B 转账 3W,但是 A 账面上只有 1W,Prepare 阶段结束后,Commit 阶段会失败(账面资金不足),但白白写了一条 Prepare 日志。
在三阶段提交中,上面的情况就可以避免,在 CanCommit 阶段会检查 A 是否可以成功发起转账,如果不行的话直接退出。类似于 TCP 三次握手中的第一次连接建立请求,判断客户端和服务器是否能够建立连接,如果可以的话,才真正建立连接,防止了资源的浪费。
MVCC
悲观锁
:操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
乐观锁
:不会上锁,只是在执行更新时判断别人是否修改数据,只有冲突时才放弃操作。
MVCC是一种并发控制的方法,维持一个数据的多个版本使读写操作没有冲突,所以既不会阻塞写,也不会阻塞读。MVCC为每个修改保存一个版本,和事务的时间戳相关联。可以提高并发性能,解决脏读的问题。
时间戳实现方式
- TrueTime API:提供物理时钟同步(服务器时钟偏差在 1 到 7ms)
保证了所有的服务器时间是在一个 tick line 中
- 时间戳预言机(TSO):中心化授时方式
共识协议
Quorum NWR模型
三要素
N:在分布式存储系统中,有多少份分布数据
W:代表一次成功的更新操作要求至少有 W 份数据写入成功
R:代表一次成功的读数据操作要求至少有 R 份数据读取成功
为了保证强一致性,需要保证 W + R > N
反证法:如果 W + R < N, R < N - W(N - W 旧的数据的份数),意味着我们将从含有 N - W 份老数据中读取 R 份数据,因此我们可能会读取到老的数据,这与我们保证强一致性矛盾。
Quorum NWR 模型将 CAP 的选择交给用户,是一种简化版的一致性模型
思考:并发更新问题
如果读到副本 1和副本 2, 得出 v=3 的结论
如果读到副本 2 和副本 3,得出 v=2 的结论
问题的根源: 允许数据被覆盖,因此 Quorum NWR 模型常用于append-only的分布式文件系统中(比如区块链 )
RAFT 协议
Raft 协议是一种分布式一致性算法, 即使出现部分节点故障,网络延迟等情况,也不影响各节点,进而提供系统的整体可用性。Raft 是使用较为广泛的分布式协议。
Leader(领导者):通常一个系统中是一主(Leader)多从(Follower)。Leader 负责处理所有的客户端请求,并向 Follower 同步请求日志。当日止同步到大多数节点上后,通知 Follower 提交日志。
Follower(跟随者):不会发送任何请求,接收并持久化 Leader 同步的日志,在 Leader 告知日志可以提交后,提交日志。当 Leader 出现故障时,主动推荐自己为 Candidate。
Candidate(候选者):Leader 选举过程中的临时角色。向其他节点发送请求投票信息,如果获得大多数选票,则晋升为 Leader。
补充概念
Log(日志):节点之间同步的信息,以只追加写的方式进行同步,解决了数据被覆盖的问题;
Term(任期号):单调递增,每个 Term 内最多只有一个 Leader;
Committed:日志被复制到多数节点,即可认为已经被提交;
Applied:日志被应用到本地状态机:执行了 Log 中的命令,修改了内存的状态。
身份状态转换图
Leader 选举
- 初始全部节点都为 Follower
- Current Term + 1
- 选举自己
- 向其他参与者发起 RequestVote 请求,retry 直到
- 收到大多数节点的投票,成为 Leader,并向其他 Follower 发送心跳
- 收到其他 Leader 的请求,转为 Follower,更新自己的 Term
- 收到部分投票但未达到多数派,选举超时,随机 timeout 后开始下一轮选主
timeout 是为了多个节点同时发起选主请求而导致的主节点无法选出的问题
两个规则
- 在一个任期内每个参与者最多只能投一票(持久化)
- 要成为 Leader,必须拿到多数投票
Log Replication 过程
新 Leader 产生,Leader 和 Follower 不同步,Leader 强制覆盖 Follower的不同步日志(删除 Follower 不同步的部分,将新的日志同步给 Follower)
- Leader 收到写请求 w
- 将 w 写入本地 log
- 向其他 Follower 发起 AppendEntried RPC
- 等待多数派回复
- 更新本地状态机,返回给客户端
- 下一个心跳通知 Follower 上一个 Log 已经被 Committed 了
- Follower 也将日志应用到本地状态机
- Follower 有问题,Leader 一直 retry
思考:如果主节点出现问题呢?
当 Leader 出现问题时,就需要进行重新选举。
- Leader 发现失去 Follower 的响应,失去 Leader 身份
- 两个 Follower 之间一段时间未收到心跳,重新进行选举,选出新的 Leader,此时发生了切主
- Leader 自杀重启,以 Follower 的身份加入进来
思考:老 Leader 未失去身份,新 Leader 已经选出,产生了双主(脑裂),该怎么解决?
在主节点失去响应后,等待两个超时时间后,才开始进行新主选举。(旧王彻底咽气,新王才能登基)
思考:Stale 读问题
发生 Leader 切换,old leader 收到了读请求,如果直接响应,可能会有 Stale Read,该如何解决?
解决方案:保证读的强一致性
读操作在 lease timeout 内,默认自己时 leader;不是则发起一次 heartbeat,等待 Commit Index 应用到状态机。
Election timeout > lease timeout : 新 leader 上任,自从上次心跳之后一定超过了 Election timeout,旧 leader 大概率能够发现自己的 Lease 过期。
Paxos 协议
Paxos 算法与 RAFT 算法区别:
- Multi-Paxos 可以并发修改日志,而 Raft 写日志操作必须是连续的
- Multi---Paxos 可以随机选主,不必最新最全的节点当选 Leader
Paxos 优势:写入并发性能高,所有节点都能写入 Paxos 劣势:没有一个节点有完整的最新数据,恢复流程复杂,需要同步历史记录
角色:Propos ers 和 Acceptors
处理流程
- Proposers:获取 Proposal ID n
- Proposers:向所有 Acceptors 广播
- Acceptors:如果 n > min_proposal 则 min_proposal := n, 返回 accepted_value 和 Proposal
- Proposers:接收到过半数回复,选择 Proposal 最大的 accepted_value 作为共识
- Proposers:第二阶段,广播 Accept(n, value) 到所有节点
- Acceptors:如果 n >= min_proposal 则 min_proposal := n, accepted_value := value, 本地持久化,返回
- 接收过半请求,如果有结果>n, 更新新的提议,跳转到 1 继续执行
分布式实践
MapReduce
以 Hadoop 为例,Input 输入是存储在 HDFS 中的,由 Mapper 将输入分别为多个 Job 并行处理(提高处理速度),由 Shuffler 将 Mapper 的结果打乱,防止数据的倾斜,最终由 Reducer 对结果进行全局汇总。
思考:故障如何处理?
- Mapper 故障:由中心化的节点重新发起调度,新起的 Mapper 重新跑一次 Job
- Reducer 故障:需要重新跑一次 Mapper 的所有 Job(代价大)
分布式 KV
架构:将海量结构化数据根据 Key 分成不同的 Region,每个 Region 构建一个单机 KV 数据库,Region 之间形成 Raft Groups, 做到强一致。
容错:当 Node 故障时,通过 Raft Learner 模式进行数据恢复
弹性:当出现局部 Key 热点或数据膨胀时,Region 可以进行 Split 操作,分成两个子 Region,反之收缩时进行 Merge 操作。(分布式 KV 是一种在线服务,它是会变的,因此对弹性有一定的要求)