数据系统与分布式(二) 分布式数据系统(复制与分片)
分布式数据系统
基础知识
-
为什么要引入分布式
-
拓展性
- 负载过大,超出了单台机器的处理上限
-
容错与高可用性
- 单台机器出现故障, 其他机器可以正常工作
- 组件失效, 冗余组件可以继续接管
-
延迟考虑
- 服务遍布全球各地, 希望就近服务
-
-
系统拓展
-
每个机器称为结点
-
垂直拓展
- 加强单台机器
-
水平拓展
- 加机器数量, 加小机器
-
-
复制与分区
-
当把数据分布在多节点的时候有两种常见的方式
-
复制
- 在多个节点上保存冗余的副本
-
分区
- 将一个大的数据库拆分成多个较小的子集
-
数据复制
-
目的
- 1.数据在地理位置上更接近用户,降低延迟
- 2.高可用, 系统出现故障, 仍然可以继续工作
- 3.拓展多台机器, 提高读吞吐量
-
主节点与从节点
-
基本原理
- 1.选一个作为主节点, client的所有写请求都发给主节点
- 2.其他节点都是从节点, 主节点把更改写入本地后, 把更改通过复制的日志或者更改流发送给所有从副本, 从副本应用这些更改
- 3.client可以向主节点发read or write, 对从节点只能read
-
同步复制和异步复制
-
同步复制的优点
- 一旦向用户确认, 从节点可以保证数据的最新版本
- 主节点发生故障时, 从节点有最新的备份, 可以迅速恢复
-
同步复制的缺点
- 一旦有某个响应特别慢, 就要一直阻塞到同步副本确认完成
-
异步复制的优点
- 性能特别好, 在处理写请求时, 不管从节点数据多么滞后, 主节点总是可以继续响应写请求, 系统的吞吐性能更好
-
异步复制的缺点
- 主节点发生失败,还未同步的写请求会丢失
- 无法保证数据的持久化
-
trade-off也就是半同步
- 一个节点是同步模式, 其他节点是异步模式.
- 万一同步的从节点性能下降或者不可用了, 把另外一个节点提升成同步模式
- 这样做的好处是保证至少有两个节点拥有最新的数据副本
-
-
配置新的从节点
-
如何在不停机 , 数据服务不中断的前提下完成从节点的设置
- 1.在某个时间点对主节点的数据副本产生一个一致性快照
- 2.将此快照拷贝到新的从节点
- 3.从节点连接到主节点, 并请求 快照之后的数据更改日志
- 4.获得日志后, 应用这些变更. 也就是 "追赶"
-
-
处理节点失效
-
从节点失效
- 追赶式恢复
-
主节点失效
-
切换的具体表现
- 选择某个从节点提升为主节点, 客户端也要把write发送给新的主节点, 从节点也要接受来自新的主节点的变更数据
-
手动切换
- 管理员手动切换
-
自动切换
-
1.确认主节点失效
- 基于超时的机制, 如果30s都没有回复, 则认为失效
-
2.选举新的主节点
- 选举的方式, 最好数据差异最小
-
3.重新配置系统让 新主节点生效
-
很多小问题
- 脑裂
- 原主节点仍然认为自己是主节点
-
-
-
-
-
复制日志的实现
-
基于语句的复制
-
把诸如insert和update这类语句发送给从节点
-
问题就是很多非确定性的语句
-
比如now()获取时间
-
多个并发执行的事务
- 等等
-
MySQL会自动切换到基于行d
-
-
-
基于预写日志(WAL)传输
- PostgreSQL和Oracle支持
- 缺点是此方案过于底层, 与存储引擎紧密耦合, 和软件版本有关
-
基于行的逻辑日志复制
- 复制和存储引擎采用不一样的日志格式, 解耦合
- MYSQL的binlog
-
基于触发器的复制
- 高度可定制, 复杂度高
-
-
-
复制滞后问题
-
主从复制所有写都要经过主节点, 可以通过水平拓展来增强 系统的读能力
-
"最终一致性"问题
- 一个应用从一个异步的从节点读取数据, 该副本落后于主节点
- 如果同时对主节点和从节点发送相同的查询, 可能会得到不一样的效果
- 但是这种不一致只是暂时的, 一段时间后, 从节点会赶上主节点
- 这种效应称为最终一致性
-
"写后读一致性"问题
-
读自己的写
-
用户在写入不久后查看数据
- 但是还没同步,看起来就像数据丢失了一样
- 用户会不高兴不高兴
-
我们需要"写后读一致性"
-
方案一
- 如果用户读可能被修改的内容, 从主节点读
- 否则 ,在从节点读
- 比如社交网站, 读自己的朋友圈, 从主节点读, 读别人的朋友圈, 在从节点读
-
方案二
-
跟踪最近更新的时间,
- 如果在1min之内, 从master读
-
同时监控从节点的复制滞后程度
- 避免从滞后超过1min的节点读取
-
-
方案三
- 通过client端发送读请求时的时间戳, 确保提供读服务时 时间上的合理
-
Tips
- 如果副本分布在多数据中心, 必须先把请求路由到主节点在的数据中心
-
-
-
-
"单调读"
-
由于两次路由的从节点不同的原因, 用户第一次看到了数据, 第二次却看不到了
-
我们需要"单调读"一致性
-
方案一
- 根据用户ID哈希到固定的一台机器
-
-
是比"强一致性"弱, , 比"最终一致性"强的
-
保证绝对不会看到回滚
-
-
"前缀一致读"
-
对于一系列按照某个顺序发生的写请求 ,读这些内容时也会按照当时写入的顺序
-
方案
- Happened-before
-
-
复制滞后的解决方案
-
如果采用最终一致性系统, 用户的体验能接受的话,
- 那就采用"最终一致性"吧
-
如果用户体验不好
-
提供更强的一致性
- 比如写后读
-
-
-
-
多主节点复制
-
单主节点的明显缺点
- 如果主节点网络中断了,就会影响所有的 写入操作
-
适用场景
-
在单数据中心引入 多节点 只是徒增复杂度
-
适用于多数据中心, 离线客户端, 协作编辑 这三个场景
-
多数据中心场景
-
在每个数据中心内, 采用常规的主从复制方案
-
在数据中心之间, 有每个的主节点来负责 数据交换和更新
-
与单主节点的对比
-
性能方面
- 肯定是多主节点性能更好,
- 因为就近访问
-
容忍数据中心失效
- 也是多主节点更好
- 每个数据中心可以独立于其他数据中心继续运行
-
容忍网络问题
- 也是多主更好
-
-
缺点
- 冲突问题
-
-
协作编辑
- 石墨文档, 腾讯文档
- 如果解决冲突
-
离线客户端操作
- 比如手机上的日历工作, 每部手机都是一个主节点
- 这种时候采用 多主节点场景比较合适
-
-
处理写冲突
-
场景
- A修改标题为20201005是美好的一天, B修改标题为 20201006是美好的一天
-
同步与异步冲突检测
-
避免冲突
- 最理想的策略就是避免冲突
- 不同用户对应到不同的主数据中心
- 但是还是有可能冲突,
-
收敛于一致性状态
-
通过分配唯一的ID
- 比如基于时间戳
- 最后写入者获胜
-
拼接这些值
- 20201005是美好的一天20201006是美好的一天
-
靠应用层的逻辑, 事后解决, 比如告诉用户
-
-
自定义冲突解决逻辑
- 最合适的方式还是得应用层来
- 在写入时执行
- 在读取时执行
-
什么是冲突
-
刚才的那种对同一个字段写
-
订票时把唯一的票给了两个人
-
自动冲突解决的可能方案
-
1.无冲突的数据类型(CRDT)
- conflict-free Replicated DataType
- 可以由多个用户编辑的数据结构
-
2.可合并的持久数据结构
- 类似git
- 二向合并 三向合并
-
3.操作转换
-
-
没现成的答案
-
-
-
拓扑结构
-
指的是多个主节点之间传播路径
-
环形拓扑
-
星形拓扑
- 拓展到树形结构
-
all to all拓扑
- 容错性更好
- 问题: 可能由于网络 原因, 出现类似"前缀一致读"的问题
- 使用版本向量的技术来
-
-
-
无主节点复制
-
思路清奇, 放弃主节点
- 亚马孙Dynamo系统 不开放, DynamoDB不是Dynamo
-
节点失效时写入数据库
-
读修复与反熵
-
读修复
- 并行读多个副本时, 可以检测到过期的返回值
-
反熵
- 有后台进程不断查找副本之间的数据差异, 有明显的同步滞后
-
-
读写quorum
-
quorum是法定人数的意思
-
3个副本的例子里, 成功的写操作至少2个, 那么失败的至多一个, 我们读的时候至少读两个, 就可以保证至少有一个是最新的副本
-
W+R>n
-
常见的设计
- n为奇数
- w = r = (n+1)/2
-
问题
- 即使满足W+R>n , 我们还是不能保证读取最新值
-
所以我们最好把w和r看成灵活可调的读取新值的概率
-
-
-
Quorum一致性的局限性
-
可以把w和r<=n
- 这样虽然可能读的是旧值, 但是提高了性能
-
监控旧值
-
我们监控复制的运行状态
-
如果差距过大, 可能就是网络问题或者节点超负荷
-
原理大概是
- 由于主节点和从节点上写入遵循相同的顺序, 每个节点维护了复制日志执行的当前偏移量. 通过比较偏移量的差值, 来衡量从节点落后于主节点的程度
-
-
宽松的quorum与回传
-
配置适当的quorum是有好处的
- 1.可以容忍一些节点慢, 因为请求不用等所有节点的响应, 只需要w或者r个
-
sloppy quorum
-
写入和读取仍然需要w和r个成功的响应, 但包含了不在先前指定的n个节点
-
原先指定的n个节点暂时不可用
-
把写请求写入了一些暂时可访问的节点中, 一旦网络问题解决, 这些临时节点会把这些写入全部发送到原始主节点上,
- 这就是数据回传,(或者说暗示移交)
-
-
-
多数据中心操作
- 无主节点复制 aim at 更好容忍并发写入冲突, 网络中断 和 延迟尖峰
-
-
检测并发写
-
Dynamo风格的数据库在并发写入时缺乏顺序保证
-
最后写入者获胜( 丢弃并发写入)
-
每个写请求附加一个时间戳
-
Last Write Wins(LWW)
-
可以实现最终收敛的目标, 但是牺牲了数据的持久性
-
如果覆盖, 丢失数据可以接受,就用
-
确保LWW安全无副作用的唯一办法
- 只写入一次然后写入值视为不可变
- 比如UUID作为主键
-
-
Happens-before关系和并发
- 并发性, 时间, 相对性
- 确定前后关系
- 合并同时写入的值
- 版本矢量
-
-
数据分区
-
面对海量数据集或者非常高的查询压力, 复制技术还不够, 需要引入分区
-
分区的定义
- 每条数据, 或者说每条记录,只属于某个特定分区
- 每个分区可以看成一个完整的小型数据库
-
-
数据分区与数据复制
-
分区通常和复制结合使用, 每个分区在多个节点上保存有副本.
- 意思是某条记录属于特定的分区, 而这个分区里有许多节点保存着这记录的拷贝
- 每个分区有自己的主副本
-
-
键-值数据的分区
-
基于关键字区间分区
- 分区边界要适配数据本身的分布特征
-
基于关键字哈希值分区
- MD5, Fowler-Noll-Vo
- 缺点, 丧失了区间查询特性
-
组合索引
-
user_id, update_timestamp
- 可以有效检索某用户在一段时间内的所有更新
-
-
负载倾斜与热点
-
所有的读/写都是针对同一个关键字
- 比如明星的微博
- 方法,如果某key是热点, 在这个key的前或后加一个随机数,比如两位的十进制,, 这样可以负载到100台机上
-
-
-
分区与二级索引
-
二级索引带来的主要挑战是它们不能规整的映射到分区中
-
基于文档分区的二级索引
- 每个分区完全独立, 如果我们想查询"红色的汽车"
- 需要把查询发送到所有的分区, 然后合并所有返回的结果
- 别名:"分散/聚集"
-
基于词条的二级索引分区
-
我们要对所有的数据构建全局索引, 但是不能把所有的全局索引存在一个节点上, 所以全局索引页必须分区
-
全局索引的缺点:
- 写入速度较慢而且比较复杂
- 所以往往是异步的 刚写入之后读可能 不太行
-
优点:
- 读取更高效
-
-
-
分区再平衡
-
随着时间的变化, 数据库可能会变化, 我们需要再平衡
-
再平衡需要满足
- 1.平衡之后, 负载, 数据存储, 读写请求等应该均匀分布
- 2.再平衡过程中, 数据库应该可以正常提供读写服务
- 3.避免不必要的负载迁移
-
动态再平衡的策略
-
为什么不用取模
-
hash (A) = 123456
-
10台机器时
- 123456%10 = 6
-
11台机器时
- 123456%10 = 3
-
12台机器时
- 123456%10 = 0
-
-
因为如果节点数N发送了变化, 会导致许多关键字需要迁移
-
-
固定数量的分区
-
比较好
- 创建远超实际节点数的分区数, 然后为每个节点分配多个分区
-
比如10个机器, 一开始就创建了1000个分区
- 每个机子负责100个分区
-
只需要做好映射关系
-
-
动态分区
- 分区数量大到一定程度, 比如10GB, 就分裂
- 分区小到一定程度, 就合并
- 类似B树
- 一个节点上可能有多个分区
- 适用于关键字区间分区, 也适用于基于哈希的分区
-
按节点比例分区
-
-
-
请求路由
-
我们已经把数据集分布到多个节点上, 但是客户端有一个 悬而未决的问题 应该连接到哪个IP地址 和哪个端口号
- 也就是服务发现的问题
-
主要有三种策略
- 1.客户端随机选, 然后节点看看是不是自己, 不是就转发
-
- 引入一个路由层
- 3.客户端就知道
-
ZooKeeper是一个独立的协调服务, 它跟踪集群范围内的元数据
- 每个节点向ZooKeeper注册自己, 路由层或者客户端订阅ZooKeeper
-
当然还可以通过gossip协议来同步集群状态变化, 虽然增加了节点的复杂性, 但是避免了对ZooKeeper的依赖
-