野猪书读书笔记第五章(续1)
文章目录

副本
副本的作用:
- 地理位置上距离使用者更近
- 当系统的某些节点挂掉后可以继续正常运行
- 减小读操作的负载
备份这件事情最麻烦的地方在于:处理备份数据的变化
有三种算法来处理节点间的数据变化:
- 单 leader
- 多 leader
- 无 leader
处理副本时还需要做一些权衡:同步备份还是异步备份,如何处理失败的备份。
Leaders and Followers
每个存储了数据库一份拷贝的节点都叫做一个副本(replica)。
每一个对数据库的写都需要被每个副本正确处理,否则的话,副本里的数据就会不一样。为了解决这个问题,最常见的解决办法叫做 leader-based 备份(又叫主动/被动,主从备份)。如下图所示:

其作用模式是:
- 某一个节点被设计为 leader(master),当有写操作时,只能发送请求到这个节点,主节点先将这个写操作的数据存在自己的存储空间里;
- 其他几点称作 followers(slave),当主节点写数据到自己的存储空间时,它同时通过备份日志将数据改变发送给所有的从节点。每个从节点通过顺序地应用所有的 日志改动 来改变数据。
- 读操作可以从任意节点读,写操作只能从主节点写
同步 vs 异步
直接上图:

follower 1 是同步的:主节点等到从节点返回更改成功的信号才进一步操作;
follower 2 是 异步的:主节点不等待从节点返回。
同步的好处是:数据可以被确保被存在了从节点上;坏处是:等!如果从节点挂了,主节点会一直等!
所以,不可能所有的从节点都是同步的。
半同步:如果有一个同步从节点挂了或者很慢,异步从节点中的一个就变成了同步的。
通常情况下,主从备份机制都被配置为完全异步的,缺点是,如果在备份的过程中主节点挂了,还没有被备份到从节点的数据就永远丢失了。好处是,主节点可以一直处理写操作。
当从节点很多或者从节点分布在不同的地理位置时,完全异步方式是被广泛采用的。
配置新从节点
步骤:
- 在某个时间点得到一个主节点的快照;
- 把快照拷贝到新节点上;
- 新节点连接到主节点并请求自从快照发生后的新数据;要求数据库有一个标志可以记录这个节点:log sequence number 之类的;
- 追加成功之后,开始正常处理数据
处理节点挂掉
节点除了意外 down 掉外,也有可能主动 down 掉,比如升级系统,升级数据库等等。
从节点down掉,追加恢复
每个从节点在本地磁盘上保留一份从主节点接收的数据更改的日志。当从节点从崩溃中恢复后,只需要读这个日志然后向主节点请求crash之后的数据即可。等追加成功后,正常备份就好。
主节点挂掉: 故障转移(failover)
当主节点挂掉后,某个从节点被顶上来当主节点。
步骤:
- 定义主节点挂掉:timeout。
- 选新的主节点
选举机制:大多数从节点选一个新的从节点成为主节点。
controller 节点: 事前选一个controller node。 - 重新配置系统
客户端发送写请求到新的主节点。其他从节点要知道新的主节点。老的主节点恢复后要知道自己已经是从节点了。
故障转移可能会因为这些事情出错:
- 如果使用的是异步备份,有一种情况:新的主节点没有收到老的主节点的全部数据,而这时老的主节点又变成了新的从节点,那这时候老的主节点上的没有备份的数据怎么办呢?现实中大部分数据库的做法是:丢弃这些数据。
- 丢弃数据有时候很可怕:当数据和其他系统有交互时。
真实事件发生在 GitHub。一台数据不完整的 MySQL 从节点被选举为了主节点,而同时公司使用了自增 Primary Key,而这些 Primary Key 同时存在了 Redis 中。这就导致了新的主节点上位后用了已经存在Redis中的 PK 作为新的 PK,从而导致了混乱。 - 有一种神奇的场景:两个节点都觉得自己是主节点。。。数据会错乱。
- Timeout 时间长短的设定要考虑清楚
这些都是 分布式系统 的基本问题
备份日志的实现
基于声明的副本
最简单的情况是:主节点把每个写操作都发送给从节点。对于关系型数据库而言,就是把 INSERT, UPDATE, DELETE 语句都发送给从节点,每个从节点都要解析并执行这些 SQL 语句就像这些语句是直接从客户端来的一样。
听起来很有道理,但这种方法有不少问题:
- 只要语句中有 非确定性的函数,比如 NOW(), RAND()等,就会造成每个 副本 出来的数据不一致;
- 顺序必须一致!当有大量并发实务操作时会有问题;
- 有副作用的语句(比如 触发器,存储过程,UDF 等)通常也是非确定性的。
解决办法就是:主节点在发送之前把非决定性的语句转换为决定性的。
Write-ahead log(WAL)
几乎所有的存储引擎都使用了这种先写数据后记录日志的方式。
我们可以使用同样的日志文件在每个节点构建出一个副本。
这种方式有一个大问题:WAL 包含了太多实现细节,导致版本更新或者更换存储引擎变得几乎不可能。
逻辑(基于行的)日志副本
另一种存储方式是:日志格式和存储引擎解耦。这种日志称为逻辑日志(以区别存储引擎中真是的物理日志)。
逻辑日志通常是一系列记录,用于描述以行为粒度对行的写入:
- 对于一行的写入操作,日志会包含这一行所有列的值;
- 对于一行的删除操作,日志会包含足够的信息来说明这一行被删除了,通常来说是主键;如果这个表没有主键,日志需要记录这行所有的数据;
- 对于一行的更新操作,日志会包含足够的信息来说明这一行被更新了,以及被更新后的所有列的值。
如果有一个事务操作更改了很多行,就会记录行日志,然后会有一行记录表明这个事务被提交(committed)了。
这种方式使数据和日志解耦了,所以兼容性更好得多。
这种兼容性不仅体现在同一个数据库的不同版本,也体现在同一份数据可以被不同数据库使用。
触发器副本
如果你想在应用层面手动控制副本(只复制某些数据),需要用到触发器和存储过程。
副本延迟的问题
单主多从结构只适用异步复制。但异步复制的问题是:在某些时候,某些从节点的数据和主节点不一致(你懂的)。等一段时间后,这些从节点上的数据最后会追加回来和主节点保持一致。这个问题被称为数据的最终一致性。
从节点最终追加回来的时间被称为复制延迟。复制延迟可能会带来下面这些问题。
只读自己写的数据
当被写的数据需要立马被读的时候,需要写后读一致性,也称为读你自己写的数据一致性。有几种方式可以实现这种一致性:
- 这种场景的数据从主节点读,其他数据从从节点读。前提是需要对业务了如指掌。
- 当大部分数据都需要满足写后读一致性时,不能全部从主节点读,需要更复杂的机制保证从从节点读数据而不遗漏;
- 由客户端记住数据最近一次被写的时间戳,根据这个时间戳来决定从库里的数据是否可用。问题是时间戳的同步问题。
- 如果是多数据中心架构,如果一个查询需要主节点参与,那么这个查询必须要路由到包含主节点的数据中心,这会带来额外的复杂度。
单调读
哈哈哈,有时会出现时光倒流的效果:第一次读到一个延时比较小的副本,第二次读到一个延时比较长的副本:时光倒流了!
单调读(monotonic reads)就是为了保证这种情况不会发生而存在的。
一种实现单调读的办法是:保证同一个用户只读同一个副本。
一致性前缀读
如果一连串的写操作以某个固定的顺序发生,那么读这些顺序化的数据必须以同样的顺序出现。
在 分 Shard 的数据库架构中,要保证这个非常困难。
一种解决办法是在分区的时候,手动控制和这个需求相关的数据都放在同一个分区。
解决副本延迟的方法
- 看需求。看是否有这方面的需求。
- 事务。分布式系统中的事务是个复杂的话题;以后会讲。
(TO BE CONTINUED…)