关于架构设计:架构师日记为什么数据一致性那么难

3次阅读

共计 8082 个字符,预计需要花费 21 分钟才能阅读完成。

作者:京东批发 刘慧卿

一 前言

在古代大型分布式软件系统中,有一个绕不过来的课题,那就是如何保证系统的数据一致性。驰名的 Paxos 算法(Megastore、Spanner),Raft 协定(ETCD、TiKV、Consul),ZAB 协定(ZooKeeper)等分布式一致性解决方案,都是在此背景下而诞生的。

数据一致性保障为什么难呢?先来看一下咱们熟知的本地数据库事务是如何实现数据一致性的。家喻户晓,数据库事务有 ACID 四大个性,即原子性 (Atomicity)、一致性(Consistency)、隔离性(Isolation) 和持久性(Durability)。任何反对数据库事务的存储引擎都须要满足这四大个性。以 Mysql 数据库的 Innodb 存储引擎的设计实现举例,数据一致性过程如下:

  1. 持久性:通过 binlog、redolog 来实现;(用于数据重放,从库备份)
  2. 原子性:通过 undolog 来实现;(用于数据回滚)
  3. 隔离性:通过读写锁 +MVCC(undolog)来实现;(用于隔离级别的实现)
  4. 一致性:通过原子性,持久性,隔离性最终实现数据一致性;

由此能够见,一致性的实现是在持久性,原子性,隔离性等各种个性根底之上的。从技术实现伎俩来看,为什么是实现数据一致性的过程须要借助这么多种日志文件呢?

这还得从硬件效率上讲起,咱们先来看一组数据(仅做示意,不同硬盘型号指标存在很大差别):

通过测试数据咱们能够得出以下几点论断:

  1. 机械硬盘读写效率方面,程序读写速率是随机读写性能的 100 倍左右;
  2. SSD 硬盘读写效率方面,程序读写速率是随机读性能的 10 倍左右;
  3. 硬盘的程序读写速率比内存随机读写还要快;

通过磁盘的读写效率咱们能够发现,数据的程序读写性能要远远高于随机读写,而是数据库的读写场景往往是随机的,为了进步性能效率,就须要尽量将随机读写转换成程序读写的实现形式。而这些拆分进去的各种日志文件就是其实现形式之一,当然还会有内存缓冲池 (Buffer Pool) 等其它伎俩一起配合来实现读写效率的晋升。

相似的场景,在数据库利用层面也是存在的,比方:举荐应用数据库自增 ID 作为主键。为什么有这条倡议呢?

这是因为 B + 树结构为了保护索引的有序性,在插入新值的时候须要做必要构造保护。如果插入的值比最大值 ID 大,则只须要在最初记录前面插入一个新记录。如果新插入的 ID 值在原先的有序记录两头,就须要移动前面的数据,空出对应的地位,而后填充新值。如果所在的数据页曾经满了,依据 B+ 树算法,这时候须要申请一个新的数据页,而后移动局部数据过来(页决裂),前面两种状况,性能会受到较大的影响。为了缩小地位移动和页决裂过程中数据的挪动,应用层保障新增的索引数据始终是程序追加模式(新增数据是索引数据的最大值),就十分有必要了。

以上示例,仅仅是本地数据一致性的简略窥探,如果叠加上集群、网络两个维度,实现分布式数据一致性,就变的更加具备挑战了。

二 分布式系统

为什么分布式系统中数据一致性会更加简单呢?次要体现在上面几点:

  1. 共享内存:分布式系统没有共享内存,不能像本地零碎一样,从共享内存中间接获取整个零碎的数据快照。而是须要别离取得各个过程(信道)的本地状态,再组合成全局状态;
  2. 全局时钟:分布式系统没有全局时钟,各个过程无奈正确取得事件音讯的时序关系, 状态的一致性难以保障;
  3. 网络超时:分布式环境下网络超时状态的存在,须要咱们找到具备高度容错个性的解决办法;

2.1 CAP 定理

CAP 定理也称为不可能三角束缚,是由加州大学伯克利分校 Eric Brewer 传授提出来的,他指出网络服务无奈同时满足以下三个个性:

  1. 一致性(Consistency):在某个写操作实现之后的任何读操作都必须返回该写操作写入的值,或者之后的写操作写入的值。即:各个数据备份的数据内容要保持一致且都为最新数据。
  2. 可用性(Availability):任何一个在线的节点收到的申请必须都做出响应。即:不管成功失败,都有回应。
  3. 分区容错性(Partition tolerance):容许网络失落从一个服务节点到另外一个服务节点的任意信息(包含音讯的提早、失落、反复、乱序,还有网络分区)。即:不同的节点可能会数据不统一,这种状况下咱们要保证系统还能失常运行。

依据 CAP 原理将数据库分成了满足 CA 准则、满足 CP 准则和满足 AP 准则三大类:

  1. CA – 单点集群,满足一致性,可用性的个性,分区容忍性受到限制。
  2. CP- 满足一致性,分区容忍性的个性,性能收到限度。
  3. AP – 满足可用性,分区容忍性的个性,数据一致性上受到限制。

CAP 定理通知咱们,在网络可能呈现分区故障的状况下,一致性和可用性(提早)之间必须进行衡量。以 Paxos 协定来看,它在 C 和 A 之间抉择了前者,即严格的一致性,而 A 则降级为大多数一致性(majority available),这和咱们接下来要介绍的 BASE 定理的抉择恰恰相反。

2.2 BASE 定理

BASE 定理是对 CAP 中一致性和可用性衡量的后果(网络带来的分区容错性无奈漠视),是对大型互联网分布式实际的总结,是基于 CAP 定理逐渐演变而来。

  1. 根本可用(Basically Available):指在分布式系统呈现不可预知的故障时(网络或存储故障等),容许损失零碎的局部个性来换取零碎的可用性。比方零碎通过断路爱护而引发疾速失败,在疾速失败模式下,反对加载默认显示的内容(动态化的或者被缓存的数据),从而保障服务仍然可用。
  2. 软状态(Soft state):指运行零碎中的数据存在中间状态,并认为该中间状态不会影响零碎的整体可用性和最终一致性,即容许零碎在不同节点的数据正本进行数据同步时存在延时。也就是说,如果一个节点承受了数据变更,然而还没有同步到其余备份节点,这个状态是被零碎所承受的,也会被标识为数据变更胜利。
  3. 最终一致性(Eventuallyconsistent):指零碎中所有的数据正本在通过一段时间的同步后,最终状态能达到统一。在分布式环境下,思考依赖服务和网络的不确定性,传统 ACID 事务会让零碎的可用性升高、响应工夫变长,这可能达不到零碎的要求,因而理论生产中应用柔性事务是一个好的抉择。

BASE 实践的核心思想就是:依照理论利用场景,优先满足分区容错性和可用性,采纳适当的形式来使零碎达到最终一致性(不谋求强一致性),这一实践思维对于咱们在设计业务零碎时,有很大的指导意义。

2.3 事件时序

在分布式系统中,不同的服务散布在不同的机器上,如何确定不同机器上的两个事件产生的先后顺序呢?首先解释下为什么分布式系统须要晓得两个事件的先后顺序。举个例子:分布式数据库中不同事务并发执行的时候,须要做事务隔离。隔离的一种做法是应用 MVCC(Multiple Version Concurrent Control)多版本并发管制,依据数据的版本号来管制该版本数据的可见性。这时候就须要晓得数据批改事件产生的先后顺序,能力正确的实现隔离性。

如何辨认事件产生的先后顺序?有以下两种思路,

  1. 逻辑工夫:只须要一个原子递增的序号标识每一个音讯即可。
  2. 物理工夫:间接采纳工夫戳来标识音讯程序。

Linux 将时钟分为零碎时钟 (System Clock) 和硬件 (Real Time Clock,简称 RTC) 时钟两种。零碎工夫是指以后 Linux Kernel 中的时钟,而硬件时钟则是主板上由电池供电的那个主板硬件时钟,这个时钟能够在 BIOS 的“Standard BIOS Feture”项中进行设置。当 Linux 启动时,零碎时钟会去读取硬件时钟的设置,而后零碎时钟就会独立于硬件运作。

那么咱们事实中是如何进行计时的呢?晚期应用惠更斯摆钟(擒纵轮),起初发现了有压电效应的石英石,只有施加电场就会触动,石英石加工到肯定尺寸,就会达到 32768 次 / 秒的触动频次,以此来记录时间。但噪声、温度、磁场、湿度等都会影响晶振频率的稳定性,所以石英晶振,每天大概会有秒级单位的误差。

有没有更加准确的计时技术呢?

有,那就是原子钟,通过原子能级跃迁之间的辐射震荡工夫,来确定工夫长度,因为微波震荡频率,受到太阳和地球的影响很小,可能做到 5400 万年误差不超过 1 秒,毛病是老本低廉。

在卫星定位系统上,就应用了原子钟。这是因为卫星定位场景中,对时间误差的容忍度很低。举个例子:如果工夫上有 100 毫秒的误差,那么引起的等效误差就有 30km(通过卫星和接收端的时间差乘以光速来确定两者之间的间隔),这对于地位定位来说,曾经是不能承受了。另外,实践上只须要三颗就能确定地位,为了保障工夫准确性,还会搭载一颗额定的卫星,来进行时间差的纠正。

思考到老本问题,目前计算机广泛应用的是石英晶振,每天会有秒级的误差。为了解决这个误差,NTP(Network Time Protocol)被提了进去,使计算机对其服务器或时钟源(如石英钟,GPS 等)做同步化,提供高精准度的工夫校对能力。NTP 的同步频率是能够本人设置的,Linux 默认最小工夫距离为 64s,默认最大工夫距离是 1024s(17 分钟左右)。

为什么在分布式系统中大部分都不间接应用物理时钟,而是应用逻辑时钟呢?

这是因为分布式系统中,从各自机器上获取物理时钟的工夫戳,而各台机器的物理时钟是很难齐全同步的,即便有 NTP(Network Time Protocol),精度也是无限的。这对于依赖时钟结构的零碎来说往往是难以承受了。常见的分布式 ID 生成算法:雪花算法(SnowFlake),为了避免 ID 反复,在设计的时候就不得不思考时钟回拨的场景。

因而分布式系统通常应用逻辑工夫来记录事件的程序关系。逻辑时钟实现计划有以下几种:

Lamport 时钟(LC)是 Leslie Lamport 在 1978 年的论文《Time, Clocks, and the Ordering of Events in a Distributed System》提出的,Lamport 逻辑时钟保障了因果关系(偏序)的正确性,但不保障相对时序的正确性。

向量时钟(VC)是 LC 的一种延长,可能提供全序关系的同时辨别其中的并发事件和因果事件。其思维是不同过程之间同步时钟的时候 不仅同步本人的时钟,还同步本人晓得的其余过程的时钟。如果过程数过多,这也会导致向量保护难度(不直观)和老本(网络通信)减少。联合 LC 和 VC 两种时钟的长处,之后又呈现了混合逻辑时钟(Hybrid Logical Clocks)。为了进一步升高网络通信开销,Google 反其道而行,采纳物理时钟 + 算法来实现记录事件程序,TrueTime 计划又应运而生,对这方面感兴趣的同学能够拓展学习。

三 罕用解决方案

后面介绍了分布式数据一致性的难点和理论知识,接下来咱们一起来理解一下以后实现分布式数据一致性的几种落地计划。

3.1 两阶段事务

XA 是由 X /Open 国际联盟提出的 Distributed Transaction Processing(DTP)模型。模型的根底就是两阶段提交协定。XA 被许多数据库(如 Oracle、DB2、SQL Server、MySQL)和中间件工具 (如 CICS 和 Tuxedo) 本地反对。协定定义了交易中间件与数据库之间的接口标准(即接口函数),交易中间件用它来告诉数据库事务的开始、完结以及提交、回滚等。XA 接口函数由数据库厂商提供。通常状况下,交易中间件与数据库通过 XA 接口标准,应用两阶段提交来实现一个全局事务。

两阶段(2PC)的组成:

  1. 提交事务申请(投票);
  2. 执行事务申请(提交或中断);

因为两阶段的执行在数据一致性 (协调者 commit 申请呈现网络故障时) 和单点故障(协调者呈现故障)方面都存在着很大的问题,所以又引入了【预提交】这一新阶段,被称为三阶段,如下:

  1. 提交事务申请(投票);
  2. 预提交申请(网络超时认为执行失败);
  3. 执行事务申请(提交或中断);

三阶段(3PC)事务是将【执行事务申请】一分为二,新增的【预提交】阶段,这解决了两类问题:

◦ 初步解决了申请超时的问题(如果超时,主动失败);

◦ 参与者超时没有收到到协调者的反馈(呈现单点故障时),则主动认为胜利,开始执行事务;

在事实中很少会抉择两(三)阶段事务的计划来解决分布式事务问题,次要有三个方面的起因:

◦ 在有些故障条件下(协调者宕机),会造成所有参与者占有读锁、写锁梗塞在第二阶段,须要人工干预能力持续,存在可用性隐患;

◦ 减少了协调者中间件,零碎变得复杂化;

◦ XA 事务的性能不高,很多业务场景难以承受;

如何解决 2PC 和 3PC 的存在的问题呢?

那就是引入多个协调者,同时引入主协调者, 并以主协调者的命令为基准,这就是一种最简略的 Paxos 算法。Paxos 的版本有: Basic Paxos、Multi Paxos、Fast-Paxos,具体落地有 Raft 和 Zookeeper 的 ZAB 协定。

小结一下,基于 XA 协定实现的分布式事务对业务侵入很小。它最大的劣势就是对应用方通明,用户能够像应用本地事务一样应用基于 XA 协定的分布式事务。XA 协定可能严格保障事务 ACID 个性。严格保障事务 ACID 个性是一把双刃剑,事务执行在过程中须要将所需资源全副锁定,它更加实用于执行工夫确定的短事务。对于长事务来说,整个事务进行期间对数据的独占,将导致对热点数据依赖的业务零碎并发性能消退显著。因而,在高并发的性能至上场景中,基于 XA 协定的分布式事务并不是最佳抉择。除了 两(三)阶段事务外,还有 TCC(Try Confirm Cancel:应用层的两阶段提交模型),Saga(大事务分解成多个独立的子事务)等分布式事务模型,这里就不再开展探讨了。

3.2 本地音讯表

本地音讯表这个计划最后是 ebay 提出的,此计划的外围是将须要分布式解决的工作通过消息日志的形式来异步执行。消息日志能够存储到数据库、本地文件或音讯队列,再通过业务规定主动或手动发动重试。上面我就以音讯存储到数据库为例,借助数据库本地事务来实现音讯的牢靠投递。

假如咱们有一个服务,须要跨网络更新两个数据库 A 和 B,因为网络调用后果除了返回胜利,失败两种后果之外,还有一种状态那就是超时。超时这种状态就比拟让人头疼了,它到底是胜利了还是失败了呢?都有可能,具体后果无奈确定,数据一致性失去了挑战。如何解决这个问题呢?具体计划这样的:

  1. 在数据库 A 中创立一张音讯表,在进行业务解决时,将业务数据和音讯数据通过数据库事务一起长久化;(保障音讯的牢靠存储)
  2. 启动一个音讯定时投递工作,将音讯表中【待投递状态】的记录投递到 MQ 中去,直至投递胜利,批改音讯状态为【已投递】;(保障音讯的牢靠投递)
  3. MQ 生产方进行音讯解决(ack+ 重试机制 + 幂等设计),执行业务逻辑,写业务数据到数据库 B,并将执行后果同步给 MQ 生产方,MQ 生产方失去回传后果,胜利则更新音讯状态为【已实现】,失败则更新音讯状态为【待投递状态】(业务上的失败则执行事务的回滚逻辑,音讯状态更新为【已勾销】);
  4. MQ 生产方定时扫描本地音讯表,把还没解决实现的音讯或者失败的音讯再投递一遍;

小结一下,本地音讯表模式,是实现柔性事务的一种实现计划,外围是将一个分布式事务拆分为多个本地事务,事务之间通过事件音讯连接,事件音讯和上个事务共用一个本地事务存储到本地音讯表,再通过定时工作轮询本地音讯表进行音讯投递,上游业务订阅音讯进行生产,实质上是依附音讯的重试机制达到最终一致性。

3.3 MQ 音讯

本地音讯表实现数据一致性的计划在某些场景下十分有用,但整个实现逻辑比较复杂;在一些不是特地重要外围的业务场景中,为了升高应用老本,很多时候就把音讯表给去掉了,间接在本地事务之外发送 MQ 音讯,音讯生产方执行完业务逻辑后,再回传执行状态(甚至容许不回传)。通过损失一部分确定性,来轻量级的实现数据同步逻辑。应用这种计划的前提是 MQ 服务的稳定性保障要做到位,否则呈现问题的概率将大大提高。

如果用 ACID 来掂量该计划,基于可靠消息服务的分布式事务计划能保障事务的最终原子性和持久性,但无奈保障一致性和隔离性。数据库的隔离性是通过锁机制来保障的,同样的思路,要想恪守隔离性准则,往往还须要在事务发起方采纳分布式锁机制来实现。总体来说,基于可靠消息服务的分布式计划实用于对业务的实时一致性以及事务的隔离性要求都不高的外部零碎。

3.4 事务音讯

有一些 MQ 是反对事务音讯的,比方 JMQ,RocketMQ,它们反对事务音讯的形式相似于采纳的二阶段提交。

以 RocketMQ 中间件为例,其思路大抵为:

• 第一阶段 Prepared 音讯,会拿到音讯的地址;

• 第二阶段执行本地事务;

• 第三阶段通过第一阶段拿到的地址去拜访音讯,并批改状态;

具体流程可参照下图:

也就是说在业务办法内要向音讯队列提交两次申请,一次发送音讯和一次确认音讯。如果确认音讯发送失败了 RocketMQ 会定期扫描音讯集群中的事务音讯,这时候发现了 Prepared 音讯,它会向音讯发送者确认,所以生产方须要实现一个 check 接口,RocketMQ 会依据发送端设置的策略来决定是回滚还是持续发送确认音讯。这样就保障了音讯发送与本地事务同时胜利或同时失败。

四 并发管制

4.1 背景

咱们常常须要在业务解决服务之上加一个缓存层,一方面为了进步了响应效率,另一方面也能节俭了上游算力,是一种比拟常见的服务优化伎俩。然而,在缓存层的实现计划上,却有很多种实现形式,其中有些实现计划却存在着很多坑,须要留神躲避。

4.2 常见问题

  1. 坑一,将写缓存、发送 MQ、调用 RPC 服务与数据库事务绑定在一起了。波及到跨网络交互的操作,服务质量重大依赖于各端网络品质,如果网络呈现抖动或者中间件服务器呈现故障,就会引起本地数据库事务的响应时长的降级,短时事务变成了长时事务,数据库链接资源被长时间占用,得不到开释,引起吞吐量刹时降落,进而影响其它业务的失常数据库操作;
  2. 坑二,并发场景下的数据一致性问题,典型场景如下:

如何解决下面提到的“先发后至”的问题呢?针对这种应用场景,这里提供一些方案设计思路。

4.3 解决方案

• 计划一,被动让缓存穿透,触发从新从数据源读取最新数据;

无论是先写数据库再写缓存,还是先写缓存再写数据库都会存在数据不统一的问题。换一种思路,不再寻求写数据的统一,而是在读数据的时候可能保持一致也能够。外围流程如下:

◦ 写完数据库动作后,被动将缓存中的老数据进行删除;

◦ 在应用数据的时候,发现缓存数据为空(未命中),则被动触发读数据库中的最新数据;

◦ 缓存穿透的场景,再将最新数据同步写入缓存即可;

尽管这种形式不能 100% 保证数据一致性,但不统一的概率大大降低了。

• 计划二,读写拆散,通过音讯或文件触发同步更新缓存数据;

这种计划的外围是:将写缓存的操作从主业务逻辑中独立进去,比方通过发送一个变更音讯或者订阅数据库 binlog 日志,通过变更音讯查询数据库的最新数据同步到缓存中去。如下图(其中步骤 4 和 5 为可选项):

◦ 写申请操作数据库;

◦ 异步工作订阅数据库 binlog 日志(MQ 音讯也能够),并触发写缓存操作;

◦ 读申请直读取缓存数据;

计划小结,计划一比较简单,容易实现。但因为存在大概率的缓存穿透的场景,在有频繁批改,高并发的场景下,数据库承压比拟大,服务的高可用很难失去保障。计划二实现了读写职责拆散(CQRS 架构设计),实现上简单一些。读操作基本上靠缓存,比拟实用于并发量高,时效敏感度低的利用场景。

五 总结

目前,分布式数据一致性问题还没有普世通用的解决方案,它须要从业务需要的角度登程,确定对各种一致性模型的接受程度,再通过具体场景来抉择解决方案。从利用角度看,分布式事务的事实场景经常无奈躲避,特地是对波及金融类的业务,数据一致性是底线,业务须要对数据有百分之百的掌控力。而个别的电商交易场景,应用基于音讯队列的柔性事务框架是不错的抉择。最初,附几种事务模型的性能比照表:

关注点 本地事务 两(三)阶段事务 柔性事务
业务革新 实现协定接口
一致性 不反对 反对 最终统一
隔离性 不反对 反对 应用层保障
并发性能 无影响 重大消退 稍微消退
适宜场景 繁多数据源 短事务 & 低并发 长事务 & 高并发

注:文中局部图片来自于互联网

正文完
 0