Starkwang.log
首发于Starkwang.log
Designing Data-Intensive Applications 读书笔记 - 第五章 Replication

Designing Data-Intensive Applications 读书笔记 - 第五章 Replication

这是《Designing Data-Intensive Applications》第五章的读书笔记,之所以从第五章开始,是因为前四章都是偏向于数据库、网络基础知识,从这一章开始(全书的Part II)才真正讲如何管理分布式数据。


一、Leader 和 Follower

  • 用户端写入的时候,必须先经过 Leader 处理
  • 其它节点是 Follower,Leader 写入完毕后会通知他们复制数据,保证一致性
  • 客户端读的时候,可以随便读,但写的时候只能向 Leader 写

1.1、同步复制与异步复制

上图 Follower1 是同步复制,Follower2 是异步复制


同步复制:

写入请求时,Leader 会一直等到所有 Follower 都确认已经写入后(期间不处理任何写请求),才向客户端返回成功

优点:保证强一致性

缺点:如果任何 Follower 挂掉,都会写失败,这在大型系统中是不现实的

所以在实际的数据库中,使用的都是半同步(semi-synchronous),即一个 Follower 是同步的,其它都是异步;如果同步的那个 Follower 挂了,那么设置一个新的 Follower 为同步模式


异步复制:

写入请求时,Leader 自己写入成功后就返回,不等待 Follower

优点:可以立刻响应写入请求,即使所有 Follower 都挂掉了

缺点:可能会导致不一致(告诉客户端写入成功了,但实际没有成功)

现在大部分使用的都是异步复制

1.2、增加新的 Follower

即如何在集群不断写入数据的同时,加入新的 Follower,让它的数据跟上大部队

  1. 给 Leader 某个时刻的数据做一个快照
  2. 把快照复制到新的 Follower 上
  3. 新的 Follower 连接上 Leader,告诉它从哪个时刻开始同步数据
  4. 直到新 Follower 的数据跟上了 Leader 的步伐(caught up),开始进入工作

1.3、处理节点宕机

Follower 宕机

处理非常简单,从宕机前的日志开始和 Leader 同步即可,和增加新的 Follower 步骤差不多

Leader 宕机

  1. 检测 Leader 宕机
  2. 选出新的 Leader
  3. 把系统配置改为新的 Leader


1.4、日志复制的实现

1.4.1 基于语句的复制,Statement-based replication

基于语句的复制,比如在 SQL 中复制 INSERT、UPDATE、DELETE 语句到 Follower。

存在一些问题:

  • NOW()、RANDOM()这样的函数,没法基于语句复制,因为每次运行的结果都不一样
  • 如果语句依赖自增数,或者跟数据库中现有的数据强相关,那么必须保证语句执行顺序跟 Leader 完全一致,在并发处理多个事务时这一点很难保证
  • 语句有副作用时,可能会导致不一致的出现

上面这些问题有办法解决,MySQL 5.1 版本之前使用的就是这种复制模式。


1.4.2 预写式日志,Write-ahead log (WAL) shipping

本书的第三章讨论了日志结构的储存引擎的实现(SSTable、LSM-Tree 和 B-Tree),如果是这种储存引擎,我们可以把它的每一次写日志都复制到 Follower 上,这样可以保证一致性。

PostgreSQL 和 Oracle 就是这样实现的,缺陷在于,这种复制方式非常底层,每一条 WAL 包含的信息实际上是“向哪一个硬盘 block 写哪些 bytes”,这就导致 WAL 和储存引擎强相关,也就是必须保证 Leader 和 Follower 的储存引擎底层完全一致,导致很难集群进行版本升级。


1.4.3 逻辑日志复制,Logical (row-based) log replication

把日志抽象为与底层引擎无关,采用 change data capture,每次有数据更改的时候都记下改了什么,例如记录每次写入的值和行号,MySQL 的 binlog 就是这样实现的。


二、复制滞后产生的问题

对于单 Leader,多 Follower的架构来说,一般是只能向 Leader 写,但可以向任何 Follower 读,这样可以大大增加读的性能。

但由于写操作需要向 Follower 复制,这里就会产生滞后问题,写完后立刻读,有可能会向 Follower 读到旧的值(因为此时 Leader 可能还没有同步变化到 Follower 上)。

当然这种不一致的状态是转临时逝的,不会永久存在,也就是所谓的 “最终一致性”。

下面是滞后的解决方法


2.1、谁写的就由谁来读(Reading Your Own Writes)

具体可以有以下策略:

  • 如果读的字段可能已经发生了变化,那么向 Leader 读取(因为 Leader 的数据一定是最新的);
  • 如果读的字段距离上一次变更时间很短,那么向 Leader 读;
  • 客户端在读请求的时候带上自己最近一次写操作的时间戳,处理这个读请求的服务器看到这个时间戳,就可以知道自己本地的数据是否过时了


2.2、单调读(Monotonic Reads)

客户端进行多次读操作时,这些读操作可能会分配到不同的 Follower 上,所以可能会发生第一次读到了数据,然后第二次读的时候数据又消失了的问题,如下图 User 2345,第一次在 Follower1 上读到了评论,第二次在 Follower2 上没有读到评论:

所以,客户端读到了新的数据,那么就不能让它读到旧数据。最简单的解决方法就是,把每个客户端的读请求都分配到固定的 Follower 上。

2.3、一致性前缀(Consistent Prefix Reads)

由于服务器之间复制数据可能产生的滞后,数据的时序可能会产生问题。

比如下图,Mr. Poons 先说了一句话,然后 Mrs. Cake 回复了他,然而对于第三方观察者而言,他们的对话时序可能是混乱的:

所以只有保证写入是按照时序的,才能使读到的数据保持正确的时序。

所以写入的时候,需要在集群中维护一个全局的时序。


三、多 Leader 复制

单个 Leader 的缺点在于,如果任何因素导致无法连接 Leader,那么你就无法向数据库写入任何数据了,这会让整个系统非常脆弱,所以我们在一些情境下需要多 Leader 的架构。

3.1、多 Leader 复制的示例

下面是一些多 Leader 架构的示例

3.1.1、多个数据中心

像上图这种情况,你可以有多个 Leader 分布在不同地方的数据中心,每个数据中心都是一个独立的集群,它们的 Leader 之间会相互同步数据。

对比一下单 Leader 和多 Leader 的优劣:

性能

单个 Leader 导致只能向一个节点写入,而多 Leader 能显著提升读写性能

数据中心挂掉时

当有多个数据中心时,其中任何一个数据中心挂掉都不会影响系统对外的服务能力

网络出问题时

单个 Leader 非常依赖集群内部网络的稳定性,而多 Leader 可以容忍暂时的网络问题,因为临时的网络问题不会影响整个系统的写入。


3.1.2、可以离线的客户端

我们可以把一个支持离线运行的客户端,和服务器端,视为两个“数据中心”,比如一些日历应用,会在本地维护一份数据,直到有网络时,才会和服务器进行数据同步,这就是一个异步的多 Leader 架构。

CouchDB 就是为此设计的。


3.1.3、多人协作编辑

像 Etherpad、Google Docs 这样的应用,允许多人同时编辑同一份文档,每个人都是一个 “Leader”,相互之间同步数据,但这显然会遇到冲突的问题。


3.2、解决写冲突

多 Leader 之间同步数据,最大的问题就是如何解决写冲突。比如下图中,两个用户都修改了文档的标题,发请求给服务器,都返回了成功,但直到 Leader 之间进行同步时才发现之前的数据有冲突。

下面是一些解决方法。


3.2.3、同步冲突检测

单 Leader 不会发生冲突,因为每次写入都是一个原子化的事务。

多 Leader 如果采用同步的方式检测冲突,也不会发生冲突。即每次写入时,都向其它的 Leader 检查有没有冲突,如果都没有冲突,那么写入成功。但这样性能极差,也丢掉了多 Leader 架构的好处,还不如用单个 Leader。


3.2.4、避免冲突

多 Leader 架构避免冲突最简单的方式就是,让可能产生冲突的请求,都走向同一个 Leader。比如对于同一项资料的修改,都路由到固定的某个 Leader 上。

这样做的缺陷在于,集群是不断变化的,很难做到长期固定,Leader 的变化就会让这个策略失效。


3.2.5、覆盖

产生冲突的写入也可以有先后顺序,我们用更新的那个写入覆盖之前的。


3.3、多 Leader 的拓扑结构

多 Leader 可以有很多种拓扑结构,环形、星形、全连接形。

全连接形是最符合直觉的,每个 Leader 都和其它所有 Leader 相互交换数据。MySQL 使用的是环形连接。


四、无 Leader 复制

无 Leader 复制完全不需要 Leader 的存在,这种架构中,客户端可以向多个节点发起写入请求。

4.1、当有节点挂掉时,如何写入数据库

只要保证多个节点写入成功,那么客户端就可以认为写入成功。


4.1.1、读取时进行修复

在读取的时候,可能会存在不一致(因为有部分节点写入失败),这时可以发现不一致并且修复它。或者所有节点都定期检查是否自己的数据跟别人有不一致的地方。


4.1.2、读写的 Quorums 机制

简单地说就是只要从多个节点那里读成功,或者写成功,那么就可以认为成功了。

具体地说,假设我们有 n 个节点,只要其中 r 个节点读成功了,那么就认为读取操作成功了;其中 w 个节点写成功了,那么就认为写入成功了。

一般来说 n 都取基数,而 r、w 都取 (n + 1) / 2,即过半节点响应成功即可。至少要保证 w + r > n,这样才能保证至少有 1 个节点能返回最新的数据。

4.2、Quorums 机制的局限性

  • 如果存在并行写入,那么集群无法知道写入的先后顺序,因为不存在 Leader。
  • 如果写的同时还在读,那么无法保证读到的是新值还是旧值。
  • 如果 w > r,那么可能发生写入成功的节点数不足 w,写入失败,而读的时候成功数大于 r,这样就读到了一个“写入失败”的值。

4.3、处理并行的写入

对于 Leaderless 架构来说,并行的写入会导致集群数据不一致的问题:

上图中由于并行的写入操作到达各个节点的时间顺序不一致,导致 Node 2 与别的节点数据不一致。

解决方法如下:

  • 数据库为每个写入都标记一个递增的版本号
  • 客户端写入前,先读取到这个版本号,然后带上这个版本号再发送写入请求
  • 数据库识别这个版本号,根据版本号决定是否写入、怎么写入

上图中,每个客户端的写入请求都会带上本地的版本号,以便告诉数据库“要在哪个版本的基础上进行变更”。本质上就是一个状态机转移:

收集完各个客户端的请求之后,合并写入的状态即可。

发布于 2018-04-30

文章被以下专栏收录