《数据密集型应用系统设计》读书笔记--第5章 数据复制
一、主从复制
1、主从复制的工作原理:
-
指定某一个副本作为主副本。当客户写数据库时,先将数据写入主副本本地。
-
主副本把数据更改作为日志发送给所以从副本。每个副本将更改日志应用到本地。严格保持与主副本相同的写入顺序。
-
客户端从数据库读取数据时,可以在主副本和从副本上读取,但是写只能在主副本上写。
2、同步复制和异步复制
同步复制:客户将更新发送给主节点,主节点将数据更新发送给从节点,从节点更新完成后通知主节点,最后由主节点通知客户更新完成。
半同步:某一个从节点是同步的,其他节点都是异步。当同步的从节点发生故障,则将另一个从节点提升为同步模式。这样保证至少有两个节点拥有最新数据。
全异步:系统吞吐性能好,但是无法保证数据的持久化。
3、配置新的从节点
增加副本,如果只是简单的复制,会造成不同节点上有不同时间点的数据。因为客户端还在不停向主副本写数据,数据不停变化。
在不停机,数据服务不中断的情况下增加从节点:
-
在某个时间点对主节点的数据副本产生一个一致性快照。
-
把此快照拷贝到从节点。
-
从节点连接到主节点后请求一致性快照之后的数据更改日志。
-
从节点应用这些数据变更,称为追赶。
4、节点失效的处理
-
从节点失效:从节点的硬盘上保存了收到的数据变更日志。从节点可以知道在故障前的最后一笔事务,并连接到主节点请求这笔事务之后的数据变更,进行追赶。
-
主节点失效:节点切换。选择某个从节点提升为主节点。客户端也需要更新,将请求发送给新的主节点。
自动切换步骤:
-
确认主节点失效:采用超时机制,节点之间频繁发送心跳信息。
-
选举新的主节点:共识问题。
-
配置系统使主节点生效:系统需要确保原主节点降级为从节点并认可新的主节点。
-
5、复制日志的实现
-
基于语句的复制:主节点记录每个写请求并且将操作语句作为日志发送给从节点。
不适应场景:非确定性语句,依赖于现有数据,有副作用的语句。
-
基于预写日志传输:所有对数据库写入的字节都被记入日志,可以使用完全相同的日志在另一个节点上构建副本。缺点是日志非常底层,导致和存储引擎紧密耦合。主从节点无法运行不同版本的软件,升级需要停机。
-
基于行的逻辑日志复制:复制日志和存储引擎的日志采用不同的格式。逻辑日志和存储引擎逻辑解耦。
-
基于触发器的复制:当数据库发生数据更改时,执行应用层代码。
6、复制滞后的问题
-
读自己写 用户提交数据随即查看自己提交的内容。但是读取时数据可能来自从节点。新的数据可能没有到达从节点,影响用户体验。写后读一致性保证用户重新加载页面,总能看到自己最近提交的更新。
方案:
1.如果用户访问可能会被修改的内容,只从主节点读取。需要有方法知道可能会被修改的信息。
2.跟踪最近更新的时间。如果在更新后的一分钟之内,则总是在主节点读取。并且监控从节点的滞后程度。
3.客户端可以在请求中包含一个时间戳,系统保证提供读服务时都包含了该时间戳,如果没有,则交给另一个副本处理。问题:跨设备一致性。
-
单调读 一个用户A向主节点写入数据,另一个用户B此时进行两次读操作。第一次读的从节点数据已经写入,读到了A用户写入的数据。第二次访问的从节点还没有收到主节点的更新数据,B用户刚刚看到的内容又消失了,仿佛回滚了。
方案:同一个用户总是从固定的副本读取。比如基于ID哈希的方法而不是随机选择副本。
-
前缀一致读 对于一系列按照某一个顺序发生的写请求,读取时也按当时写入的顺序。分布式数据库中,不同的分区独立运行,不存在全局的写入顺序。
方案:任何具有因果关系的写入都交给一个分区来完成。
二、多主节点复制
1、适用场景
-
多数据中心:为了容灾和更接近用户的考虑,数据库的副本横跨多个数据中心。在每个数据中心内,采用主从复制方案。在数据中心之间,各主节点负责同其他数据中心的主节点进行数据的交换更新。
-
离线客户端操作:在离线状态下,用户对本地数据进行任意更改,在设备再次上线时,与服务器和其他设备同步。本地数据库相当于一个主节点,联网时同步相当与数据中心之间的多主复制。
-
协作编辑:加快协作效率会减小可编辑的粒度。这会导致多主复制的情况产生。
2、处理写冲突
避免冲突:对特定数据的写请求总是通过同一个主节点。如特定用户的个人信息都通过特定的数据中心修改。对于每个用户来说相当于主从复制。
冲突解决原则:收敛于一致状态。应该确保数据在所有副本中最终状态是一致的。
-
收敛的解决方案可能的方式:
-
基于时间戳,或者用户id的优先级确定最后胜利者。
-
副本设定优先级。
-
将写入的值合并在一起。
-
保留冲突的信息,用应用层来解决。可以让用户自定义解决方案。
三、无主节点复制
放弃主节点,允许任何副本直接接受来自客户端的写请求。
节点失效时写入数据库不需要执行主节点复制模型下的切换操作。失效的节点重新上线以后可能会读到过期的数据。为解决这个问题,客户端读取数据时,不是向一个副本读取,读请求并行地发送到多个副本。可以采用版本号确定哪个值更新。
1、修复失效节点:
-
读取到包含新值和旧值的数据时,把更新的值写入落后的节点。(读修复)缺点:很少读的数据库丢失的数据可能无法检测到。
-
后台进程不断查找副本之间的差异(反熵过程)
2、确定读成功:
如果有n个副本,写入需要w个节点确认,读取必须至少查询r个节点。
需要w+r>n。这样读取的节点中一定包含最新值。(Quorum)
w和r可以配置,对于读多写少的的负载,可以设置w=n,r=1。
3、宽松的Quorum:
在需要高可用的场景中,当客户端和节点的网络发生故障导致响应的节点达不到仲裁需要的数量。宽松的quorum中客户写入的值可以被写入不在n个节点之中的节点上,只要响应的节点数大于w即可认为写入成功。当网络问题解决时再发送到原始节点上。但客户端不一定能从r个节点读取到新值。
4、跨数据中心的操作:
1、可以向所有副本发送,但只等待来自本地数据中心的确认。、
2、只和本地数据中心交互,数据中心之间多主复制。
5、并发写
并发写需要解决多个客户端同时写所有副本时,多个副本之间最终收敛于相同内容的问题。
1、最后写入者获胜。每个写请求附加一个时间戳,每个节点丢弃早的写入数据。这样牺牲了数据持久性,虽然向客户端发送了写成功消息,但是数据被丢弃了。
2、因果关系和并发关系:如果B操作依赖与A操作的发生,就是Happens-Before关系。如果互相不需要知道对方是否发生,就是并发关系。
解决并发关系的算法(p178例子推演):
-
服务器为每个主键维护一个版本号。当主键写入新值时递增版本号并且保存新值和版本号。
-
客户端读主键。服务器返回所有主键的当前值和服务器的版本号。
-
客户端写主键时包含之前读到的版本号,和新值合并以后用此版本号返回服务器。
-
服务器收到写入时,覆盖该版本号和更低版本的所有值,保存更高版本的所有值。
合并同时写入的值
版本矢量 :多副本中,可以保存副本的版本号和主键的版本号。可以在一个副本上读取在另一个副本上写入。数据库决定应该覆盖还是保留并发值。