网易杭研总结:数据库高可用技术之道(1)
转载来源: https://zhuanlan.zhihu.com/p/84197454
数据库作为IT系统中最关键的服务之一,其可用性一直是系统设计中的重点考虑因素。同时,由于数据库有数据有状态的天性,数据库高可用有其天然的复杂性和难点,云原生架构下尤其如此,是一个值得深入探讨的课题。本系列文章将基于网易杭州研究院的研究与实践,解析数据库高可用技术要点,梳理主流数据库方案,为数据库技术建设规划提供参考。
本文由作者授权网易云发布,未经许可,请勿转载!
作者:倪山三,网易杭州研究院运维工程师
Catalog
-
- Prologue
-
- 数据库高可用技术要点
-
- 1.1. 冗余设计 — 数据冗余方案
- 1.2. 冗余设计 — 冗余数据间的一致性
- 1.3. 冗余设计 — 实例冗余
- 1.4. 故障切换 — 服务角色
- 1.5. 故障切换 — 故障感知与应对
- 1.6. 故障切换 — 业务连接切换
-
- 主流数据库方案概览
-
- 2.1. 主流数据库高可用功能概览
- 2.2. MySQL数据库高可用功能概览
0. Prologue
数据库作为IT系统中最关键的服务之一, 其可用性一直是系统设计中的重点考虑因素. 同时, 由于数据库有数据有状态的天性, 数据库高可用有其天然的复杂性和难点, 加之云原生架构的技术趋势又提出了新的要求, 因而是一个值得深入探讨的课题.
我们谈高可用, 通常是指一个系统作为整体, 在系统内部分组件受软硬件故障影响的情况下, 对外提供持续不间断服务的能力. 说白了是一种系统内部的容错能力。
系统内部错误有大量不同形式, 例如应用程序和操作系统软件都可能触发bug导致hang死; 资源可能出现瓶颈发生CPU过载或内存OOM; 硬件可能有问题导致磁盘无法读写, 网络异常中断等等. 进一步的, 对一个系统来说, 随着部署规模不同, 错误的影响规模也会不同. 有时候错误影响一个服务程序实例; 有时候影响一台主机; 有时候一个交换机下一组机柜; 有时候是整个机房乃至地区. 因此我们针对系统进行容错设计的时候会提供面向不同类型故障和不同级别故障的应对方案, 例如一些架构设计中的常见的名词: 服务单点故障, rack aware, 机房容灾… 这些都是对同一容错需求的不同表述.
但是说到底, 计算机系统同大部分人造工业系统一样, 容错技术的根本思路只有一条:
- 冗余设计.
- 如何在发生问题时使用上冗余资源.

图1, 飞机液压系统大致布线, 绿, 蓝, 黄分别是三条具备独立工作能力的冗余走线. 并且还要具备自动和手动双份切换单元. IT系统容错设计也同样别无他法.
在这个目标前提下, IT系统内部各层服务根据自己的实际情况来进行高可用设计. 通常占据系统内多数节点的应用程序部分, 其核心资源是应用代码本身, 非常容易通过发布广泛冗余, 这类前后两次请求间一般没有强关联的无状态服务, 高可用设计相对简单, 只要有多个执行相同业务代码的服务实例, 加上前端一种具备负载均衡和后端探活能力的代理服务, 就可以看做一个简易的, 具备内部容错能力的高可用系统.

图2, 利用带健康检查的nginx, 就能够很容易组建一套具备后端容错能力的高可用集群. 当然实际业务环境的分发层设计远比这复杂, 只是想表达应用层服务冗余通常并不困难.
在这一设计中, 相同代码server的多节点部署就是冗余设计, Nginx层的负载均衡和健康检查屏蔽, 就是容错发生时的请求分发控制.
而在数据库这一环节, 情况就要更复杂一些. 数据库的核心资源是数据, 因此作为一种服务, 数据库不但要对其程序实例进行冗余设计, 更需要对其管理的数据进行冗余保护. 数据冗余的难点一方面是系统数据量肯定远大于代码量, 但是更核心的难点是数据随时都在频繁变化, 这使得数据库冗余方案往往比核心资源两次发布间不会修改的应用层来的复杂.
如果我们不想搞的很复杂, 按照应用层等价冗余服务间一般互相不建立关系, 通过分发层提供可用性保护的思路, 其实我们也能做一个类似的方案 —— 同步变更(双写)冗余系统.

图3, 由应用程序同时写两个数据库或数据源其实是非常常见的方案, 但是实现上有很多需要注意的地方, 是个不太容易做的非常好的思路.
但是实践证明简单粗暴的双写冗余有着很多缺陷, 比如, 我们搞冗余是要提高可用性, 如果双写要求强一致, 那其实是多引入一个故障点; 如果不是强一致写, 允许故障服务暂停写入, 那恢复后数据一致性如何补全; 另外两个库的响应时间短板还要直接影响业务响应时间 … 等等, 所以到头来发现设计一个合理的同步变更冗余系统其实难度很高. 数据库更倾向于通过多实例互相通信关联, 组成集群, 内部来解决冗余问题, 不需要业务做太多额外的设计.
多年发展下来的结果就是各种数据库自成一派, 我们看到各种数据库往往都是以一组集群为单位向外提供服务, 对外黑盒, 内部解决冗余问题, 充满神秘感. 我们在各种渠道能够频繁看到各种各样的数据库架构图, 好像没有哪个数据库架构图是一样的:

图4, MySQL, MongoDB, Redis, HBase, Oracle五种主流数据库的高可用架构示意, 实现各有千秋, 甚至同一种数据库也有多种实现形式和架构.
由于各种数据库对于可用性保证方面的实现与思想有着许多不同点, 由此带来的问题是, 各种数据库到底能做什么, 怎么做, 是否足够做的好, 是否有应用设计上需要关注的地方, 业务开发同学可能通常对这些方面了解不够, 有一些片面的认识, 甚至存在理解误区. 这是本文的出发点, 希望能够帮助大家对各种数据库高可用架构设计与实现的共性与差异, 加深一点了解, 在业务上实际使用各种主流数据库的时候, 能够得心应手.
1. 数据库高可用技术要点
我们先来看数据库高可用架构设计的共性部分. 抛开复杂多变的具体实现, 仅从关键技术点和解决思路来看, 数据库高可用方案可以分为两个根本要点, 各三个共通的核心技术点, 以及市面上各种主流产品对应的一系列解决思路, 如下图所示.

图5, 数据库高可用架构很多样, 但是归纳起来无非是通过不同设计实现解决少数几个有共性的技术问题, 并组装起来.
我们会先一个一个介绍每个技术要点的意义, 以及对应的主流产品的设计思路. 这部分内容会显得支离破碎, 难以消化, 但是不要紧, 下一章节我们会对大量现实中的数据库产品整合完成的方案做分析说明, 相信到时候就能理解这其中从局部零件到完整功能的转变.
1.1. 冗余设计 — — 数据冗余方案
作为数据库服务的核心价值, 数据冗余的意义显而易见. 除了几乎必备的磁盘RAID阵列本地冗余防止单一磁盘故障外, 我们通常还要求在不同的存储介质上冗余一份数据, 以防止单机, 机柜, 机房级别的故障; 退一步讲, RAID修复过程中对IO性能也有一定影响, 因此跨介质的冗余是必须的.
而且这份跨介质冗余最好是以尽可能实时的形式体现. 这里要提可用性理论的一个重要概念, MTTR(mean time to restoration), 我们肯定希望任何情况下, 服务的MTTR都稳定且尽可能短, 而恢复数据库服务无非几种方案:
- 故障服务自身恢复服务, MTTR波动大, 由于需要持久化日志回放, MTTR可能在数秒到个小时间波动, 更关键的是存在硬件故障无法恢复的可能性.
- 通过冗余服务替代, 需要数据实时同步, 只要同步功能未过载, 通常1~2分钟以内完成恢复, MTTR较稳定.
- 从备份数据恢复, 线上服务通常不可接受的方案, 根据数据量大小, 可能花费数小时到天级别的时间.
并不是说通过数据冗余主备切换恢复是最快的, 有时故障实例自己恢复过来可能要比切换还快, 但是对可度量的MTTR以及必定能成功的恢复概率的追求, 使得数据冗余主备切换成为了高可用方案的主流.
1.1.1. 事务日志异步同步
关于如何做到数据跨介质实时冗余, 数据库业界采用最广泛的是基于事务日志传输回放的方案, 在数据库软件层面解决该问题. 理由是改方案软硬件门槛低, 几乎只依赖普通TCP网络, 是成本最优解.

图6, MySQL replication架构.

图7, Oracle dataguard的架构, 可以看到, 和MySQL的方案在流程上几乎一致.
由于数据库本地事务持久化需求, 记录一份事务串行日志是客观需要, 这份日志能够帮助数据库从异常重启或者历史备份中通过事务回放, 补全所有在线变更, 起到保证持久化特性的功能. 同理, 这份日志通过网络传输到另外一个数据库, 通过完全回放, 能够起到使目标库和源库数据完全一致的效果.
支持该方案的常见数据库包括: MySQL, Oracle, SQL-Server, PostgrSQL, MongoDB, Redis … 几乎全部传统RDB和很大一部分NoSQL. 可以说对我们而言, 线上80%的数据库数据冗余是通过传输回放日志架构实现的. 我们通常管这一方案叫主从(master-slave), 大家应该都非常熟悉.
这里值得注意, 对于MySQL和Redis来说, MySQL事务支持分不同引擎实现不同, 而Redis根本不支持持久化, 因此这两种数据库使用的事务日志可以说基本是专门为做数据冗余同步准备的, 并不是传统的redo log, MySQL不同存储引擎有各自专门的redo log.
该方案的也有着非常显著的缺点. 我们之前提过, “只要同步功能未过载”的情况下, 冗余切换MTTR稳定性最好. 但是哪我们线上最常用的MySQL来讲, 同步功能过载是一个很容易发生的问题.

图8, 日志同步的两个步骤.
从上图可以看到, 日志传输回放有两个阶段, 第一步是接收日志并本地持久化, 第二步是将接收到的日志中的SQL回放到本地数据库中. 第一步通常很少有大量延迟的情况, 而且后面会提到有机制保证这一步延迟不会影响数据一致性. 但是第二步日志回放却是一个非常低效的过程, 主要有两个原因:
- 由于回放SQL要再在从库花一遍和主库相同的更新耗时, 包括数据定位, 缓存读, 日志持久化, 刷脏…等等, 流程长, IO开销大.
- 进一步的, 由于从库回放的是串行事务日志, 为了保证数据一致, 数据回放逻辑不能出错, 其更新并行度远不及主库, 主库并发越高, 从库延迟可能性越大.
我们把这个情况下遇到的日志到了从库, 但来不及回放到数据库中的情况称为复制延迟. 复制延迟的结果就是从库可访问的数据同主库不相同, 落后回放延迟的时间. 这个情况下, 显而易见地, 高可用切换不可能启用存在数据延迟的从库, 因此可见日志回放延迟对高可用保障是致命的威胁. 很遗憾, 根据实际经验, MySQL主库高并发写的情况下, 数千TPS, 一万TPS以下, 无论底层硬件如何优秀, 延迟都是不可避免的. 尽管MySQL近年来通过并行复制等手段想要改善这一问题, 但是面对线上多变的业务场景, 其实效果并不显著.
存在这一问题的数据库包括MySQL, MongoDB, PostgreSQL. Redis由于内存效率高, 几乎不太可能遇上这个问题, 而Oracle由于redo回放能够做到block级别并行, 其效率远胜MySQL, 虽然有复制延迟的可能, 但是线上并不常见.
那么作为我们数据库主力的MySQL应用, 我们如何解决线上这一问题? 其实很简单, 通过分布式集群不断对底层数据库扩容, 只要扩到单实例负载在延迟风险线以下, 就能够回避延迟的问题.
基于日志回放架构也有显著的好处, 那就是冗余的备库数据都是天然可只读的, 在读写分离需要显著增加读负载节点的场景下非常合适.
1.1.2. 存储同步
接着上面, 通过日志复制数据存在日志回放性能低下造成复制延迟的巨大风险. 那么MySQL作为业界应用最广泛的数据库之一, 除了不断水平扩容限制主库吞吐量外, 是否还有其他手段可以帮助解决这个问题.
作为云计算规模领先的AWS, 其RDS中大部分实例也是MySQL兼容型的, 但是AWS从设计初期就没有选择MySQL服务的日志同步, 而是在EC2和EBS间引入了存储层同步层来实现.

图9, AWS RDS高可用架构的示意图
存储层读写同步的优势一个是可以绕开SQL apply的开销和事务前后逻辑造成的并行度不足问题, 存储同步本身的效率可能影响主库写性能, 但是不会出现复制库应用延迟的情况. 同时一套方案适用于多种不同数据库 —— 无需关心每种数据库间架构差异, 因为对操作系统来说, 向存储层发送的请求是没有区别的, 只要这些写请求能够被复制, 那么数据库数据持久化的结果可以认为一致.
但是存储层同步也有较高的技术门槛, 显然, 任何环境下我们都可以简单地配置基于日志传输和回放的数据冗余同步, 但是EC2和EBS存储同步方案确无法运用于AWS外的环境. 而存储同步虽然无需数据库操心延迟和架构问题, 但是同时引入了EBS层的异地读写性能和块一致性保障难题. AWS在此基础上提供的高可用RDS服务实际上是有一定技术壁垒的, 甚至对EBS有大量额外负担的.

图10, AWS Aurora论文中谈到的存储层同步造成的写放大问题, 相较于日志同步, EBS存储层同步需要承担5~10倍以上的同步数据传输开销, 这是额外开销, 也是下一代云原生技术需要解决的问题之一.
其实AWS实现存储层写同步的底层技术方案并不神秘, 在2015年云原生数据库Aurora发布的时候, AWS曾描述了一些传统RDS的实现细节 ( https://www.youtube.com/watch?v=CwWFrZGMDds), 包括多AZ间存储同步的底层技术栈, 使用的就是有十几年历史, 2009年Linux 2.6 Kernel 已经纳入主线的DRBD(Distributed Replicated Block Device)技术.

图11, DRBD技术, 一种基于软件和普通tcp/ip协议, 在服务器之间的对块设备 (硬盘, 分区, 逻辑卷等) 进行镜像的技术.
这一技术对业界并不陌生, 但是就国内数据库基础架构研究领域的热度来看似乎对该方案没有引起足够重视. 虽然大家都知道DRBD可以运用于数据库高可用方案, 但是包括网易内部做私有云的时候, RDS高可用数据冗余设计毫不迟疑的选择了日志同步. DRBD普遍被认为开销大, 对主库IO性能有影响, 并且由于未广泛验证稳定性存疑. 说来很惭愧, 一直以来我们并没有对DRBD进行堪称足够的研究和测试, 而这方面的研究即使在今天看来也是有很大价值, 特别是万兆设备的普及后. 同时需要说明的是, 基础技术归基础技术, AWS在应用DRBD技术中其资源调度, 网络设计, 吞吐量隔离和保障方面依然有着显著的技术优势.
除此以外, 存储同步除了通用开源的纯软件实现外, 高端存储设备制造商也结合各自的专有软硬件环境提供类似的数据冗余保障方案, 例如EMC的MirrorView或VPLEX技术, IBM的PPRC技术等. 这些技术方案也有较好的历史声誉和完善的功能, 但是较高的软硬件门槛和相对封闭的环境是其广泛运用的主要障碍.
存储层同步技术做数据冗余适用于任何计算和存储未进行分离的数据库种类, 包括绝大部分关系型数据库和MongoDB. Redis由于数据全内存不涉及块设备, 因此不适用. 另外存储同步有一个显著问题就是冗余的那份数据由于内存一致性问题, 通常是不可读访问的.
1.1.3. 存储池化
刚才提到了所谓存储计算未分离的数据库, 也就是数据库数据读写直接访问本地块设备的情况. MySQL, Oracle等传统RDB基本都是属于这一类型. 后来发展起一些所谓分布式NoSQL, 以HBase为代表; 一些时兴的NewSQL, 以TiDB为代表; 以及一些云原生数据库, 以AWS Aurora和阿里云PolarDB为代表, 这些新兴数据库在架构上做到了存储和计算分离, 存储作为一个池化的资源服务存在, 计算通过网络访问存储服务, 而不再依赖本地挂载的块设备. 在这一情况下, 独立的存储服务其数据冗余模式就和传统方案有着巨大区别.
额外提一下, 可能有同学提出Oracle的ASM技术也是存储服务化, 不过由于ASM设计初衷还是替代裸设备, 功能上和存储计算分离有所区别.

图12, 分布式NoSQL代表的Hbase架构, 数据持久化完全脱离了操作系统读写, 依赖网络分布式文件系统HDFS, 对于Hbase来说, HDFS服务就是存储池.

图13, 分布式NewSQL代表的TiDB架构, TiDB历史上曾经使用Hbase作为存储层, 不过目前已经转移到自研的TiKV这一专用的分布式存储层.

图14, 云原生数据库代表的AWS Aurora, 绿色方块代表存储层, 是一个对上层SQL服务层独立的池化服务.
在存在独立存储服务层的方案中, 数据多副本冗余不需要数据库服务层操心, 而是存储服务应当具备的基本功能. 通常数据库层对存储服务进行基于网络的写请求, 一旦存储服务返回写成功, 那么就应该在其内部完成了某种形式的多副本冗余保证. 并且存储层故障的时候, 上层数据库也不用操心如何切换访问副本, 这一切都是黑盒的, 如何做到数据不丢以及不间断访问, 都由存储服务来实现.
发表回复