第五章 复制
最后更新于:2022-04-02 04:18:33
[TOC]
## 概述
三种流行的变更复制算法:**单领导者(single leader)**,**多领导者(multi leader)**和**无领导者(leaderless)**。几乎所有分布式数据库都使用这三种方法之一
## 领导者与追随者
最常见的解决方案被称为**基于领导者的复制(leader-based replication)**(也称**主动/被动(active/passive)**或**主/从(master/slave)**复制)
1. 副本之一被指定为**领导者(leader)**,也称为**主库(master|primary)**。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
2. 其他副本被称为**追随者(followers)**,亦称为**只读副本(read replicas)**,**从库(slaves)**,**备库( sencondaries)**,**热备(hot-standby)**。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志(replication log)**记录或**变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)
## 同步复制与异步复制
复制系统的一个重要细节是:复制是**同步(synchronously)**发生还是**异步(asynchronously)**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)
** 同步复制**
1. 优点: 从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。
2. 缺点: 如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用
**半同步**
在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库
## 设置新从库
有时候需要设置一个新的从库:也许是为了增加副本的数量,或替换失败的节点
1. 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如MySQL的innobackupex 【12】。
2. 将快照复制到新的从库节点。
3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称:例如,PostgreSQL将其称为**日志序列号(log sequence number, LSN)**,MySQL将其称为**二进制日志坐标(binlog coordinates)**。
4. 当从库处理完快照之后积压的数据变更,我们说它**赶上(caught up)**了主库。现在它可以继续处理主库产生的数据变化了
## 处理节点宕机
### 从库失效:追赶恢复
从库可以从日志中知道,在发生故障之前处理的最后一个事务。因此,从库可以连接到主库,并请求在从库断开连接时发生的所有数据变更。当应用完所有这些变化后,它就赶上了主库,并可以像以前一样继续接收数据变更流
### 主库失效:故障切换
主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为**故障切换(failover)**。
故障切换可以手动进行或自动进行。
自动故障切换过程
1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用**超时(Timeout)**:节点频繁地相互来回传递消息,并且如果一个节点在一段时间内(例如30秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点(controller node)**来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,。
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库。如果老领导回来,可能仍然认为自己是主库,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个从库
## 复制日志的实现
### 基于语句的复制
存在的问题
* 任何调用**非确定性函数(nondeterministic)**的语句,可能会在每个副本上生成不同的值。例如,使用`NOW()`获取当前日期时间,或使用`RAND()`获取一个随机数。
* 如果语句使用了**自增列(auto increment)**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
* 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的
的确有办法绕开这些问题 ——例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法
### 传输预写式日志(WAL)
* 对于日志结构存储引擎,日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
* 对于覆写单个磁盘块的B树,每次修改都会先写入**预写式日志(Write Ahead Log, WAL)**,以便崩溃后索引可以恢复到一个一致的状态
### 逻辑日志复制(基于行)
复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来
* 对于插入的行,日志包含所有列的新值。
* 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值。
* 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少所有已更改的列的新值)
### 基于触发器的复制
触发器允许您注册在数据库系统中发生数据更改(写入事务)时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中,使用外部程序读取这个表,再加上任何业务逻辑处理,会后将数据变更复制到另一个系统去
## 读己之写
如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了
在这种情况下,我们需要**读写一致性(read-after-write consistency)**,也称为**读己之写一致性(read-your-writes consistency)**【24】
## 单调读
从异步从库读取第二个异常例子是,用户可能会遇到**时光倒流(moving backward in time)**,首先查询了一个延迟很小的从库,然后是一个延迟较大的从库
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取(不同的用户可以从不同的副本读取)
## 多主复制
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为**多领导者配置**(也称多主、多活复制)
## 需要离线操作的客户端
多主复制的另一种适用场景是:应用程序在断网之后仍然需要继续工作
有一些工具旨在使这种多领导者配置更容易。例如,CouchDB就是为这种操作模式而设计的。
## 协同编辑
实时协作编辑应用程序允许多个人同时编辑文档
如果要保证不会发生编辑冲突,则应用程序必须先取得文档的锁定,然后用户才能对其进行编辑。如果另一个用户想要编辑同一个文档,他们首先必须等到第一个用户提交修改并释放锁定。这种协作模式相当于在领导者上进行交易的单领导者复制
';