Kyle's Notebook

《DDIA》阅读笔记(五):数据复制(单主节点)

Word count: 4.6kReading time: 15 min
2021/01/09

《DDIA》阅读笔记(五):数据复制(单主节点)

本章结构如下:

  • 数据复制
    • 单主节点复制
      • 同步/异步复制
      • 配置新从节点
      • 处理节点失效
        • 从节点失效:追赶式恢复
        • 主节点失效:节点切换
          • 处理步骤
            • 确认主节点失效
            • 选举新的主节点
            • 使新主节点生效
          • 处理步骤
            • 数据丢失
            • 数据不一致
            • 脑裂
            • 超时
      • 复制日志的实现
        • 基于语句的复制
        • 基于预写日志的复制
        • 基于行逻辑日志的复制
        • 基于触发器的复制
      • 复制滞后问题
        • 一致性模型
          • 写后读
            • 主节点处理
            • 更新时间戳
            • 多数据中心
          • 单调读
          • 前缀读
        • 解决方案

本篇介绍多种复制架构原理、存在问题及其解决方案。

数据复制指通过互联网络在多台机器上保存相同数据的副本,其目的是:

  • 低延迟:使数据在地理位置上更接近用户,从而降低访问延迟。

  • 高可用:当部分组件出现故障,系统依然可以继续工作,从而提高可用性。

  • 可扩展:扩展至多台机器以同时提供数据访问服务,从而提高读吞吐量。

考虑数据复制问题的预设前提:

  • 数据规模较小,集群的每一台机器都可以保存数据集的完整副本(否则必须分区)。

  • 数据持续更改,需要不断复制。

  • 考虑采用异步/同步复制、如何处理失败副本。

  • ……

其中主从复制较流行,因为相对简单、不需要担心冲突问题,但出现可能节点失效、网络中断或者延迟抖动等情况。

客户端写入操作发送到主节点,由主节点负责将数据更改事件发送到其他从节点。每个副本可以接收读请求,但内容可能是过期值。

PostgreSQL(9+)、MySQL、Oracle、SQL Server 等关系型数据库,Kafka、RabbitMQ 等消息队列,以及一些网络文件系统或复制块设备都支持主从复制:

  • 某一个副本为主副本(主节点),客户写数据库时将写请求首先发送给主副本,写入本地存储。

  • 其他副本为从副本(从节点)。主副本把新数据写入本地存储后,将 数据更改 作为复制日志或更改流发送给所有从副本。每个从副本获得更改日志之后,严格保持与主副本相同的写入顺序应用到本地。

  • 客户端从数据库中读数据时可在主副本或从副本上执行查询,但只有主副本才可以接收写请求,在客户端的角度从副本只读。

主从复制

同步与异步

两种复制方式区别在于主节点接收到客户端的写入请求时,是否需要等待从节点确认写入后才返回。

  • 同步复制:最新的写入对其他客户端可见,其优势在于保证向客户端确认时主从节点已经完成同步,即使主节点发生故障,从节点可以继续提供最新的数据,但如果从节点无法完成确认,则写入不能视为成功(阻塞)。

  • 异步复制:优势在于客户端不需要等待从节点的日志接收延迟,但从节点会比主节点落后(这个时间不确定,如从节点刚从故障中恢复、已接近最大的设计上限或节点间网络出现问题)。完全异步可以保证高吞吐量,但当主节点发生失败且不可恢复时,有尚未复制到从节点的写请求都会丢失,即使已响应写入请求仍无法持久化。

同步复制与异步复制

主节点发生故障时、异步复制系统可能会丢失数据的问题非常严重,在保证数据不去失的前提下,采用诸如 链式复制 等方法可以提高复制性能与系统可用性。

采用同步复制或异步复制,是在复制性能和系统可用性之间平衡。实践中可采用 半同步复制:把某一个从节点设为同步、而其他节点则是异步模式。当同步的从节点变得不可用或性能下降,则将另一个异步的从节点提升为同步模式。

对于关系数据库系统,同步或异步通常是一个可配置的选项;而其他系统则可能是硬性指定或者只能二选一。

配置新从节点

如需要增加副本数以提高容错能力,或者替换失败的副本,就需要考虑增加新的从节点。

要确保高可用,在不停机、数据服务不中断的前提下完成操作,需完成以下步骤:

  • 对主节点的数据副本产生一致性快照,避免长时间锁定整个数据库。

  • 将此快照拷贝到新的从节点。

  • 从节点连接到主节点并请求快照点之后所发生的数据更改日志。在第一步创建快照时,快照与系统复制日志的某个确定位置相关联(如 PostgreSQL 的 log sequence number、MySQL 的 binlog coordinates) 。

  • 追赶:获得日志之后,从节点应用这些快照点之后所有数据变更。

  • 继续处理主节点上新的数据变化,重复以上步骤。

目前大多数数据库都支持此功能, 快照也是系统备份所必需的。有时需要第三方工具,如 MySQL innobackupex。

处理节点失效

多副本、多节点必然存在节点失效的情况,关键是减少节点中断带来的影响。

从节点失效:追赶式恢复

由于从节点保存了副本收到的数据变更日志,如果崩溃然后顺利重启,或者主从节点之间的只是暂时的网络中断(闪断),则恢复比较容易。

根据副本的复制日志,从节点可知在发生故障之前处理的最后一笔事务,连接到主节点并请求自那笔事务之后中断期间内所有的数据变更。

在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后如常持续接收来自主节点数据流的变化。

主节点失效:节点切换

  • 选择某个从节点将其提升为主节点。

  • 客户端更新,使写请求会发送给新的主节点。

  • 其他从节点接受来自新主节点上的变更数据。

具体步骤

  • 确认主节点失效:大多数系统采用了超时机制。节点间频繁地互相发生发送心跳存活消息,如某一个节点在一段比较长时间内没有响应即认为该节点失效。

  • 选举新的主节点:选举新主节点需超过多数的节点达成共识,候选节点最好与原主节点的数据差异最小,可以最小化数据丢失的风险。

  • 使新主节点生效:重新配置系统使新主节点生效。客户端现在需要将写请求发送给新的主节点,如果原主节点之后重新上线,可能仍然自认为是主节点,此时系统需要确保原主节点降级为从节点,并认可新的主节点。

存在问题

  • 数据丢失:如果使用了异步复制,且失效之前新的主节点并未收到原主节点的所有数据,在选举之后,原主节点很快又重新上线并加入到集群,新的主节点很可能会收到冲突的写请求。常见的解决方案是原主节点上未完成复制的写请求就此丢弃,但不能确保数据更新持久化。

  • 数据不一致:如在数据库外有其他系统依赖于数据库的内容并协同使用,切换主节点时丢弃无效数据存在危险:未完全同步的从节点被提升为主节点,新主节点的自增计数器落后于原主节点、重新使用了已被分配的主键,而恰好这些主键已被外部系统(如缓存)所引用,结果出现数据系统之间(缓存与数据库)的不一致,且可能导致某些私有数据被错误地泄露给其他用户。

  • 脑裂:在同一时刻两个节点都认为自己是主节点,两个主节点都可能接受写请求,最后数据可能会丢失或者破坏。有些系统会采取措施来强制关闭其中一个节点,有时可能出现两个节点都被关闭的情况。

  • 超时:主节点失效后,超时时间设置得越长也意味着总体恢复时间就越长,设置得太短也会导致太多不必要的切换,因此需要配置合适的超时时间。

复制日志的实现

基于语句

主节点将每个写请求语句作为日志发送给从节点,但也有一些不适用的场景:

  • 任何调用非确定性函数的语句:如 NOW() 获取当前时间、RAND() 获取随机数,在不同的副本上可能产生不同的值。

  • 如果语句中使用了自增列,或者依赖于数据库的现有数据,则所有副本都必须按相同顺序执行,如果有多个同时执行的并发事务,则有很大限制。

  • 有副作用的语句(如触发器、存储过程、自定义函数),可能会在每个副本上产生不同的副作用。

基于操作语句进行复制需要结合一些特殊措施,比如主节点在记录操作语句时将非确定性函数替换为执行之后的确定结果,所有节点直接使用相同的结果值。

由于存在太多边界条件需要考虑,目前通常首选其他复制实现方案。

基于预写日志

预写日志(WAL)即每个写操作都是以追加写的方式写入到日志中:

  • 对于日志结构存储引擎(比如 SSTables 和 LSM-trees),日志时主要的存储方式。日志段在后台压缩并支持垃圾回收。

  • 对于采用覆写磁盘的 B-trees 结构,每次修改会预先写入日志,如系统崩溃,通过索引更新的方式迅速恢复到此前一致状态。

所有对数据库写入的字节序列都被记入日志。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主节点还可以通过网络将其发送给从节点。从节点收到日志进行处理,建立和主节点内容完全相同的数据副本。

缺点在于是日志描述的数据结果比较底层:包含了哪些磁盘块的哪些字节发生改变等,使复制方案和存储引擎紧密耦合。

基于行逻辑日志

复制和存储引擎采用不同的日志格式:

  • 复制与存储逻辑剥离,易于向后兼容。

  • 主从节点可以运行不同版本的软件或不同的存储引擎。

  • 对于外部应用程序而言,逻辑日志格式也更容易解析。

对于关系数据库,日志通常是指一系列记录来描述数据表行级别的写请求:

  • 对于行插入,日志包含所有相关列的新值。

  • 对于行删除,日志里有足够的信息来唯一标识已删除的行,通常是靠主键,但如果表上没有定义主键,就需要记录所有列的旧值。

  • 对于行更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少包含所有已更新列的新值)。

如果一条事务涉及多行的修改会产生多个日志记录,并在后面跟着一条指出该事务已经提交的记录,如 MySQL 的 binlog(当配置为基于行的复制时)。

变更数据捕获:对于外部应用程序逻辑日志格式更容易解析,如果要将数据库的内容发送到外部系统(如用于离线分析的数据仓库),或构建自定义索引和缓存等,基于逻辑日志的复制更有优势。

基于触发器

不依赖于数据库系统可提高灵活性,即通过代码在应用程序层实现复制。比如只复制数据的一部分、异构数据库复制、自定义冲突解决逻辑。

一种方式是通过其他工具读取数据库日志让应用程序获取数据变更(比如 Oracle GoldenGate),另一种方法则是借助触发器和存储过程(将数据更改记录到一个单独的表,外部处理逻辑访问该表、实施自定义应用层逻辑,比如 Oracle 的 Databus 和 Postgres 的 Bucardo)。

基于触发器的复制通常比其他复制方式开销更高,也比数据库内置复制更容易出错,或者暴露一些限制。

复制滞后问题

主从复制要求所有写请求都经由主节点,而任何副本只能接受读请求。

  • 只需添加更多的从副本,就可以提高读请求的服务吞吐量。

  • 如果使用同步复制,单点故障或网络中断会使整个系统无法写入,节点越多故障发生概率越高,越扩展越不可靠。

  • 使用异步复制在短时间内必然存在复制滞后问题、数据不一致,只能在从节点追赶上后确保最终一致性。

写后读

写后读一致性保证用户总能看到自己所提交的最新数据。

许多应用让用户提交数据后查看自己提交的内容,在异步复制时,写入数据发生在主节点、读取数据发生在从节点,则可能出现读取时新数据未同步到从节点的情况,在客户端看来数据写入不成功。

读自己的写

读写一致性机制保证如果用户重新加载页面,他们总能看到自己最近提交的更新。

主节点处理:

  • 如果用户访问可能会被修改的内容,从主节点读取,否则在从节点读取。这要求一些方法在实际执行查询之前就已经知道内容是否可能会被修改,比如主节点读取用户自己的首页配置文件,从节点读取其他用户的配置文件。

  • 如果应用大部分内容都可能被所有用户修改,该方法将不太有效,它会导致大部分内容都必须经由主节点,丧失了读操作的扩展性。此时需要其他方案来判断是否从主节点读取。

更新时间戳时要求客户端记住最近更新时的时间戳(逻辑时间或系统时间)并附带在读请求中,据此系统可确保对该用户提供读服务时都应该至少包含了该时间戳的更新(选择足够新的副本处理读请求)。但还是存在以下问题:

  • 服务端与客户端的时钟同步。

  • 多端设备应用(比如浏览器和 APP)难以记住用户上次更新时间戳,同一设备无法完全获知其他设备的信息,此时元数据必须全局共享。

对于多数据中心的情况(出于地理位置、高可用的考虑,副本可能分布在多数据中心):

  • 必须先把请求路由到主节点所在的数据中心。

  • 对于多端设备应用,如果副本分布在多数据中心,如果要求必须从主节点读取,需要确保来自不同设备的请求路由到同一数据中心。

单调读

即在某个时间点读到数据之后,保证此后不会出现比该时间点更早的数据。

  • 需要确保同一用户总是从固定的同一副本执行读取,如基于用户 id 哈希的方法选择副本。

  • 如果该副本失效,用户的查询须重新路由到另一个副本。

  • 其一致性介乎强一致性和最终一致性之间

单调读

前缀读

前缀读保证对于一系列按照某个顺序发生的写请求,则读取这些内容时也会按照当时写入的顺序。

  • 如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生顺序错乱反常。

  • 在分区(分片)数据库中不同分区独立运行,不存在全局的写入顺序,这导致读数据时可能会看到数据库的某部分旧值和另一部分新值。

  • 解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,这将大大降低效率。

前缀一致读

目前有一些新的算法来显式地追踪事件因果关系(Happened Before)。

解决方案

  • 应用层保障:在应用层可以提供比底层数据库更强有力的保证,代价是应用层代码中处理这些问题通常非常复杂且容易出错。

  • 事务:如不必担心底层复制问题,假定数据库必然正确,则可以利用事务。单节点事务已很成熟,但分布式往往放弃支持事务而确保最终一致性。


多主节点和无主节点复制方案会更可靠,但代价是系统的复杂性和弱一致性保证(待续)。

CATALOG
  1. 1. 《DDIA》阅读笔记(五):数据复制(单主节点)
    1. 1.1. 同步与异步
    2. 1.2. 配置新从节点
    3. 1.3. 处理节点失效
      1. 1.3.1. 从节点失效:追赶式恢复
      2. 1.3.2. 主节点失效:节点切换
        1. 1.3.2.1. 具体步骤
        2. 1.3.2.2. 存在问题
    4. 1.4. 复制日志的实现
      1. 1.4.1. 基于语句
      2. 1.4.2. 基于预写日志
      3. 1.4.3. 基于行逻辑日志
      4. 1.4.4. 基于触发器
    5. 1.5. 复制滞后问题
      1. 1.5.1. 写后读
      2. 1.5.2. 单调读
      3. 1.5.3. 前缀读
      4. 1.5.4. 解决方案