第二部分:分布式数据

处于各种原因,希望将数据库分布到多台机器上:

  • 可伸缩性
    负载分散到多台机器上
  • 容错/高可用性
    单台/多台机器出现故障的情况下仍然能继续工作,可以使用多台机器,以提供冗余
  • 延迟

伸缩至更高的载荷

第五章:复制

可能出于各种各样的原因希望能复制数据

  • 使得数据与用户在地理上接近,从而减少延迟
  • 系统一部分出现故障也能继续工作,从而提高可用性
  • 伸缩可以接收请求的机器数量,从而提高吞吐量

复制的困难之处在于处理复制数据的变更

三种流行的变更复制算法

  • 单领导者(single leader,单主)
  • 多领导者(multi leader, 多主)
  • 无领导者(leaderless, 无主)

复制时需要进行很多权衡

  • 使用同步复制还是异步复制

  • 如何处理失败的副本

    ······

领导者和追随者

存储了数据库拷贝的每个节点称为副本(replica)

存在多个副本时,如何确保所有数据都落在所有副本上?

最常见的方案被称为主/从复制(master/slave) (也称为基于领导者的复制(leader-based replication)主动/被动复制(active/passive) ),其工作原理如下

  1. 其中一个副本被指定为 领导者(leader) ,也称为 主库(master|primary) 。当客户端要向数据库写入时,它必须将请求发送给该 领导者,其会将新数据写入其本地存储。
  2. 其他副本被称为 追随者(followers) ,亦称为 只读副本(read replicas)从库(slaves)备库( secondaries)热备(hot-standby) 。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为 复制日志(replication log)变更流(change stream) 。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照与领导者相同的处理顺序来进行所有写入。
  3. 当客户想要从数据库中读取数据时,它可以向领导者或任一追随者进行查询。但只有领导者才能接受写入操作(从客户端的角度来看从库都是只读的)。

image-20230821192328128

同步复制和异步复制

复制系统的一个重要细节是:复制是同步(synchronously) 发生的还是异步(asynchronously) 发生的

image-20230821192339238
(Follower1是同步,Follower2是异步)

同步复制

  • 优点是,从库能保证有与主库一致的最新数据副本,如果主库突然失效,我们可以确信这些数据仍然能从库上找到
  • 缺点是,如果同步从库没有相应(比如已经崩溃,或者出现网络故障或其它原因),主库就无法处理写入操作,主库必须阻止所有写入,并等待同步副本再次可用

将所有库都设置为同步是不切实际的,通常数据库上启用同步意味着一个从库是同步的,其它从库是异步的,当同步从库变得不可用或缓慢时将一个异步从库改为同步。这保证主库和同步从库上拥有最新的数据。这种配置有时也被称为半同步(semi-synchronous)

通常基于领导者的复制都配置为完全异步

  • 缺点是:这种情况下几遍向客户端确认成功写入也不能保证是持久(Durable) 的,因为如果主库失效且不可恢复,尚未复制给从库的写入都会丢失。
  • 优点是:及时所有的从库都落后了,主库也可以继续处理写入

链式复制(chain replication) 是同步复制的一种变体,为解决主库故障时的数据丢失而研究

设置新从库

如何保证新的从库拥有主库数据的精确副本?

简单的复制是没有意义的,因为数据库在不断地变化,复制内容中的不同部分可能包含不同时间点的内容,也可以锁定数据库来保持一致,但会违背高可用的目标

理想过程

  1. 在某个时刻获取主库的一致性快照(如果可能,不必锁定整个数据库)
  2. 将快照复制到新的从库节点
  3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置(例如MySQL 将其称为 二进制日志坐标(binlog coordinates) )精确关联
  4. 完成快照之后累计的数据变更,赶上(caught up)主库

处理节点宕机

如何节点都可能宕机,目标是即使个别节点失效,也能保持整个系统运行

从库失效:追赶恢复

从库可以从日志中知道,在发生故障之前处理的最后一个事务

从库可以重新连接到主库,并请求在从库断开期间发生的所有数据变更,并在完成所有这些变更之后赶上主库

主库失效:故障切换

处理起来相当棘手:其中一个库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其它从库需要开始拉取来自新主库的数据变更,这个过程被称为故障切换(failover)

故障切换可以手动进行或自动进行

手动进行:通知管理员主库挂了,采取必要步骤创建新的主库

自动故障切换通常有如下步骤:

  1. 确认主库失效。大多数系统只是简单的使用超时(timeout) :节点频繁相互传递消息,如果一个节点在一段时间内没有响应就认为它挂了
  2. 选择一个新的主库:可以通过选举来完成,或者由之前选定的控制器节点来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库。让所有节点同意一个新的领导者是一个共识问题
  3. 重新配置系统以启用新的主库。客户端需要将它们的写请求发送给新的主库。如果旧主库恢复,系统需要确保旧主库意识到新主库的存在,并成为一个从库

故障切换的过程中很多地方可能出错

  • 如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作。如果旧主库重新加入集群,如何处理这写没有复制的写入。在此期间,新主库可能已经收到了与这部分写入相冲突的写入,最常见的办法是丢弃老主库未复制的写入
  • 如果数据库需要和其它外部存储相协调,那么丢弃写入内容是极其危险的操作
  • 发生某些故障时可能会出现两个节点都以为自己是主库的情况,称为脑裂(split brain) 。这种情况非常危险,如果两个主库都可以接受写操作,却没有冲突解决机制,那么数据就可能丢失或损坏
  • 主库被宣告死亡之前的正确超时应该怎么配置?在主库失效的情况下,超时时间越长意味着恢复时间越长。但如果超时时间设置太短,又可能出现不必要的故障切换

这些问题没有简单的解决方案。因此,即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障切换

复制日志的实现

基于语句的复制

最简单的情况下,主库记录下它执行的每个写入请求语句,并将该语句日志发送给从库

问题

  • 非确定性函数(nondeterministic) 的语句可能会产生不同的值,例如NOW()
  • 如果使用了自增列,或者依赖于数据库中的现有数据,则必须在每个副本上按照完全相同的顺序执行它们,否则可能产生并的结果。当有多个并发执行的事务时,这可能会称称为一个限制
  • 有副作用的语句(例如:触发器,用户定义函数,存储过程),可能产生不同的副作用

这些问题可以解决,但由于边缘情况太多,通常选择别的复制方法

传输预写式日志(WAL)

  • 对于日志结构存储引擎,日志是主要的存储位置
  • 对于覆写单个磁盘块的B树,每次修改都会先写入预写式日志(WAL),以便崩溃后索引可以恢复到一致状态

任何情况下,该日志都包含了所有数据库写入的仅追加字节序列,可以使用完全相同的日志在另一个节点上构建副本

缺点是日志记录的数据非常底层,WAL包含哪块磁盘块中的哪些字节发生了改变。这使复制与存储引擎紧密耦合。通常不可能在主库和从库上运行不同版本的数据库软件。这使得升级时可能需要停机

逻辑日志复制(基于行)

关系型数据库的逻辑日志通常以行的粒度来描述对数据库表的写入记录的序列:

  • 对于插入行,日志包含所有列的新值
  • 对于删除的行,日志包含足够的信息来唯一表示被删除的行
  • 对于更新的行,日志包含足够的信息来唯一标识被更新的行,以及所有列的新值

修改多行的事务会生成多条这样的日志记录,后面跟着一条指明事务已经提交的记录

由于逻辑日志与存储引擎解耦,系统可以更容易向后兼容,主库和从库能够运行不同版本的数据库,或者不同的存储引擎

对外部应用程序来说,逻辑日志格式更容易解析,更容易实现 数据变更捕获(change data capture)

基于触发器的复制

触发器允许你将数据更改发生时自动执行的自定义程序代码注册在数据库系统中。这有机会将更改记录到一个单独的表中去,使用外部程序读取这个表加上一些业务逻辑就可以将数据变更复制到另一个系统中去

优点是灵活性更高,缺点是开销更高,且更容易出错

复制延迟问题

应用程序从异步从库读取时,如果从库落后,可能会看到过时的信息。同时对主库和从库进行查询结果可能不一致。如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致,这种效应称为最终一致性(eventual consistency)

复制延迟(relication lag) ,即写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒。但如果系统接近极限的情况,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至达到几分钟

复制延迟时可能发生一些问题

读己之写

对于异步复制,如果在写入之后马上查看数据,则新数据可能尚未到达副本。对于用户而言,看起来好像是刚提交的数据丢失了

在这种情况下,我们需要写后读一致性(read-after-write consistency) ,也称为读己之写一致性(read-your-writers consistency) 。这是一个保证,保证用户重新加载页面,他们总会看到他们提交的任何更新。它不会对其他用户的写作出承诺,其他用户的更新可能仍要等待一段时间才能看到。它保证用户自己的输入已被正确保存

实现的技术

  • 对于用户可能修改过的内容,总是从主库读取;这需要有办法不通过实际的查询就可以知道用户是否修改了东西。例如社交网络上用户只能修改自己的资料:总是从主库读取紫的档案,从从库读取其它用户的档案
  • 如果大部分内容都必须从主库读取,可以更换其它标准,例如更新过后的一分钟内从主库读。还可以监控从库的复制延迟,防止向任何滞后主库超过一分钟的从库发出查询
  • 客户端可以记住最近一次写入的时间戳,系统需要确保从库处理该用户的读取请求时,该时间戳前的变更都已经传播到了本从库中,如果不够新,旧换一个从库,或者等待从库赶上主库
  • 如果你的副本分布在多个数据中心,还会有额外的复杂性。任何需要由主库提供服务的请求都必须路由到包含该主库的数据中心

另一种复杂性是可能需要提供跨设备的写后读一致性:如果用户在一个设备上输入了一些信息,然后在另一个设备上查看,则应该看到他们刚输入的信息

这会导致一些额外的问题

  • 记住用户上次更新时间的方法变得困难,因为不知道另一个设备上发生了什么,需要对这些元数据进行中心化的存储
  • 如果副本分布在不同的数据中心,很难保证来自不同设备的连接会路由到同一数据中心。如果使用的方法需要读主库,可能首先需要把来自该用户所有设备的请求都路由到同一个数据中心

单调读

异步从库读取时可能发生的异常的第二个例子是用户可能会遇到时光倒流(moving backward in time)

如果首先查询了一个延迟很小的从库,然后查询一个延迟较大的从库,就可能发现先看到的东西又消失的情况

image-20230821193038499

单调读(monotonic reads)可以保证这种异常不会发生

  • 比强一致性更弱,比最终一致性更强的保证
  • 用户顺序地进行多次读取
  • 实现的方式是确保一个用户总是从同一个副本进行读取

一致前缀读

image-20230821193627899

如果某些分区的复制速度慢于其它分区,例如提问的分区复制速度慢于回答的分区复制速度,那么观察者可能会在看到问题之前先看到答案

一致前缀读(consistent prefix reads)保证,如果一个系列写入按照某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现

这是分区或分片数据库中的一个特殊问题。如果数据库总是以相同的顺序应用写入,而读取总是看到一致的前缀,那么这种异常不会发生。但是许多分布式数据库中,不同分区独立运行,因此不存在全局的写入顺序(当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态)

一种解决方法是,确保任何因果相关的写入都写入相同分区,还有一些显式跟踪因果依赖关系的算法

如果仅保证单调读,还是可能存在这个库的的分区复制速度导致因果问题

复制延迟的解决方案

应用程序可以提供比低层数据库更强有力的保证,例如通过主库进行某种读取,但在应用程序代码中处理这些问题是复杂的

数据库通过事务提供强大的保证,使得应用程序开发人员不必担心一些微妙的问题,应用程序可以更加简单

走向分布式数据库时,许多系统放弃了事务,声称事务在性能和可用性上的代价太高,并断言在可伸缩系统中最终一致性是不可避免的。这个叙述有一些道理,但不完全正确

多主复制

允许多个节点写入,处理写入的每个节点都必须将该数据变更转发给所有其他节点,我们将其称之为 多领导者配置(multi-leader configuration,也称多主、多活复制,即 master-master replication 或 active/active replication)

多主复制的应用场景

在单个数据中心内部使用多个主库的配置没有太大意义,因为其导致的复杂性已经超过了能带来的好处

运维多个数据中心

假如有一个数据库,副本分散在好几个不同的数据中心。多主配置可以在每个数据中心都有主库。数据中心内使用常规的主从复制。数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中

image-20230821202739332

运维多个数据中心时,单主和多主的适应情况

性能 容忍数据中心停机 容忍网络问题问题
单主 每个写入都穿过互联网 主库所在的数据中心发生故障,必须切换另一个数据中心里的从库成为主库 网络中断影响写入
多主 每个写操作都可以在本地数据中心处理 每个数据中心独立于其它,当故障数据中心恢复时复制会自动赶上 临时的网络中断不会妨碍正在处理的写入

多主复制的缺点

  • 两个不同的数据中心可能会同时修改相同数据,需要解决写冲突
  • 常与其他数据库功能之间出现意外反应,例如自增主键、触发器、完整性约束等都可能会有麻烦

需要离线操作的客户端

多主复制的另一种适用场景是:应用程序断网之后仍然需要继续工作

设备离线状态下进行更改,设备下次上线时需要与服务器和其他设备同步。在这种情况下,每个设备都有一个充当书库的本地数据库,并且在所有设备上的副本之间同步时,存在异步的多主复制过程,复制延迟取决于何时联网

协同编辑

实时协作编辑应用程序允许多个人同时编辑文档。当一个用户编辑文档时,所做的更改将立即应用到其本地副本(Web 浏览器或客户端应用程序中的文档状态),并异步复制到服务器和编辑同一文档的任何其他用户

如果要保证不会发生编辑冲突,则应用程序必须取得文档的锁定才能操作,换句话说保证同时只有一个人操作

为了加速协作,可能希望将更改的单位设置得非常小,big避免锁定,但同时也带来了多主复制的所有挑战,包括需要解决冲突

处理写入冲突