引言
在第 9 章《数据库与事务型数据》中,我们讨论了分布式数据库的重要性,以及它们如何在出现部分系统故障时仍能保障业务连续性。我们也从分布式系统的视角出发,说明了传统数据库概念(如一致性与持久性)在含义上的差异。数据复制是分布式系统实现容错的首要机制。本章将探讨数据复制的概念理解、各类部分失效(partial failure),以及针对这些失效所采用的不同复制方式。
同时,利用多台计算与存储资源不仅能获得容错能力,还能实现系统的负载均衡。负载均衡要么用于并行处理多条数据服务请求,要么将单个请求拆分为多个子请求并行处理。无论哪种方式,都可借助多份硬件资源加速响应。将单个请求拆分并并行处理,通常依赖于将数据划分为多个子集,这被称为数据分区。本章将学习多种数据分区技术,以及最能从这些技术中受益的具体场景。
结构
本章涵盖以下主题:
- 故障与容错
- 数据复制基础
- 复制的类型
- 配置多个副本
- 从副本读取数据
- 跨数据中心复制
- 数据分区
- 其他常见分区方案
- 散射-汇聚(scatter & gather)操作
目标
通过本章的学习,读者将掌握数据复制的基本概念;能够将各种可能影响应用可用性的故障场景与合适的复制机制对应起来,并应用于应对这些故障。
同样地,读者将掌握数据分区的基本概念,理解不同分区方案及其最佳适用点,并明白各分区方法之间的取舍。
故障与容错
在当今时代,每个人几乎都时刻在线。一旦你的网络服务无法响应用户请求,分分钟就可能被推上社交平台的热搜。此类不可用会带来业务损失与负面口碑。由于业务依赖于形形色色的软硬件组件,任何一个部件在任何时刻都可能发生故障。
大型用户应用运行在典型的数据中心架构之上。数据中心包含以下用于存储数据并确保数据可用性的硬件类型:
- 具备独立磁盘的数据存储节点
- 用于容纳存储节点的机架
- 连接到存储节点的网络交换机
- 市电供电
- UPS 等后备电源
典型数据中心架构如图 12.1 所示:
图 12.1:数据中心架构
由于上述硬件的存在,任何组件都有发生故障的概率;同时运行在存储节点、网络交换机等之上的软件也可能出现异常。以下列出数据中心中各类非穷尽的故障类型:
- 数据存储软件(数据库或数据仓库)报错或故障
- 存储节点的操作系统崩溃
- 存储节点的硬件(CPU、内存等)失效
- 存储磁盘损坏或数据损坏
- 整个机架断电
- 连接该机架的交换机故障
- 整个数据中心停电
- 洪水、地震等灾难导致整个数据中心损毁
尽管这些故障从轻微到灾难不等,其共同影响都是:由于数据不可用而造成业务损失。
大型业务必须预见此类事件、为最坏情况做准备,同时确保业务连续性。对旅行聚合平台、电商网站等业务而言,数据可用是实现业务连续性的关键。各类数据复制技术正是为此而生。
数据复制基础
数据复制是创建并管理数据副本的过程,以便在原始数据不可用时仍可对外提供服务。
继续以旅行聚合平台为例。每当用户执行预订、取消、写评价等操作时,都会有一些事务性数据写入后端数据库。后端数据库负责确保数据写入的持久性。然而,正如第 9 章所述,不同数据库对“持久性”的定义并不相同:
- 对传统关系型数据库而言,“持久”意味着事务已写入本地磁盘;
- 对现代分布式数据库而言,“持久”意味着事务已经写入主库节点的本地磁盘以及一个或多个副本节点。
在现代数据库系统中,不仅 MongoDB、Couchbase 等分布式数据库开箱即用地支持复制,MySQL、PostgreSQL 等传统关系型数据库也支持复制——但诸如故障检测、自动主从切换、使用便捷度等细节依各产品实现而异。
表 12.1 总结了传统 RDBMS 与现代分布式数据库在“持久性”上的差异:
| 传统 RDBMS | 分布式数据库 |
|---|---|
| 写入仅在单个数据库节点本地磁盘落盘后才算持久 | 写入需在预设最少数量的多个节点本地磁盘落盘后才算持久 |
| 持久性上限受该节点磁盘带宽限制 | 同时受磁盘带宽与网络带宽限制 |
| 单节点失效会导致写入丢失 | 单节点失效不会导致写入丢失 |
| 写入通常比分布式数据库更快 | 写入通常慢于单机 RDBMS |
表 12.1:传统 RDBMS vs 分布式数据库的持久性
数据工程师需要做出的重要决策之一是:为应用数据配置多少副本数。若仅保留一个副本,那么两个不同存储节点同时故障就可能导致数据丢失。尽管概率较小,许多用户仍倾向于配置 2 个以上副本。
同时,用户常会将副本分布在以下不同的容错域中:
- 不同的机架
- 不同的网络交换机
- 不同的供电回路
- 不同的可用区(AZ)
这样可以在前述多种故障下仍确保高可用与避免数据丢失。
图 12.2 展示了基本的复制工作方式:
图 12.2:数据复制的基础原理
由于高可用需求各不相同,数据复制的技术也有多种风格。接下来的内容将介绍这些复制类型、它们所解决的用例,以及选型时需要权衡的要点。
数据复制的类型
对于旅行聚合平台而言,预订记录、预订取消、支付明细等都是业务关键数据,绝不能出现无法访问的情况。如果数据工程师没有正确配置数据复制,哪怕一两台存储节点故障也可能导致业务受损。为确保数据被及时复制,通常会采用同步复制技术。同步数据复制的含义是:只有当数据同时写入主库与副本库后,用户操作才被视为成功;若主库或任一副本写入失败,则用户事务失败。通常,业务关键数据都会采用同步复制。
图 12.3 展示了同步复制的工作方式:
图 12.3:同步数据复制
如图 12.3 所示,同步复制按如下事件顺序执行:
- 用户程序向数据库发起写请求,该请求进入主数据存储。
- 数据在主库完成持久化。
- 数据副本被发送到各个副本存储。
- 数据在副本存储上完成持久化。
- 副本向主库发送确认,表明数据已持久化。
- 用户应用收到写入成功的响应。
在这一过程中,若步骤 1~6 的任意环节发生故障,用户的写请求都会被视为失败。无论主库还是副本出现问题,都会导致操作失败。此外,由于需要多次持久化,写入总时延较高。采用同步复制时,数据丢失极为罕见,因为这通常要求主库与副本同时发生故障。
为权衡高延迟问题,可以选择半同步复制。像用户评论、评分这类数据,相比预订记录并非同等关键。对于这类数据,半同步复制是不错的选择,因为即便发生丢失,也不会直接造成业务损失。
图 12.4 展示了半同步复制的工作方式:
图 12.4:半同步数据复制
如图 12.4 所示,半同步复制按如下事件顺序执行:
-
用户程序向数据库发起写请求,该请求进入主数据存储。
-
数据在主库完成持久化。
-
数据副本被发送到各个副本存储。
-
以下两步并行触发:
- 用户应用获得写入成功的响应;
- 数据在副本存储上完成持久化。
在半同步复制中,用户程序不会等待数据在副本上完成持久化,只要复制已被触发即可返回成功。这暴露出一个较小的故障窗口,可能导致数据丢失。换言之,半同步复制下存在两种可能丢失数据的情形:
- 主库与副本同时发生故障;
- 主库在本地持久化并向用户返回成功之后、在副本完成持久化之前发生故障。
此时,数据丢失的概率与复制时延成正比。现代数据库在高性能硬件支持下,通常可将复制时延控制在 1 秒以内。
对于多数直接影响业务或关键用户体验的数据,1 秒级的时延已经足够。但也有一些场景不需要如此低的复制时延。例如用户上传头像。数据工程师可以把用户头像二进制数据存入数据库;若主库故障,恢复头像绝不是首要任务,即使丢失也不太会对业务或用户体验产生直接影响。对这类非关键数据,可以选择异步复制。图 12.5 展示了异步复制的工作方式:
图 12.5:异步数据复制
如图 12.5 所示,异步复制按如下事件顺序执行:
-
用户程序向数据库发起写请求,该请求进入主数据存储。
-
数据在主库完成持久化。
-
以下两步并行触发:
- 数据副本被发送到各个副本存储;
- 用户应用获得写入成功的响应。
-
数据在副本存储上完成持久化。
注意:在异步复制中,用户应用可能在复制尚未触发之前就收到写入成功响应。正因如此,异步复制的潜在数据丢失窗口大于半同步复制。
下面总结不同复制类型的典型用例:
- **同步复制:**用户数据、基础影片数据等
- **半同步复制:**影片评论、评分等
- **异步复制:**头像图片、海报图片等
配置多副本
上述示例均以单个副本为前提。但为进一步提升可用性,数据工程师可能会配置更多副本。副本数越多,复制开销越大,尤其在同步或半同步复制下,会显著拖慢写入。图 12.6 展示了配置 5 个副本时的复制开销:
图 12.6:五副本同步数据复制
如图 12.6 所示,五副本同步复制按如下事件顺序执行:
- 用户程序发起写请求至主库。
- 主库完成持久化。
- 数据副本并发发送至 5 个副本库。
- 各副本落盘完成(每个副本的网络与磁盘时延不同,耗时不一)。
- 各副本完成后向主库逐一发送确认。
- 在步骤 4、5 于各副本进行时,主库等待全部 5 个副本的确认。
- 收到所有确认后,用户应用得到成功响应。
可见,多副本带来两大问题:
- **复制与确认链路更长。**副本数越多,复制、落盘与确认的总体开销越大;任意一个副本变慢,整体写入都被牵制。
- **失败概率上升。**网络抖动、磁盘错误等“活动部件”增多,至少一个副本写入失败的概率提升,从而增加整笔写请求失败的概率。
为缓解上述问题,许多数据库允许配置多数派(quorum)确认:当收到多数副本确认后即判定写入成功。在 5 个副本的例子中,达到 3 个或以上确认即视为成功。
从副本读取数据
前文讨论了各种故障与复制方式。接下来说明系统在故障发生时的响应,以及副本如何保障可用性。图 12.7 展示了一个简单的单节点故障,以及副本如何继续对外提供服务:
图 12.7:单节点故障下由副本继续提供服务
过程如下:
- 初始化阶段,数据工程师配置了两台存储节点:一台主库,一台副本。
- 所有用户程序对主库发起读写。
- 主库写入的数据同步复制到副本。
- 只有当副本也成功持久化后,写操作才返回成功。
- 所有读操作由主库提供。
- 发生故障事件(主库失效)。
- 将副本提升为主库。
- 所有用户程序切换到新主库,读写请求均由新主库承载。
在实际系统中,第 7、8 步的执行方式依赖各数据库实现。许多现代数据库提供如下自动化能力,以替代人工干预:
- **自动故障转移(auto-failover):**自动检测主库故障并自动提升副本为主库。
- **自动副本读取(read-from-replica):**当主库读取失败或超时时,自动尝试从副本读取数据以继续服务。
跨数据中心复制(XDCR)
一般可将故障事件分成两大类:
- 数据中心内部的软硬件故障,例如磁盘损坏、某台存储节点宕机、某个交换机或机架失效等;
- 灾难性事件,例如电网故障、地震、洪水等,可能导致整个数据中心不可用。
对于第 1 类故障,通常在同一数据中心内、但位于不同节点/机架上部署副本。数据中心内复制速度快且稳定,因此可以采用同步复制。
而针对第 2 类灾难性故障,整个数据中心可能瘫痪。此类事件往往影响更大的地理范围,甚至波及同一城市或区域的多个机房。为应对这类场景,数据工程师会配置跨数据中心复制(XDCR) 。图 12.8 展示了 XDCR 的示意:
图 12.8:跨数据中心复制
从图中可见:
- 有两个用于承载和服务业务数据的数据中心:主数据中心在纽约,副本数据中心在旧金山。
- 用户程序与纽约主数据中心交互。
- 主数据中心的数据被复制到旧金山的数据中心。
- 复制通常通过公有互联网进行,因而速度较慢;最坏情况下复制延迟可达数分钟,因此 XDCR 通常是异步的。
- 一旦纽约数据中心因灾难不可访问,旧金山数据中心被提升为活跃站点。
- 用户程序改为与旧金山机房通信,完成读写操作。
如上架构下,复制延迟可能达到数到几十分钟;若此时发生灾难,则可能造成相当时间窗口内的数据丢失。比如延迟 20 分钟,最坏会丢失 20 分钟的业务写入。为尽量避免灾难场景下的数据丢失,可配置双向 XDCR。
注:跨地域高复制延迟会影响对一致性/实时性极其敏感的 OLTP 写入(如银行业务),从而带来用户体验上的延迟。
双向 XDCR 与冲突解决
双向 XDCR 允许在无故障前提下,应用同时对两个数据中心进行读写。图 12.9 展示了其工作方式:
图 12.9:双向 XDCR
要点如下:
- 为应对灾难,用户在纽约与旧金山各部署一个数据中心。
- 两个数据中心都作为活跃数据存储,可同时对外提供读写。
- 数据双向复制:纽约→旧金山,同时旧金山→纽约。
- 若其中一处发生灾难,另一处仍可继续提供服务。
- 应用可以选择将每一次写入同时发送到两个数据中心,从而显著缩小数据丢失窗口。
双向复制会引入写入冲突的可能:例如应用 A 更新某记录,同时应用 B 也更新同一记录;在同一数据中心内,数据库可序列化此类并发;但若两个更新分别落到不同数据中心,底层数据库无法天然串行化,便会产生冲突。
常见的冲突解决机制包括:
- 最后写入获胜(Last Write Wins, LWW)
- 版本向量(Version Vectors)
- 无冲突可复制数据类型(CRDT)
- 人工干预/业务自定义规则
数据分区
旅行聚合网站需要管理海量数据:除预订记录、历史与支付外,还包括点击流、搜索历史等用于分析与洞察的数据,这些数据会在短期内迅速膨胀。
当数据量持续增长,首要挑战是如何在可接受的延迟下托管、管理并服务这些数据。一种常见方法是:增加硬件资源(存储/计算/内存等),并将数据划分为多个相互独立的子集(分区) ,分别托管与服务。这种划分称为数据分区(partitioning) 。
分区方案(partition scheme)的选择将直接影响时延、吞吐与成本,因此尤为关键。本文聚焦两种最常用的方案:哈希分区与范围分区。
哈希分区(Hash Partitioning)
哈希分区常用于实现各分区之间负载近似均衡。做法是对分区键应用哈希函数以得到分区号。以 bookings 表为例,主键 bookingId 为自增整数。对整数型键,取模(mod)往往是良好的哈希函数,尤其当键空间存在空洞时,仍能较好地均匀分布。
示例:有 90 条记录(bookingId 为 0~89)。按 bookingId % 4 分 4 个分区:
- 分区 0:23 条,
0, 4, 8, …, 88 - 分区 1:23 条,
1, 5, 9, …, 89 - 分区 2:22 条,
2, 6, 10, …, 86 - 分区 3:22 条,
3, 7, 11, …, 87
写入时依据哈希计算分区号并落入对应分区;读取时同样用哈希定位目标分区,仅访问相关分区,这称为分区裁剪(partition elimination) 。
哈希分区擅长基于分区键的点查;但对于基于二级键的范围扫描并不理想。
范围分区(Range Partitioning)
范围分区适用于围绕二级键的区间做扫描查询的场景。比如分析师希望查询价格区间在 250 的酒店预订。若仍使用基于 bookingId 的哈希分区,系统需要遍历所有分区的所有记录再过滤,代价高。此时更适合采用范围分区。
做法是按二级键的取值范围划分分区。以上述“预订价格”作为分区键,示例划分为 5 个分区:
- P1:价格 < $101
- P2:201
- P3:301
- P4:401
- P5:价格 ≥ $401
写入时根据价格选择分区;读取时根据查询区间只访问相关分区,实现分区裁剪。例如查询 250 的记录,只需访问 P3。
需要注意,价格在整体数据中的分布通常不均匀,因此范围分区的负载可能倾斜,带来查询性能与成本的波动。
下表总结了分区方案与受益的查询类型:
| 分区方案 | 受益的查询类型 |
|---|---|
| 哈希分区 | 点查(如:给定 userId 查询用户详情) |
| 范围分区 | 区间查询(如:价格在 100~200 的商品) |
| 基于时间的分区 | 时间范围查询(如:查询最近交易) |
表 12.2:分区方案与对应受益的查询类型
其他常见的分区方案
一些其他常见的分区方案包括:
- 基于时间的分区(Time-based partitioning) :按记录中的某个时间字段将数据划分为多个分区。若大多数用户查询基于时间窗口(如“获取过去 10 天内的所有预订”“获取上个月的所有取消”),则时间分区有助于实现高质量的分区裁剪(partition elimination)。
- 轮询分区(Round-robin partitioning) :将数据均匀地分布到所有分区。由于选择分区的逻辑不依赖任何范围或查找键,轮询分区无法用于分区裁剪。
- 垂直分区(Vertical partitioning) :按列(或经常被一起查询的字段)对数据进行拆分。该分区模式与第 10 章《数据仓库与数据分析》中介绍的列式存储具有相似的收益。
Scatter 与 Gather 操作
实际系统通常只选用上述某一种数据分区方案。然而,用户查询可能很复杂,需要在多字段上执行过滤、分组、聚合、LIMIT 等操作。比如:用户要“获取任意 10 个城市,其中酒店预订价格超过 $300”。设当前采用的是基于 bookingId 的哈希分区。由于该查询无需按 bookingId 做查找,它必须遍历所有分区才能找出结果。再假设每个分区由独立存储节点分别执行查询。执行流程如下:
- Scatter(分发) :查询执行器将同一条查询发送到每个存储节点,让其在本分区独立执行相同操作。
- 在每个存储节点上,读取其分区内的所有记录。执行过程中在内存中维护一个“城市集合”,其中每个城市至少有一笔预订价格超过 $300。
- 当每个节点各自找到这样最多 10 个城市后,返回结果给查询执行器。
- Gather(汇总) :查询执行器收集所有节点的返回。根据分区数量与实际数据分布,汇总结果可能多于 10 个城市。查询执行器对结果进行后处理,仅保留10 个城市并返回给终端用户。
图 12.10 直观展示了上述四步的 scatter–gather 数据服务过程:
图 12.10:Scatter 与 Gather
小结
本章我们学习了软件与硬件故障/失效的基础,并认识到为避免业务受损,需要进行数据复制,将数据副本存放于独立硬件上。随后,我们梳理了同步、半同步与异步复制,以及各自的优缺点。
接着,我们了解了如何通过 XDCR(跨数据中心复制) 在灾难场景下保障数据可用性,并认识到双向 XDCR 的优势,以及处理其引入的写冲突的常用机制。
随后,我们引入了数据分区的概念,说明其如何帮助将工作切分到多个存储与计算单元,并介绍了多种分区方案及其如何支持分区裁剪。最后,我们学习了在多分区环境下进行查询时常用的 scatter–gather 模式。
下一章将讨论冷热分层机制与设计模式:如何存放热数据以实现低延迟服务,以及如何存放冷数据以降低存储成本。