| B站视频 : 美团数据库自治服务平台建设
1 背景&目标
MySQL的故障与SQL的性能,是DBA跟研发同学每天都需要关注的两个重要问题,它们直接影响着数据库跟业务应用程序的稳定性。而当故障或者SQL性能问题发生时,如何快速发现、分析以及处理这些问题,使得数据库或者业务系统快速恢复,是一项比较大的挑战。
针对此问题,美团数据库自治平台经过多轮的迭代建设,在多个场景下已经实现了异常的发现、分析以及处理的端到端能力。本文将跟大家分享一下我们平台建设的心路历程,同时提供一些经验、教训供同行参考,希望能够起到“抛砖引玉”的作用。本文主要介绍以下主题:
- 异常发现 :基于数理统计方式的动态阀值策略,来发现数据库系统的指标异常。
- 故障分析 :丰富完善数据库关键信息,来做精确的数据库异常根因分析;深入挖掘内核价值,来解决根因诊断方面的疑难杂症。
- 故障处理 :依据异常根因分析的不同结果,通过自助化或自动化的方式来进行故障的恢复处理。
- 内核可观测性建设 :如何跟数据库内核团队合作,从内核的角度来分析SQL性能问题,通过内核团队大量的内核代码改造,力求将数据库的可观测性跟诊断做到极致。
- 单SQL优化建议 :通过改造MySQL存储引擎,同时结合查询优化来打造基于Cost模式的索引优化建议。
- 基于workload索引优化建议 :基于整个DB或者实例的Workload策略的索引优化建议,为实现数据库的索引自维护提供前置条件。
- 基于SQL生命周期的治理 :实现从SQL上线前、执行过程中、执行完毕后几个环节,以期实现端到端的慢SQL治理。
2 平台演进策略
美团数据库自治平台从下到上总体分为四层,分别为接口与展示、平台功能层,计算与存储、数据采集层,平台的总体架构以及每一层的作用如下:
- 数据库采集层 :要进行数据库的诊断与分析,需要依靠关键的指标以及SQL文本数据,当前在每个数据库实例上部署一个数据采集程序(rds-agent)统一负责采集、上报关键数值指标以及SQL文本数据。
- 数据计算与存储层 :数据采集层上报上来的数据,依托Kafka、Flink&Spark作为数据缓冲,对关键组件进行相关的数据处理,如SQL解析、SQL模版化、数据聚合等操作,再把处理的结果存入ES、Blade(美团自研的分布式数据库)、Hive等分布式数据库或者大数据平台,提供给上层的平台功能层使用。
- 平台功能层 :此层是整个系统最为重要的部分,由于平台同时服务于DBA运维团队及研发团队,所以平台的建设分成了两条路:1)主要面向DBA用户,按照可观测性建设、异常发现、故障根因分析、故障处理几个阶段来进行建设;2)主要面向研发同学,按照SQL优化建议、风险SQL的发现、分析与SQL治理等跟SQL相关的几个阶段来建设。当然,两者并没有严格界限,这些功能所有的用户都可以同时使用。
- 接口与展示 :平台功能层提供的核心功能会通过Portal来展示,同时为了让平台提供的功能更好地集成在用户自己的系统中,我们也通过OpenAPI的方式对外提供服务。
3 异常发现
数据库产生异常时需要尽早地发现,才能防止异常一进步放大,避免造成真正的故障。异常发现的主要方式是对数据库或者OS的关键数值指标进行监控,相关指标包括seconds_behind_master、slow_queries、thread_running、system load、Threads_connected等等,也可以是业务端研发关注的关键指标,如“应用程序访问数据库的报错数量”、“SQL执行平均耗时”等指标来进行监控。如果这些指标短时间内发生比较大的波动,那么数据库很可能出现了一些异常,这就需要及时进行处理。
这些异常如何才能被发现呢?业界一般有基于静态阀值以及动态阀值的两种异常发现策略。前者很简单,如根据专家经验,人工设定seconds_behind_master或者Threads_connected的告警阀值,超过阀值就认为发生了异常。此方式虽然简单易用,但OLTP、OLAP等不同的业务场景,对于相同指标的敏感度是不一样的,如果所有场景都使用统一的静态阀值来做异常发现,难免会有很多误告。而如果每个场景都去手工去调整,既不灵活,成本又太高,解决方案是基于不同场景的历史时序数据,使用数理统计的方式来分别建模,通过拟合出各自场景的模型来作为异常发现的策略。
3.1 数据分布规律与算法选择
基于数理统计方法的异常发现,需要根据具体的场景使用特定的模型。一般来说,模型的选择跟时序数据的分布形态有很大的关系,时序数据的分布并不总是都像正态分布一样都是对称的,而是有些是左偏的,有些是右偏的,当然也有对称分布的。下图就展示典型的三种不同的时序数据分布形态。
针对上面的三种不同时序数据的分布形态,以及每种异常检测算法自身的特性,我们分别采用不同的异常检测算法。
对于低偏态高对称分布选择“绝对中位差(MAD)”,中等偏态分布选择“箱形图(Boxplot)”,高偏态分布选择“极值理论(EVT)”。没有选择3Sigma的主要原因是:它对异常容忍度较低(建模的时候,如果有噪音等异常点也不会对模型的形态产生很大的影响,则说明异常容忍度很高),而绝对中位差(MAD)从理论上而言具有更好的异常容忍度,所以在数据呈现高对称分布时,通过绝对中位差替代3Sigma来进行检测。
3.2 模型选择
数据分布跟算法适用场景的分析之后,对内部的时序数据进行检查,发现数据的规律主要呈现漂移、周期和平稳三种状态,对样本先进行时序的漂移(如果检测存在漂移的场景,则需要根据检测获得的漂移点t来切割输入时序,使用漂移点后的时序样本作为后续建模流程的输入)。
之后同时进行平稳性分析(如果输入时序S满足平稳性检验,则直接通过箱形图或绝对中位差的方式来进行建模)以及周期分析(存在周期性的情况下,将周期跨度记为T,将输入时序S根据跨度T进行切割,针对各个时间索引j∈{0,1,⋯,T−1} 所组成的数据桶进行建模流程。不存在周期性的情况下,针对全部输入时序S作为数据桶进行建模流程),再对时序数据分布特性进行偏度的计算,最后再根据不同的偏度特性选择不同的算法模型,具体如下:
在算法确定之后,先在离线环境针对不同的场景使用历史指标来训练模型,模型训练完毕之后会存入数据库,在生产环境运行过程中,对于不同场景下的数值指标根据其特性来加载不同的模型,并且结合Flink实时计算框架来实时的发现指标的异常并进行告警。
4 异常诊断
发现指标异常后,需要快速的给出异常的根因,我们可以根据具体的根因来选择不同的处理策略,然后进行自动或者手动的恢复工作。根因分析可以基于专家经验,也可以严格地按照内核代码的逻辑来进行分析。
本文重点讲述后者,强调如何使用“内核思维”来解决专家经验很难或者无法解决的诊断问题。本文将列举“内核代码路径分析”、”内核日志分析”、“内核功能增强“、“内核Core Dump分析”以及“内核埋点”等几种不同的范式,来说明数据库根因诊断的思路。
4.1 主从延迟(内核代码路径分析)
这里先介绍“内核代码路径分析”这个方式来诊断根因。对于数据一致性要求比较高的应用程序,seconds_behind_master是一个十分重要的指标,如果其值过大就需要诊断其根因,防止应用程序读取到不一致的数据。根据专家经验,其值过大可能由“QPS突增”、“大事务”、“大表DDL”、“锁阻塞”、“表缺少主键或者唯一健”、“低效执行计划”、“硬件资源不足”等因数造成,把这些专家经验总结成规则列表,当异常产生时逐个迭代去验证是不是符合某个规则,据此来诊断根因,然而此方式存在如下两大问题:
- 无法枚举所有根因 :经验由于其固有的局限性不可能考虑到所有的故障场景,如何完整的给出造成seconds_behind_master值异常的所有规则是一个挑战;另外,如果需要对一个全新的指标进行诊断,而在没有任何的专家经验情况下,如何能快速地整理出完整的规则列表?
- 缺乏对根因的深层次理解 :“QPS突增”、“大事务”、“大表DDL”、“锁阻塞”、“低效执行计划”、“硬件资源不足”等因素会造成seconds_behind_master指标值的异常,但是为什么这些因数会造成指标值的异常呢?如果不从内核源码角度来了解这些因素跟seconds_behind_master之间的逻辑计算关系,那么是无法了解真正原因的。
4.1.1 内核代码路径分析
针对上面两个问题,具体策略是直接看seconds_behind_master这个变量在内核层面是如何计算的,只有这样才能完整的枚举出所有影响seconds_behind_master值计算的因数。
从 源码角度 看,seconds_behind_master的值由①time(0)、②mi->rli->last_master_timestamp和③mi->clock_diff_with_master这三个变量来决定(代码层面seconds_behind_master的计算逻辑是:seconds_behind_master=((long)(time(0) - mi->rli->last_master_timestamp)- mi->clock_diff_with_master),其中time(0)是系统当前时间(用秒表示),clock_diff_with_master这个值的计算很复杂、又很关键,会放到下一节详细进行说明。
而针对 mi->clock_diff_with_master 的计算,这个变量从源码层面看就是主、从实例之间的时间差;根据当前的信息就可以看出来,从库的当前时间以及主从库之间的时间差都会影响seconds_behind_master值的计算。seconds_behind_master的计算和事务在主从库执行的情况如下:
last_master_timestamp计算逻辑
从上面分析可以知道,last_master_timestamp值是影响seconds_behind_master值计算的关键变量,所以很有必要从源码角度分析影响last_master_timestamp值的因数有哪些(从而间接获取了影响seconds_behind_master值的因素)。
last_master_timestamp的值来自于一个叫rli->gaq->head_queue()的成员变量 new_ts (此处的rli->gaq->head_queue()是指代某个最新的已经完成replay的事务对应的event group,event group是指一个事务在binlog文件里生成一组event来表示某个事务,这个event group里的event从主库传输到从库后进行replay操作来还原主库的事务)。new_ts值来源于rli->gaq->head_queue())->ts,而rli->gaq->head_queue())->ts的值是通过ptr_group->ts= common_header->when.tv_sec + (time_t) exec_time计算获取的。
再看一下when.tv_sec跟exec_time的含义,前者指代SQL在主库上的SQL执行的开始时间,后者指代SQL在主库上的“执行时长”,“执行时长”又跟“锁阻塞”、“低效执行计划”、“硬件资源不足”等因素息息相关。
值得注意的是,前面提到的rli->gaq->head_queue())->ts的计算跟slave_checkpoint_period以及 sql_delay 两个变量也有关系,按照这个思路层层迭代下去找出所有影响seconds_behind_master值的因素,这些因素都是潜在的主从延迟异常的根源,这样也解决了前面说的“无法枚举所有根因”跟“缺乏对根因的深层次理解”两大问题。
为了便于理解上诉的逻辑,放出关键的源代码:获取last_master_timestamp值的来源rli->gaq->head_queue()的成员变量 new_ts 。
bool mts_checkpoint_routine(Relay_log_info *rli, ulonglong period,
bool force, bool need_data_lock)
cnt= rli->gaq->move_queue_head(&rli->workers);
.......................
ts= rli->gaq->empty()
: reinterpret_cast<Slave_job_group*>(rli->gaq->head_queue())->ts; //其中的ts来自下面的get_slave_worker函数;
rli->reset_notified_checkpoint(cnt, ts, need_data_lock, true);
// 社区版本的代码 rli->reset_notified_checkpoint(cnt, rli->gaq->lwm.ts, need_data_lock);
/* end-of "Coordinator::"commit_positions" */
......................
获取Master实例上执行的SQL的开始跟执行时长信息tv_sec跟exec_time。
Slave_worker *Log_event::get_slave_worker(Relay_log_info *rli)
if (ends_group() || (!rli->curr_group_seen_begin && (get_type_code() == binary_log::QUERY_EVENT || !rli->curr_group_seen_gtid)))
..............
ptr_group->checkpoint_seqno= rli->checkpoint_seqno;