- 访问的数据地理位置更接近用户 减少访问延时 (多数据中心)
- 节点宕机数据仍可用 (高可用)
- 多副本可读,增加读吞吐量
主从复制:
同步复制 -> 强一致性, 弱可用性 一旦某个从节点宕机则写失败
解决方案:一个从节点同步复制,其他异步复制,一旦同步节点宕机提升一个异步节点为同步节点
新强一致性复制算法: chain replication
异步复制 -> 强可用性,弱一致性
增加从节点:
挑战:主节点数据仍在写入,直接复制会导致数据不一致
方案:
复制数据时锁主节点不允许写 ,太粗暴
更好的方案分3步:
- 对主节点数据做一个一致性快照,并记录对应的复制日志位置
- 复制快照到从节点
- 将快照后新增的复制日志应用到从节点上,直到追上主节点
从节点宕机恢复:
从宕机前复制日志位置开始追赶
主节点宕机 -> FAILOVER 机制
比较复杂,一般需要3步:
- 宕机检测 : 一般通过心跳
- 重新选主,一致性算法和多数派协议
- 请求路由到新主节点,并通知客户端
问题: - 数据丢失,新选的主节点没有全部原主节点的数据
- 脑裂:两个节点同时认为自己是主节点
- 合理的心跳超时设置 (网络抖动可能误报)
手动FAILOVER优于自动?
复制日志实现:
- 基于SQL语句: (MYSQL早期版本)
问题: 特殊函数now(), random() 等带来的不确定性. 自增主键冲突. trigger, 存储过程等的影响 - 基于WAL日志: (POSTGRESQL, ORACLE)
问题:和存储引擎强耦合, 滚动升级可能有问题 - 逻辑复制: 自定义结构,基于修改的数据值.
优点:不依赖于存储引擎, 兼容性好 - 触发器(TRIGGER): (DATABUS FOR ORACLE, BUCARDO)
优点:灵活性
问题:OVERHEAD
复制延时的问题:
最终一致性: 以较弱的一致性换取读的可扩展性
一致性多弱取决于复制延时和读取策略
-
read-your-own-write-consistency (read-after-write consistency): 确保自己写入的数据一定会被随后的读请求读到
实现方式:
如果用户只修改自身的数据并知道自身数据的ID, 自身数据从主节点读,其他数据从随机节点读
如果用户可以修改许多其他的数据,需要客户端记录 key -> last modify time. 在一定时间范围内只从主节点读, 否则从随机节点读. 或者客户端把 key+last modify time 发给查询节点, 查询节点比较时间戳,等待赶上再返回结果或者将读请求
多数据中心: 路由读请求到同一数据中心 -
monotonic read: 确保第二次读结果不会比第一次旧
实现方式:
对同一主键每次读从同一副本(可能出现数据倾斜) - consistent prefix read: 确保读写的因果顺序不会错乱 (比如问答顺序)
实现方式:
全局的一致性写顺序
追踪因果关系causual dependency的算法
通过分布式事务解决复制延时问题.
多主复制:
支持多点写入
每个LEADER对于另一个LEADER是FOLLOWER
主要应用场景是多数据中心
每个数据中心有一个LEADER, 数据从LEADER复制到本数据中心的FOLLOWER和另一个数据中心的LEADER.
性能:写请求可由LOCAL 数据中心的LEADER处理. 不用跨广域网
数据中心容错: 一个数据中心宕机后另一个数据中心仍可独立运行,数据中心恢复后再将数据复制过去
网络容错:跨广域网异步复制,暂时网络不稳定不会影响写入
缺点:必须解决写冲突
设计时必须考虑自增主键冲突. TRIGGER, 数据完整性约束等
写冲突检测和解决
- 同步检测:等数据复制到每个节点再检测冲突并通知用户. 延迟大,完全牺牲了多主复制的优势
- 冲突避免: 保证相同的键写入到同一个数据中心, 数据HASH,路由等. 数据中心如果宕机或者用户位置改变则会出现问题
- 每个写操作一个UUID,对同一数据的最后一次写获胜 (基于时间戳等), 可能丢失数据
- 每个REPLICA有一个UUID, 对同一数据写来自高位REPLICA获胜, 可能丢失数据
- 保存写冲突数据,之后自动解决或者提示用户解决
解决冲突的时机:
写时:一旦冲突发生,只能后台解决无法让用户干预 (Bucardo)
读时: 保存冲突数据,在下次读的时候自动解决或提示用户(CouchDB)
事务中的每个写冲突会单独解决,无法保持事务边界
自动冲突解决:
Conflict free replicated data types
Mergeable persistent data structures (GIT)
Operational transformation
多主复制拓扑:(超过两个MASTER)
星状
环状
所有对所有
需要防止循环复制(写请求记录经过的节点编号)
星状或者环状复制某个节点宕机会导致不可写
所有对所有 容易因延迟导致数据一致性问题或写失败
无主复制:
没有副本概念,客户端直接写多个节点
多数节点响应写请求则成功
读请求也发送到多个节点,版本号保证读取最新的数据
保证: 写成功节点数(W) + 读节点数(R) > 集群节点数(N)
读写并行发往所有节点, W, R 为响应的节点数量
如果 W + R < N 牺牲一致性换取低延时和高可用性
一般选择 W > N/2, R > N/2,容许最多N/2 节点宕机
W + R > N 依然有可能读到旧数据:
- Sloppy Quorum
- 并发写冲突
- 读写冲突, 读的节点可能没写入最新数据
- 写某些节点失败,写成功节点未回滚
- 写成功节点宕机,数据从旧节点恢复
监控数据陈旧程度很困难, 无法像主从复制一样监控REPLICATION LAG. 写顺序不确定
Sloppy quorum: 部分节点宕机导致无法写入通常写入的节点(读写迁移)
Hinted handoff: 节点恢复后读写迁移的节点将临时写入数据写回通常写入节点
Sloppy quorum 提高了可用性但是降低了读到最新数据的可能
无主复制也适合多数据中心的场景: 写请求发送多数据中心但只等待本数据中心确认
保证宕机节点最终一致性:
Read repair: 客户端发现读版本冲突自动更新版本比较旧的节点
Anti-entropy process: 后台进程自动比较数据版本并修复 (不一致时间长)
写冲突可能发生在以下情况:
- 节点因网络故障丢失写请求
- 不同节点收到写顺序不一致
写冲突处理:
- 最后写获胜:为写请求分配版本号,冲突只保留版本最大的数据. 可能丢失数据,适用于每个KEY写一次不再更新的场景 (时序数据?)
- 并发写定义: 如果两次写操作有强时序关系或互相依赖 (如A为插入key1,B为key + 1),则A,B称为causually dependent. 任意写操作A, B, 必有A在B之前,A在B之后,或者AB为并发
- 并发写检测算法:
每个KEY维护一个版本,每次写版本+1
客户端写KEY之前必须先从SERVER读一次KEY的最新版本
写操作带上写之前读到的KEY版本,可以覆盖同一个KEY更低或者相同版本,但是更高版本必须保留 (并发写)
购物车举例:
A, B 两个客户端同时向购物车添加产品,每次写提交,服务器端生成一个购物车新版本,并将最新版本的购物车内容及相应版本号返回给提交写请求的客户端
客户端提交写请求时要将提交的内容和之前的购物车版本合并,并附带之前的版本
服务端针对同一个客户端,把新的版本覆盖之前的版本,但是不同客户端提交的最新版本认为是concurrent write,不会互相覆盖,需要冲突解决 - 处理并发写:
由客户端合并多个concurrent write的版本.
删除操作不能直接物理删除,而要标记(tombstone)
自动冲突解决, 见上文 - 向量钟 (version vector):
适用于leaderless replication, 没有主节点而是多个对等的replica
版本号针对于每个replica的每个KEY
写之前先从replica读取最新的version vector, 合并写数据再发回
保证从replica A读,再写到replica B是安全的