今年 3 月 GitHub 在一周内出现了多次服务不可用的情况,每起事件持续时长在 2-5 小时,据有媒体统计,GitHub 在一周中多次中断影响的开发者数量高达 7300 万。事后 GitHub 高级工程副总裁 Keith Ballinger 发文表示,「我知道这会影响许多客户的生产力,我们也非常重视这一点。过去几周发生的宕机事件根本原因是我们的‘MySQL1’集群中的资源争夺,在负载高峰期,影响了 GitHub 大量服务和功能。」
数据库故障对企业系统服务的影响可见一斑,今天我将结合 B 站自身的实战经验来跟大家说说数据库故障治理相关的心得:
作者介绍
B 站 DBA Leader-王志广
十年以上数据库运维经验,曾在多家大型互联网公司任职,主导和参与了多家数据库私有云建设、数据库多活、数据库架构从商业数据库到开源数据库的迁移。目前主要专注于 B 站数据库多活、数据库服务治理等。
温馨提醒:本文约 4900 字,预计花费 8 分钟阅读。
「TakinTalks 稳定性社区 」公众号回复 “交流” 进入读者交流群;
一、什么是数据库故障
数据库故障顾名思义就是数据库相关的故障,它在学术上没有明确的定义,故各公司一般以对应用的影响范围来量化定义数据库故障。如下图所示:
二、常见的数据库故障有哪些?
故障治理就得对症下药,所以治理的第一步就是明确常见的数据库故障有哪些,今天就 MySQL、缓存两个大方向来跟大家一起梳理一下。
1、MySQL
作为在互联网公司广泛使用的传统意义上的数据库,MySQL 的故障可以分为以下几种:
1.1 实例不可用
数据库作为一种特殊的应用,在其生命周期内无法保证 100%可用。数据库实例不可用的原因一般有:
-
硬件故障:硬件故障是生产环境中无法避免问题。常见的硬件故障包含了 CPU、内存、磁盘、主板等。
-
系统故障:系统故障包含操作系统 bug 和数据库 bug。
-
网络故障:网络故障包含网络设备故障和专线故障。
-
其他:如资源分配不合理导致的 OOM。
1.2 数据延迟
数据延迟包含数据库的主从延迟和依赖 binlog 的数据订阅服务的延迟。
1.2.1 主从复制延迟
主从复制延迟是线上经常出现的问题之一,对于读写分离的业务来说,从库延迟的影响可能是致命的。导致主从延迟的原因一般有:
-
主库执行了一个非常大的数据变更事务;
-
主库变更频率过快, 导致从库跟不上;
-
使用原生的表结构变更语句进行大表的 DDL ;
-
从库配置参数较低、硬件性能较差;
-
从库负载过高,导致性能变差 ;
-
从库 MDL 锁 ;
-
主从之间网络问题 ;
-
主从复制 Bug (多线程复制 bug 最为常见)。
1.2.2 数据订阅服务的延迟
binlog 的消费是数据订阅服务中的核心功能之一,对很多依赖 binlog 消费的业务来说,订阅服务的延迟将导致线上业务的不可用,导致订阅服务延迟的原因一般有:
-
上游从库延迟
-
Kafka 性能瓶颈
-
canal 类组件性能瓶颈
1.3 数据损坏
数据损坏随时可能发生在任何人身上,没有任何办法可保证它不会发生。导致数据损坏的原因如下:
-
未开启双一参数,在宕机场景下丢失;
-
未使用半同步复制,在宕机场景下丢失;
-
使用三方工具进行 DDL 的某些特殊场景下可能会丢, 比如 pt-osc、gh-ost;
-
误操作、误删除数据或者蓄意删除,也就是常说的运维人员删库跑路。
1.4 性能下降
数据库的性能下降一般有以下几种表现慢查询增多、性能抖动、压力过载。
1.4.1 慢查询增多
慢查询也是我们经常会遇到的场景,严重情况下会导致数据库雪崩, 一般导致慢查询增多有如下原因:
-
新业务上线 SQL 效率较差或者没有合适的索引;
-
业务场景发生改变, 边缘场景被触发;
-
数据倾斜, 导致未走合适的索引;
-
Innodb buffer pool 命中率低, 触发大量物理读;
-
优化器 bug, 导致未走最优索引宿主机磁盘异常。
1.4.2 性能抖动
正常情况下数据库响应时间比较稳定,但是数据库一些内部行为也会导致数据库偶尔出现性能抖动,比如对于 MySQL 来说刷脏(触发同步刷脏时尤为明显)。一般我们评估一个实例是否正常,会根据 P999 分位的响应时间来判断, 当然还有其他的一些可能会导致性能抖动的原因, 比如慢查询、批量操作、定时任务等。
1.4.3 压力过载
压力过载一般会出现在如下的一些场景:
-
突发流量;
-
上游缓存失效;
-
大型活动中流量超过预期。
2、缓存
常见的缓存故障包含以下几种:缓存穿透、缓存击穿、缓存雪崩,下面展开详细说说。
2.1 缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义。
造成缓存穿透的原因有两个。
-
自身业务代码或者数据出现问题(例如:set 和 get 的 key 不一致)。
-
一些恶意攻击、爬虫等造成大量空命中。
2.2 缓存击穿
缓存击穿主要是是以下两种场景:
-
当前 key 是一个热点 key(例如一个秒杀活动),并发量非常大。
-
重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
2.3 缓存雪崩
缓存雪崩,一般是由大量的 key 集中在一段时间内失效,或者 redis 服务故障所引起的,导致查询请求查不到缓存,大量的请求涌入数据库。
缓存雪崩会导致以下两种后果:
-
数据库瞬间被大量的流量打死
-
该服务接口访问时间过长,导致耗尽了线程资源,从而引起整个系统的崩溃。
三、B 站在数据库故障治理方面做了啥?
上面讲述了常见的数据库故障,下面我们来说说如何从数据库架构设计上来规避故障,这里会结合 B 站的一些实践来跟大家分享。
1、高可用
高可用(High Availability)是系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。所以在做数据架构设计时一定要考虑 HA 能力,无论是选用集群模式(MGR、PXC 等),或是使用传统主从复制加上其他高可用组件(Orchestrator、MySQL Replication Manager、MHA 等),都可以帮我们在实例不可用时把损失最小化。
2、扩容
扩容一般分为垂直扩容和水平扩容两种,对于传统 MySQL 来讲,垂直扩容的收益比通常不高, 而基于其本身状态的水平扩容又很难做到快速,所以如何做应急扩容一直是业内比较头疼的问题。
这时候引入云原生数据库是一个不错的选择,业内的云原生数据库大都进行了存储计算分离改造,对于计算层可以通过 k8s 的 HPA 能力进行快速扩容,而存储层则依赖于底层共享存储或者分布式存储也有不同的扩容方式。
在 B 站我们大量引入了 TiDB, 在一些适合弹性伸缩的场景下有不错的收益。
-
垂直扩容:增大 buffer pool、 cpu 限制等配额,需要平台侧完善 Quota 管理和变更能力。
-
水平扩容:对于 MySQL 来讲, 应急快速水平扩容只能用于读负载,我们会有一些异地机房的实例,一般情况下用作灾备以及离线用途, 在极端场景下可以通过平台操作一键接入读负载池;对于 TiDB 来讲,我们把计算节点放入 k8s,通过 k8s 的 HPA 能力进行水平弹性伸缩。
3、多活建设
多活是高可用的升级版,一般分为异地灾备、同城多活、异地多活、两地三中心、三地五中心等不同的架构设计。(回顾传送门:B站SRE负责人亲述713故障后的多活容灾建设 )
-
异地灾备:当主机房的实例全部不可用时进行切换,一般是由高可用组件触发切换,高可用组件本身应该是一个分布式跨机房的架构。比如在 B 站, 我们的高可用组件是三机房投票决策的,同时应当注意避免跨机房专线异常造成的误判以及误切换熔断策略。
-
同城双活:主库在同城某一个 zone,从库则分布在同城的不同 zone,每个 zone 的读请求实现单元内闭环,写请求则跨 zone 访问对应的主库,需要在数据库 proxy 或者 sdk 层实现请求路由的自动感知和判断。
-
异地多活:依赖业务单元化改造,读写请求全部单元内闭环,要求整体架构具备单元流量调度以及异常流量矫正的能力。属于同一个 global cluster 的集群间通过 DTS 进行双向数据同步,DTS 层需要解决数据回环问题以及冲突检测机制,对于数据冲突的场景提供不同的策略,比如覆盖策略、暂停同步策略等。对于冲突数据也可以考虑写入消息队列,便于业务侧矫正处理,同时应具备全局发号器主键填充能力,避免双向同步的主键冲突。
-
两地三中心: 可以简单理解为同城多活加上异地灾备架构,其中灾备机房应当根据灾难承受程度和数据保护程度来权衡地理距离。
上述几种架构中, 异地灾备和同城双活的读流量通过数据库接入层管控进行切流,比如 proxy、sdk 的元数据管理,写流量则以数据库高可用组件的切换作为切流手段;而异地多活架构下的切流方案一般是通过应用上层切换,比如 CDN、SLB、应用网关等实现。
4、Proxy 数据库代理
数据库代理(又名 proxy)是位于数据库服务和应用服务之间的网络代理服务,用于代理应用服务访问数据库时的所有请求。
数据库代理一般包含以下核心功能:
-
读写分离
-
拦截
-
限流
-
熔断
通过对 Proxy 的大量使用,我们可以实现针对某个数据库、某个服务、某类 SQL 指纹进行拦截,限流、熔断来阻止某些异常流量打崩数据的场景。
5、慢查询预警
慢查询是导致数据库性能下降中少数具有可预测性的内容,为此我们专门建设了慢查询预警体系,这套体系包含日志采集、流式处理、结果分析、告警及其他操作(如:自愈等)。慢查询预警系统的采集样本是过去 7 天同时间段前后十分钟,和当天前 10 分钟数据,通过多次线性回归,我们实现了对偶发性的抖动的过滤,不同业务级别环比倍数、持续性增长(未到阈值倍数,但持续增长或存在)慢查询的预警,并且基于规则引擎实现自定义处理。
6、一个具体的小案例分享
下面分享一个针对复制 bug 的处理过程。
-
问题背景:
业务研发收到多起创建预约的问题反馈,如:创建预约后拉取不到预约详情
查询从库未读取到相关数据
-
核心处理步骤:
DBA 先检查从库复制状态,未发现复制异常;使用 show global variables like ‘%gtid%’检查 gtid,发现 gtid_binlog_pos 不发生变化,故判断可能是 mariadb 多线程复制 bug 导致数据不同步;与业务研发同事沟通后,通过修改 proxy 配置将所有流量切换到主库,同时切换 canal 到主库,最终因复制 bug 导致的业务影响很快被控制下来。
-
后续优化:
-
增加心跳表以规避复制 bug 导致的常规复制监控不可用。
-
基于高可用、心跳表、数据库负载(QPS、CPU、网络等)等信息,实现在从库异常情况下将流量切换到主库和数据订阅的数据源切换到主库。
-
说明:
触发该复制 bug 后,使用 show slave status 看不到复制状态的任何异常,下图是当时的监控:
四、数据库故障演练应该怎么做?
故障演练是一种遵循混沌工程实验原理的解决方案,提供丰富的故障场景模拟(如宕机、CPU 负载过高、网络延迟、丢包、磁盘写满、I/O 飙高、OOM)等,能够帮助分布式系统提升容错性和可恢复性。
故障演练需要建立一套标准的演练流程,包含准备阶段、执行阶段、检查阶段和恢复阶段。
数据库作为基础软件,其自身的故障演练不能独立于应用场景,而是作为应用系统演练的一部分存在。下面分享下一个故障演练的案例。
B 站很多业务的数据库是 Gzone 架构,保证单机房的数据库、缓存是否可以承载业务的全部流量是 Gzone 架构的核心要求。下图是模拟单机房故障演练的架构图:
此次演练是模拟在同城一个 IDC(B 机房)因某种不可抗力的情况下,通过 CDN 将流量切换到 A 机房,验证单机房中应用、缓存、数据库是否可以承载全部流量,出现非预期内的情况下现有预案是否可以覆盖相关场景、相关关联系统的可用性等。
最终我们通过模拟 check 了数据库切流的效果以及相关预案的覆盖度、有效性,为系统的稳定性增加了一份保障。由于故障演练这块的内容比较多,下次我们再开一篇来说说故障演练的流程与实操。(如果你对 B 站「故障演练」感兴趣,后台回复“0922”订阅相关内容)
五、总结与展望
万物流变,无物常驻。博尔赫斯 · 赫拉克利说过:没有人能两次踏进同一条河流,故障也是类似的,每次故障发生的时间、地点、原因、影响范围、处理的人都不同。若从这个角度来看故障治理,它是在解决过去某一个时刻的问题,那为什么还要做故障治理呢?
简单来说,故障治理的目的就是通过技术手段提前规避类似的故障问题,不让系统在同一个地方跌倒两次,而且大量的实践也证实了这些做法的有效性。
未来 B 站在数据库故障治理方面也会持续发力,我们将持续关注数据库故障标准化、演练常态化。通过常态化的演练更早发现和暴露问题,不断丰富的故障场景,可以做到最小粒度定义故障、发现故障和处理故障。
公众号回复【0922】订阅故障演练内容
公众号回复【交流】进入读者交流群
公众号回复【7161】获取「B 站多活容灾建设」资料
声明:本文由公众号「TakinTalks 稳定性社区」联合社区专家共同原创撰写,如需转载,请后台回复“转载”获得授权。
本文来自投稿,不代表TakinTalks稳定性技术交流平台立场,如若转载,请联系原作者。