这是我参与「第五届青训营」笔记创作活动的第7天
后端进阶-分布式理论~
前言:
本次直播课程从概述、系统模型、理论基础、分布式事务、共识协议、分布式实践六大方面带领我们了解分布式相关的理论知识,讲述了分布式理论中涉及到的各种概念,帮助我们快速入门分布式架构...
分布式概述
什么是分布式?
分布式系统是计算机程序的集合,这些程序利用跨多个独立计算节点的计算资源来实现共同的目标。
说白了就是程序和数据可以不再位于一个服务器上,而是分散到多个服务器,通过彼此之间的相互协调来保证程序正确运行。
感觉和几年前炒的火热的比特币概念类似,分布式系统基于去中心化的思想,分布式是一把双刃剑,既有优势也有面临着风险:
- 五大优势:去中心化、低成本、弹性、资源共享、可靠性高
- 面临的挑战:故障、网络、环境、安全
Why-How-What
站在使用者的视角:面对海量的数据存储、运算的需求,分布式系统显然再合适不过了,由于分布式系统中对于每个节点来说不需要特别昂贵的设备,多台廉价的服务器也可以构成高性能的计算平台,同时现在有很多成熟的分布式框架可供使用,帮助使用者解决单机时代的成本高、负载大、系统不稳定、容错低等等痛点。
站在学习者的角度:分布式已经是后端开发的必备技能了,目前很多互联网公司都在往分布式迁移,掌握分布式理论也可以帮助我们理解后台服务器之间协作的机理。
常见的分布式系统
分布式系统可以分成三大板块:分布式存储、分布式数据库、分布式计算。
系统模型
故障模型
在分布式系统中,故障可能发生在节点或者通信链路上,下面是从最广泛最难的到最特定最简单的顺序列出的故障类型:
- byzantine failure:节点可以任意篡改发送给其他节点的数据,是最难处理的故障
- Performance failure:节点未在特定时间段内收到数据,即时间太早或太晚
- omission failure:节点收到数据的时间无限晚,即收不到数据
- crash failure:节点停止响应,持续性的故障
- fail-stop failure:错误可检测,是最容易处理的故障
byzantine failure 也就是拜占庭故障非常难处理,在现实中解决这类故障的成本极高,只有在特定的领域才会通过高冗余解决它。
拜占庭将军问题
分布式系统中的数据是分散存储在多个节点上的,因此要保证多台主机的数据一致性,也就是多台主机要达成共识。
下面先从拜占庭将军问题入手,引出解决分布式系统确保一致性的思路:
- 拜占庭将军问题:由莱斯利·兰伯特提出的点对点通信中的基本问题。拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军,问题是这些将军在地理上是分隔开来的,并且将军中存在叛徒。叛徒可以任意行动以达到以下目标:欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。如果叛徒达到了这些目的之一,则任何攻击行动的结果都是注定要失败的,只有完全达成一致的努力才能获得胜利。
- 内涵:“拜占庭将军问题”延伸到互联网生活中来,其内涵可概括为:在互联网大背景下,当需要与不熟悉的对方进行价值交换活动时,人们如何才能防止不会被其中的恶意破坏者欺骗、迷惑从而作出错误的决策。进一步将“拜占庭将军问题”延伸到技术领域中来,其内涵可概括为:在缺少可信任的中央节点和可信任的通道的情况下,分布在网络中的各个节点应如何达成共识。
两将军问题:
- 两支军队的将军只能派信使穿越敌方领土互相通信,以此约定进攻时间。该问题希望求解如何在两名将军派出的任何信使都可能被俘虏的情况下,就进攻时间达成共识。
- 结论:两将军问题是被证实无解的电脑通信问题,两支军队理论上永远无法达成共识。
- 解决方案:
- 方案一:同时发送 N 个信使,任何一个达到对方军队,都算成功。
- 方案二:设置超时时间,发送后未在一定时间返回,则加派信使。
TCP 三次握手是该问题的一个工程解,TCP 三次握手在两个方向确认包的序列号,增加了超市重试。
三将军问题:
两个“忠将” A 和 B,一个“叛徒” C,互相传递消息,消息可能丢失,也可能被篡改,当有一个将军是“叛徒”(即出现拜占庭故障)时,整个系统无法达成一致。由于“叛徒” C 的存在,将军 A 和将军 B 获得不同的信息。这样将军 A 获得 2 票进攻 1 票撤退的信息,将军 B 获得 1 票进攻 2 票撤退的信息,产生了不一致
四将军问题:
将军 D 作为消息分发中枢,约定如果没收到消息则执行撤退。
步骤:
- 如果 D 为“叛徒”,ABC 无论收到任何消息,总能达成一致。
- D 为“忠将”,ABC 有 2 人将 D 的消息进行正确的传递,同样能保证最终决策符合大多数。
进而能够证明,当有 个将军, 个“叛徒”时,可以进行 轮协商,最终达成一致。
共识和一致性
不同客户端 A 和 B 看到客户端 C 写入,由于时机不同,可能产生数据读取的偏差,如果能保证 A 和 B 读取到一致的数据,就可以说是具备一致性。
- 最终一致性(Eventually consistent):A 读到
x = 0,当 C 正在写入时,A 和 B 可能读到 0 或者 1,但是当 C 写入完成后,A 和 B 最终能读到一致的数据。 - 线性一致性(Linearizability):当 A 读到更新的版本
x = 1后,及时将消息同步给其他客户端,这样其他客户端立即能获取到x = 1。
时间和事件顺序
1978 年 Leslie Lamport 发表的论文《Time, Clocks, and the Ordering of Events in a Distributed System》 定义了计算机系统中的时间和事件顺序,引入 happened before 和并发的定义,可以以此对分布式系统中的事件进行推导。
定义 happened before 关系,记为 ->,其满足如下三个条件:
- 如果 a 和 b 是在相同节点上的两个事件,a 在 b 之前发生,则定义
a->b - 如果事件 a 表示某个节点发送某条消息,b 是另一个节点接受这条消息,则有
a->b - 如果有
a->b且b->c,则有a->c
当且仅当 且 时,我们称这两个事件为并发的(concurrent)。
不难在上图中找到若干满足条件的事件对,例如 p1->r4,其由 p1->q2->q4->r3->r4 推导而来。
同时 Leslie Lamport 描述了一种逻辑时钟的表示方法,称为 Lamport 逻辑时钟,用来求取事件的全序序列。
分布式系统中按是否存在节点交互分为三类事件,一类发生于节点内部,二是发送事件,三是接收事件。对于每一个节点 我们定义时钟 为一个函数,它为任意的事件 a 赋值编号为 。
- 如果 a 和 b 是在相同节点上的两个事件,a 在 b 之前发生,则有
- 如果事件 a 表示节点 发送某条消息,b 表示节点 接受这条消息,则有
于是我们可以在时空图中加入类似上图虚线所示的 tick line,在同一节点内的连续两个事件之间,至少要有一条 tick line,利用逻辑时钟,我们就可以对整个系统中的事件进行全序排序。
理论基础
CAP 理论
CAP 分别指一致性、可用性和分区容错性,CAP 理论往往运用于数据库领域,同样可以适用于分布式存储方向。
| 选项 | 描述 |
|---|---|
| C(Consistence) | 一致性,指数据在多个副本之间能够保持一致 |
| A(Availability) | 可用性,指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应,但不保证获取的数据是最新的 |
| P(Network partitioning) | 分区容错性,分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障 |
CAP 理论的意义在于它告诉我们一致性、可用性和分区容错性是不可能同时满足的,最多只能保证其中的两点。
- CA:放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择。
- AP:放弃一致性,追求分区容错性和可用性,例如一些注重用户体验的系统。
- CP:放弃可用性,追求一致性和分区容错性,例如与钱财安全相关的系统。
ACID 理论
事务是数据库系统中非常重要的概念,它是一连串操作的集合,一个事务中的所有操作要么全部执行,要么全都不执行。
数据库事务拥有四个特性 ACID,即分别为原子性、一致性、隔离性和持久性:
- 原子性(Atomicity):事务包含的所有操作要么全部成功,要么全部失败。
- 一致性(Consistency):区别于 CAP 理论中的一致性概念,这里是指事务必须使数据库从一个正确的状态迁移到另一个正确的状态。
- 隔离性(Isolation):多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发的事务之间要相互隔离。
- 持久性(Durability):一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
BASE 理论
BASE 理论是针对 AP 系统而言的,其来源于对大型互联网分布式实践的总结,其核心思想是:
- Basically Available(基本可用):假设系统,出现了不可预知的故障,但还是能用。
- Soft state(软状态):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性。
- Eventually consistent(最终一致性):数据最终一定能够达到一致的状态。
分布式事务
二阶段提交
二阶段提交(Two-phase Commit)是为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种演算法。
三个假设:
- 协调者和参与者进行通信
- 预写式日志被保持在可靠的存储设备上
- 所有节点不会永久性损坏,即使损坏后仍然可以恢复
正常流程:Prepare 阶段和 Commit 阶段。
异常流程:
- Prepare 阶段失败 -> 回滚
- 协调者宕机 -> 重新启用新的协调者
- 双故障重启 -> 数据库管理员介入
在 Prepare 阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。
三阶段提交
2PC 设计还存在缺陷,为了解决单点故障问题与阻塞问题,提出了三阶段提交(Three-phase commit)协议。
3PC 将两阶段提交中的 Prepare 阶段,拆分成两部分:CanCommit 和 PreCommit 机制,另外引入超时机制,在等待超时之后,会继续进行事务的提交。
🎈详细解释:三阶段提交 - 维基百科,自由的百科全书 wiki
MVCC(多版本并发控制)
MVCC 是一种并发控制的方法,维持一个数据的多个版本使读写操作没有冲突,所以既不会阻塞写,也不阻塞,提高并发性能的同时也解决了脏读的问题。
🎈MVCC 详解:全网最全的一篇数据库MVCC详解,不全我负责-mysql教程-PHP中文网
共识协议
Quorum NWR 模型
Quorum NWR 模型将 CAP 的选择交给用户,是一种简化版的一致性模型。
三要素:
- N:在分布式存储系统中,有多少份备份数据
- W:代表一次成功的更新操作要求至少有 W 份数据写入成功
- R:代表一次成功的读数据操作要求至少有 R 份数据成功读取
为了保证强一致性,需要保证 。
Quorum NWR 模型允许数据被覆盖,这会导致并发更新引起一致性的问题。
RAFT 协议
Raft 协议是一种分布式一致性算法(共识算法),即使出现部分节点故障,网络延时等情况,也不影响各节点,进而提高系统的整体可用性。Raft 是使用较为广泛的分布式协议,一定意义上讲,RAFT 也使用了 Quorum 机制。
三种角色:
- Leader - 领导者:Leader 负责处理所有的客户端请求,并向 Follower 同步请求日志,当日志同步到大多数节点上后,通知 Follower 提交日志。
- Follower - 跟随者:接受并持久化 Leader 同步的日志,在 Leader 告知日志可以提交后,提交日志。
- Candidate - 备选者:Leader 选举过程中的临时角色,向其他节点发送请求投票信息。
四种定义:
- Log(日志):节点之间同步的信息,以只追加写的方式进行同步,解决了数据被覆盖的问题。
- Term(任期号):单调递增,每个 Term 内最多只有一个 Leader。
- Committed:日志被复制到多数派节点,即可认为已经被提交。
- Applied:日志被应用到本地状态机,执行了 log 中命令,修改了内存状态。
Leader 选举过程:
- 初始全部为 Follower
- Current Term + 1
- 选举自己
- 向其它参与者发起 RequestVote 请求,retry 直到:
- 收到多数派请求,成为 Leader,并发送心跳
- 收到其它 Leader 的请求,转为 Follower,更新自己的 Term
- 收到部分,但未达到多数派,选举超时,随机 timeout 开始下一轮
- 两个规则:
- 在一个任期内每个参与者最多投一票(持久化)
- 要成为 Leader,必须拿到多数投票
切主(当 Leader 出现问题时,就需要进行重新选举):
- Leader 发现失去 Follower 的响应,失去 Leader 身份
- 两个 Follower 之间一段时间未收到心跳,重新进行选举,选出新的 Leader,此时发生了切主
- Leader 自杀重启,以 Follower 的身份加入进来
Stale Read(过时读取):
当发生 Leader 切换,old leader 收到了读请求,如果直接响应,可能会导致 Stale 读。
Paxos 协议:
Paxos 算法与 RAFT 算法区别:
- Multi-Paxos 可以并发修改日志,而 Raft 写日志操作必须是连续的
- Multi-Paxos 可以随机选主,不必最新最全的节点当选 Leader
优势 & 劣势:
优势:写入并发性能高,所有节点都能写
劣势:没有一个节点有完整的最新的数据,恢复流程复杂,需要同步历史记录
分布式实践
课程结尾给出了两个分布式系统的实践,主要带我们思考设计思路。
简易版 MapReduce 实现思路
MapReduce 是 Google 提出的分布式计算模型,分布式框架 Hadoop 实现了 MapReduce,基于分而治之的思想来计算海量的数据。
MapReduce 可以分成三个阶段:
- Mapper - 将输入分解为多个 Job 来并行处理,彼此之间几乎没有依赖关系
- Shuffler - 将 mapper 结果打乱,防止数据倾斜
- Reduce - 对 map 阶段的结果进行全局汇总
容错设计:
- Mapper 故障:由中心化节点重新发起调度,新起 mapper 重跑 job
- Reducer 故障:重跑 mapper,代价较大
分布式 KV
设计思路(架构):
将海量结构化数据根据 key 分成不同的 Region,每个 Region 构建一个单机 KV 数据库,Region 之间形成 Raft Groups,做到强一致。
容错设计:当 Node 故障时,通过 Raft Learner 模式进行数据修复。
弹性:当出现局部 Key 热点或数据膨胀时,Region 可以进行 Split 操作,分成两个子 Region,反之收缩时进行 Merge 操作。
分布式 KV 的实现教程在网上有很多,pingCAP 提供了开源课程 TinyKV,感兴趣的话可以看看。
🎈课程链接:github.com/tidb-incuba…
🚀【参考】🚀