Table of Contents generated with DocToc
此篇为《数据密集型应用系统设计》(DDIA)读书笔记,笔记可能存在遗漏,建议直接阅读原书。
第五章 数据复制
- 主从复制
- 复制滞后
- 复制滞后带来的问题
- 多主节点复制
- 适用场景
- 处理写冲突
- 拓扑结构
- 无主节点复制
- 读修复和反熵
- 读写quorum
- 宽松的quorum和数据回传
- 多数据中心操作
- 检测并发写
- 合并同时写入的值
- 版本矢量
第五章 数据复制
主从复制
复制滞后
如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。由于并非所有的写入都反应到从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。但如果停止写数据库,经过一段时间后,从节点最终会追上主节点并与主节点保持一致,这种效应也被称为最终一致性。
复制滞后带来的问题
三种复制滞后带来的问题场景:
1.读自己的写
许多应用会让用户提交一些数据,然后查看自己所提交的内容,对于异步复制会出现这样的问题:提交数据须发送到主节点,但是当用户读取数据时,数据可能来自从节点,然而这时新数据可能尚未到达从节点,导致用户以为提交的数据丢失。
针对这种情况,我们需要“读写一致性”,该机制保证如果用户重新加载页面,他们总能看到自己最近提交的更新,但对其他用户则没有任何保证(其他用户的更新可能无法及时看到)。
基于主从复制的系统实现读写一致性的可行方案:
- 如果用户访问可能会被修改的内容,从主节点读取;否则,在从节点读取;所以在查询执行之前需要判断内容是否可能被修改;
- 如果应用的大部分内容都必须经由主节点,这就丧失了读操作的扩展性。这就需要其他的方案,例如:跟踪最近的更新时间,如果更新后一分钟之内,则从主节点读取;同时监控从节点的复制滞后程度,避免从那些滞后时间过长的从节点读取;
- 读请求+客户端记住最近更新时的时间戳,系统确保对该用户提供读服务时都应该至少包含了该时间戳的更新,否则可以交给另一个副本来处理或等待至副本接收到最近的更新;
- 如果副本分布在多数据中心,必须先把请求路由到主节点所在的数据中心(?);
如果同一用户从多个设备访问数据时,还有一些需要考虑的问题:
- 记住用户上次更新时间戳的方法实现起来会比较困难;
- 如果副本分布在多数据中心,无法保证来自不同设备的连接经过路由之后都到达同一数据中心;
2.单调读
如图,用户1234发出一条写请求,主节点执行完毕之后,将请求转发给其他从节点,其中从节点1复制滞后较短,从节点2复制滞后较长。此时,用户2345发起了两次对reply_to=5555的读请求,第一次查询成功得到了用户数据,而第二次请求,由于滞后过长,导致返回为空。
单调读一致性保证:当读取数据时,如果某个用户依次进行多次读取,则他不会看到回滚现象,即在读取较新值之后又发生读旧值的情况。
实现单调读的一种方式是:确保每个用户总是从固定的同一副本执行读取(例如:基于用户ID的哈希算法来选择从节点);
3.前缀一致读
复制滞后带来的因果反常:
Mr. Pooms: How far into the future can you see, Mrs. Cake?
Mrs. Cake: About ten seconds usually, Mr. Pooms.
但是由于滞后程度不同其他人听到的可能是:
Mrs. Cake: About ten seconds usually, Mr. Pooms.
Mr. Pooms: How far into the future can you see, Mrs. Cake?
为了防止这种异常,引入了另一种保证:前缀一致读(对于一系列按照某个顺序发生的写请求,那么读取这些内容是也会按照当时写入的顺序)。
一种解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但该方案的真实效率会大打折扣。
多主节点复制
由主从复制模型中“一主多从”变为“多主多从”,复制流程类似:处理写的每个主节点都必须将该数据更改转发到所有其他节点 。此时,每个主节点还是其他主节点的从节点。
适用场景
1.多数据中心
数据中心内部采用主从复制方案,而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换和更新。
每个数据中心之间通过广域网连接。
缺点:不同的数据中心可能会同时修改相同的数据,因而必须解决潜在的写冲突。
2.离线客户端操作——网络断开后还要继续工作
3.协作编辑(通常不会将协作编辑等价于数据库复制问题,但二者有很多相似之处)
处理写冲突
1.避免冲突
确保特定用户的更新请求总是路由到特定的数据中心,并在该数据中心的主节点上进行读/写。从用户角度来看,这基本就等价于主从复制模型来。。。
2.收敛于一致状态
至少要保证数据在所有副本中最终状态是一致的。
- 为每个写入分配一个特定的ID,并制定规则,挑选胜利者;
- 将所有写入合并为一条,在应用层进行处理;
3.解决冲突最合适的方式可能还是依靠应用层
例如:保存导致冲突的所有可能,在读取数据时,由应用层处理。
拓扑结构
环形和星形拓扑存在的问题:如果某一节点发生故障,在修复之前,会影响其他节点之间复制日志的转发;
全链接拓扑存在的问题:存在某些网络链路比其他链路更快的情况,从而导致复制日志之间的覆盖;
冲突检测技术在许多多主节点复制系统中的实现还不够完善
无主节点复制
同时向所有副本发出写请求/读请求。
读修复和反熵
当一个失效节点重新上线后,如何赶上中间错过的那些写请求?两种机制:
- 读修复
当客户端并行读取多个副本时,可以检测到过期值(根据版本号),然后将新值写入到该副本;这种方法主要适合那些被频繁读取到场景。
- 反熵过程
一些数据存储有后台进程不断查找副本之间数据的差异,将任何缺少的数据从一个副本复制到另一个副本。与基于主节点复制分复制日志不同的是,反熵过程并不保证以特定的顺序复制写入,并且会引入明显的同步滞后。
读写quorum
n
个副本,写入需要w
个节点确认,读取必须至少查询r
个节点,则只要w + r > n
,读取节点就一定包含最新值,然后根据版本号就可以确定过期值。不要把w
和r
当作绝对的保证,而是一种灵活可调的读取新值的概率,因为现实情况往往更加复杂
宽松的quorum和数据回传
配置适当的quorum的数据库系统可以容忍某些节点故障,也不需要进行故障切换,同时还可以容忍某些节点变慢(不需要等待所有副本响应)。
在一个大规模集群中(节点数远大于n),客户可能在网络中断期间还能连接到某些数据库节点,但这些节点又不能够满足数据仲裁的那些节点,此时你可能面临一个选择:
- 如果无法达到
w
或r
,将错误明确返回给客户端; - 只是将它们暂时写入到一些可以访问到的节点(这些节点并不在n个节点集合中);
第二种方案称为“宽松的仲裁”(sloppy quorum
):写入和读取仍然需要w
和r
个成功的响应,但包含了那些并不在先前指定的n个节点。一旦网络问题解决,临时节点需要把接收到的写入全部发送到原始节点上,这就是所谓的“数据回传”。
可以看出,sloppy quorum
对于提高写入可用性特别有用,只要有任何w
个节点可用,数据库就可以接收新的写入。同时,这也意味着,即使满足w + r > n
,也不能保证着读取r
个副本时一定能读取到新值,因为新值可能被临时写入n
之外的某些节点且尚未回传。所以,sloppy quorum
更像是为了数据持久性而设计的一个保证措施。
多数据中心操作
在一些数据库系统中(Cassandra和Voldemort):副本的数量n
是所有数据中心的节点总数,每个客户端的写入都会发送到所有副本,但客户端通常只会等待来自本地数据中心内的quorum节点数的确认。
而有些数据库系统(如:Riak)则将客户端与数据库节点之间的通信限制在一个数据中心内,因此n
描述的是一个数据中心内的副本数量。集群之间跨数据中心的复制则在后台异步运行,类似于多主节点复制风格。
检测并发写
处理写冲突
- 最后写入者获胜:
强制对写请求排序,最后只有一个写入值存活下来。
如何判断两个操作是否并发?
- Happens-before关系和并发
如果B知道A,或者依赖于A,或者以某种方式在A基础上构建,则称操作A中操作B之前发生。所以,如果两个操作都不在另一个之前发生,那么操作是并发的。(呃。。。)
如果一个操作发生在另一个操作之前,则后面的操作可以覆盖较早的操作;如果属于并发,就需要解决潜在的冲突问题。
如何让确定操作并发性,即两个操作究竟属于并发还是一个发生在另一个之前(依赖)?
- 服务器为每个主键维护一个版本号,每当主键新值写入时递增版本号,并将新版本号于写入的值一起保存;
- 当客户端读取主键时,服务器将返回所有当前值以及最新的版本号。且要求写之前,必须先发生读请求;
- 客户端写请求必须包含:之前读带的版本号、读到的值和新值三者合并后的集合。写请求的响应和读操作一样;
- 当服务器收到带有特定版本号的写入时,覆盖该版本号或更低版本的所有值;如果一个写请求没有包含版本号,它将与所有其他写入同时进行,不会覆盖任何已有值,其传入的值将包含在后续读请求的返回值列表中;
合并同时写入的值
不舍弃并发写入的值和旧值,而是将旧值和新值进行合并。但是对于删除操作,项目在删除时不能简单地从数据库中删除,系统必须保留一个对应的版本号以恰当的标记该项目需要在合并时删除(结合购物车案例)。
版本矢量
对每个副本的每个主键均定义一个版本号,每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本看到的版本号,通过这些信息来指示要覆盖哪些值、该保留哪些值。所有副本的版本号集合称为版本矢量。