复制的目的:
- 高可用
- 允许断网时继续工作
- 低延迟
- 高吞吐量
由于数据会随着时间改变,流行的数据变更复制算法是个难题,流行的变更复制算法有下面三种:
- 单领导
- 多领导
- 无领导
单领导
基于领导者的复制(主从复制):主库接受所有写操作,从库只接受读.主库发生变更时,将变更日志发送给所有的从库,从库根据日志更新数据.
这种方式应用在PostgreSQL
/MySQL
等关系型数据库上,也用与MongoDB
之类的非关系型数据库.
同步复制与异步复制
复制的过程采用同步还是异步也需要考量.
同步复制时主库会等待从库的响应,可以保证从主库有一致的最新的数据.
异步复制可以降低时延,但是不能保证数据持久化.
部分同步,其他follower
异步可以被称为半同步.通常情况下,基于领导者的复制都配置成为完全异步,并得到广泛使用.
设置新从库
有时为了增加副本数量或者替换失败节点,需要设置新从库.
可以在某时刻获得主库的快照,避免锁定整个数据库,从库根据快照进行更新,并且拉取快照之后发生的数据变更进行处理.
节点宕机
从库失效: 追赶主库的数据变更进行恢复.
主库失效: 需要提升从库作为主库,将客户端的请求发送至新的主库,其他从库也需要开始同步新主库(failover
).通常有三个步骤:
- 确认主库失效
- 选举新主库
- 重新配置整个系统
failover
的问题: - 异步复制时新主库数据落后于老主库,老主库重新上线后,数据冲突.解决方案:简单丢弃.但是如果与其他存储协调,比如
redis
,有可能导致数据不匹配. - 脑裂:同时出现两个节点都以为自己是主库,都接收写操作,而没有冲突解决,那么数据可能丢失或者损坏.
- 确认主库失效的超时时间的设置.
复制日志的影响
基于语句(statement
)的复制: 将所有写语句(insert
, update
和delete
)日志转发给每一个从库.
缺点很明显:
- 调用诸如
NOW
等非确定性函数,可能在不同的从库产生不同的效果. - 所有语句要按顺序执行
- 有副作用的语句可能在每个从库上带来的副作用不一样
传输预写日志(WAL
): 写操作通常追加到日志中,比如日志结构存储引擎(SSTable
和LSM
)和使用B树
.此时,日志是包含所有写入的追加字节序列.PG
使用这种方式.
缺点是: 记录的数据非常底层,包含磁盘中的字节更改.通常很难在主库和从库运行不同版本的数据库,影响到数据库的滚动升级.
基于行(row
)的复制: 逻辑日志复制,复制和存储引擎分离,使用不同的日志格式.例如,插入新行时,日志包含所有列的值;删除行时,标识该行已删除;更新行时,标识该行,以及新值.MySQL
使用这种方式.这种方式易于解析,可以用来进行日志分析.
基于触发器的复制: 允许用户自定义应用程序代码,在写入事务时,自动执行.它很灵活,但是也有更高的开销,更容易出错.
复制延迟
通常复制方案中,采用异步同步的方式.因此可能出现从库落后的情形.但是最终从库会赶上主库并且保持一致(最终一致性).
多主复制
多主情况,数据库副本可以分散在不同的数据中心,并且每个数据中心都有主库.数据中心内使用主从复制,数据中心之间,每个数据中心的主库都会将更改复制到其他数据中心的主库.
与单节点主从复制方案相比,多主情况下每个写操作可以在本地数据中心处理,性能可能会更好.同时,每个数据中心是独立的,如果主库所在的数据中心发生故障,可以切换到其他数据中心.网络抖动也不会影响最终的写入.在离线的情况下,应用程序可以自身充当主库,这也是多主适用的场景.
但是,需要面临不同数据中心修改相同数据的问题,即写冲突.
处理写冲突
以协同编辑为例,当两个用户同时进行更改时,每个用户的更改已经成功应用到本地主库,只在主库同步的时候才检测到冲突,此时冲突检测是异步的.方案可考虑的有:
- 避免冲突,保证特定的操作都通过同一个主库,就不会发生冲突.
- 实现冲突的合并解决,方案有只采纳特定的记录(比如最新的,但是会带来数据丢失),或者保留所有记录等待解决.可以在写入时,复制变更日志时检测冲突,调用应用层冲突处理逻辑;也可以在读取时,提示用户进行解决.
拓扑结构
常用的有:环形,星形和全部至全部形.
环形和星形如果有节点出现故障,会影响其他节点之间复制日志的转发.
全部至全部形可能出现复制日志被覆盖,可以利用版本向量来解决.
无主节点复制
这种复制模式放弃主节点,任何节点都可以接受来自客户端的请求(读和写).
节点失效
客户在写入的时候只要收到部分节点的响应,就可以认为写入成功,可以忽略某个节点失效的情形.
但是当节点失效又重新上线时,如果用户请求该节点的数据,则有可能是过期的数据.因此,客户发送读请求时也是向多个副本并行发送请求,再利用相应的技术确定应该使用哪个值,并且对节点进行更新.
节点失效重新上线后的数据追赶恢复,可以使用下面两种机制:
- 读修复: 客户端读取时,通过多副本获得的值,判断该值过期,将新值写入副本.
- 反熵过程: 后台进程不断查找副本之间的差异,将缺少的数据从一个副本复制到另一个副本.但是不能保证日志按照顺序复制写入,也有明显的同步滞后.
读写quorum
前面提到,客户写入时收到部分节点的响应,可以认为写入成功,那么部分节点是多少节点呢?
通常如果有n
个副本,写入需要w
个节点确认,读必须要查询r
个节点,只要w+r>n
,就能保证读取的节点中一定包含最新值.w
和r
分别就是写和读的quorum
.
与此对应的还有宽松的quorum
,可以容忍偶尔读取旧值,对需要高可用和低延迟的场景来说,具有很高的吸引力.
在网络中断的情况下,客户仍然能连接到某些节点,但是节点又不是能够满足仲裁的节点,此时允许采用宽松的仲裁方式:
写入和读取仍然需要w
和r
个成功的响应,但是包含并不在先前指定的n
个节点,比如当前连接的是临时节点.当网络问题得到解决后,临时节点需要把接受到的写入全部发送到原始主节点上(数据回传).即只要集群中有任何w
个节点可用,数据库就可以接受写入.也说明了即使满足w+r>n
,也不能保证读取的值是最新的值.
监控旧值
基于领导者的复制中,数据库会公开复制滞后的度量标准,可以获得复制滞后量.
然而在无主复制中,没有固定写入顺序,使得监控复制变得更加困难
检测并发写入
和多领导者复制类似,无主复制进行读修复和数据回传时也可能产生冲突.实现最终冲突解决,需要副本趋于相同的值:
1.最后写入胜利(LWW
,丢弃并发写入)的方式.但是此种方式可能会丢失部分数据,但在缓存中,是可以接受的.如果需要保证不丢失数据,只能确保每一个key
只被写入一次,然后视为不可变.
- 利用版本向量捕捉操作之间的依赖关系,对每个
key
以及每个副本使用版本号.
复制滞后的处理方式
读自己的写
出现复制延迟时,用户可能查看不到自己刚更新的内容.此时如需要保证读写一致性,可以有以下方案:
- 从主库读取用户自己可能修改的内容,从从库读取其他用户的内容,适用于用户可能修改的内容较少,否则大部分读都落在主库上;
- 可以跟踪上次更新的时间,如果时间较短,从主库读;或者监控从库的延迟,防止滞后比较久的查询落在从库;
- 可以使用客户端记录时间戳,请求从库时,如果时间戳已经被从库记录,则可以返回查询.但是如果用户使用多设备终端,那么其他设备不知道时间戳,此种方案不合适,需要中心存储元数据.
如果数据库分布在多个数据中心,用户的请求都需要路由到包含主库的数据中心.多设备情况下,很难保证不同设备会路由到同一数据中心.
单调读
如果每次查询的从库时延不一样,有可能出现每次查看的数据都不一样的情况.
可以采用每次用户都读取同一个从库来保证单调读.
一致前缀读
如果数据写入不同的分区,那么全局的写入顺序无法得到保证.
可以通过确保因果相关的写入都写入相同分区,以及采用跟踪因果依赖关系的算法解决.
总结:明明是异步复制却假设复制是同步的,这是很多麻烦的根源。