今天我们来聊一下数据库的性能优化,第一部分简单介绍一下性能优化的通用的方法,第二部分我们讲一个实际案例。
性能优化这个事情核心只有一句话,用户响应时间去哪儿了?性能优化很困难的原因在于,为了定位用户响应时间在各个模块的分布,需要对系统的各个部件进行测量和分析,从底层硬件,CPU、IO、网络到上层应用架构,应用代码跟数据库的交互方式都需要涉及。
用户响应时间
性能优化的第一个概念是用户响应时间。用户响应时间是用户在使用一个业务系统的时候,发起一个请求到这个请求返回总体消耗的时间为用户响应时间。一个典型的用户响应时间的分布如下图:
从时序图看,一个用户响应时间可能包括:用户请求到达应用服务器的网络时间
应用服务器本身业务逻辑处理时间
应用服务器跟数据库服务器之间交互消耗的网络时间
数据库多次处理 SQL 的时间
应用服务器返回用户数据的网络时间
数据库时间
数据库时间为单位时间内数据库提供的服务时间。对比数据库时间和应用总的用户响应时间,可以判断应用系统的瓶颈是否在数据库中。
平均 TPS X 平均事务延迟 X ΔT
平均的 QPS X 平均的延迟 X ΔT
平均的活跃连接数 X ΔT, 下图数据库活跃连接图的面积即为数据库时间
趋近 0,数据库时间在总的服务时间里面是很小的占比,说明瓶颈并不在数据库中。
趋近 1,说明整个应用系统瓶颈是在数据库里面。工程师通过降低数据库时间来进行性能优化,比如优化 SQL 执行计划、解决数据库中存在的热点争用等。
实际案例
背景
这个例子是我们与合作伙伴一起完成的课题,银行核心应用在分布式数据库和国产 ARM 服务器上联合优化的案例。系统的硬件采用的是 ARM 服务器,每台服务器有 16 个 Numa,每台机器有一个 NVMe 盘。银行核心应用的负载属于 “Read Heavy”,查询语句占比 66%。本次应用涵盖 4 支混合交易。TPS 从 1 到 30
这个结果在合作伙伴的实验室跑起来之后,业务的 TPS 只有 1 左右,远低于预期。业务端会有超时的报错 (Coprocessor task terminated due to exceeding the deadline)。通常这种情况都是执行计划不优化造成的,比如说缺少索引,导致需要全表扫描。从 TiDB 的 Dashboard 上面会看到数据库的 QPS 只有 100 左右,80、90-in-txn 的延迟超过一分钟,再看 Top SQL,可以看到有 Top SQL 因为缺失索引在走全表扫描的。TPS 从 30 到 320
接着为了提高资源利用率,我们检查了一下集群的拓扑。测试环境是六台 ARM 服务器,每台16 个 Numa,每个 Numa 是 8C 16GB。现有的拓扑部署了 3 个 TiDB + 3 个 TiKV。TiDB 是绑定到 0~4 的 Numa 上面,没有充分利用整个机器的能力。我们对这个组网的方式做了调整,部署了 36 个 TiDB + 6 个 TiKV,每个 TiDB 会绑两个 Numa ,每个 TiKV 有四个 Numa 。做了这个组网方式的修改之后,TPS 上升到了 320。TPS 从 320 到 600
在 TPS 320 的压力下观察到一个现象是,数据库的 CPU 利用率比较低,每个 TiDB 虽然绑定了两个 Numa ,有 16 核的 CPU,但是 CPU 使用在 100% - 520%,用了 1-5 个逻辑 CPU 左右。同时,应用服务器的 CPU 使用率不到 10%。query 80th 延迟是 3.84 毫秒。这是一种非常典型的情况,看起来数据库的压力不大,应用服务器的 CPU 利用率很小,但是总体的 TPS 上不去。目前硬件资源肯定是充足的,我们不确定整个系统的瓶颈在哪里。根据之前讲到的用户响应时间跟数据库时间的比例关系:整个系统的火焰图
mpstat -P ALL 5 命令输出
另外,对于没有绑核的程序 —— PD 和 HAProxy,我们在火焰图里面观察到关于内存的访问或者内存的加锁等系统调用占比非常高。对于开启 Numa 的系统,其实 CPU 访问内存的速度是不平等的。通常访问远端 Numa 的内存延迟是访问本地 Numa 内存的十倍。硬件厂商也推荐应用最好不要进行跨 Numa 部署,因为在 ARM 服务器进行跨 Numa 的内存访问,延迟会更高,极大的影响程序执行性能。PD-Server 进程 perf top 命令输出
基于上面的分析,我们进行了组网方式的调整。对于六台机器,1)第一个 Numa 都空出来专门处理网络软中断,不跑任何的程序;2)所有的程序都需要绑核,每个 TiDB 只绑一个 Numa,TiDB 的数据翻倍, PD 和 HAProxy 也进行绑核。做了这个调整之后,应用的 TPS 上升到 600。Connection Idle duration 的 80-in-txn 延迟就从 26 毫秒下降到 5 毫秒。TPS 从 600 到 880
Load Runner
TPS 抖动解决
TPS 880 时应用出现明显的波动,事务处理延迟出现巨大的波动。从 Dashboard 中可以看到同样的 QPS 波动,P999 延迟在同样的时间出现小的尖刺。数据库是造成应用性能波动的原因吗?Duration P9999
Grafana TiKV-Detail 面板观察到 OOM 重启
TiKV.log 日志显示 OOM
SQL 执行计划稳定性 - 永不准确的统计信息
在某一次压测的过程中,应用 TPS 掉为 0,从 TiDB Dashboard 我们发现出现一条 Top SQL。这个 sql 执行计划发现了变化,出现了两个执行计划。MQ_PRODUCER_MSG 是一个消息队列表,query 包含 flow_id 和 status 两个过滤条件,flow_id 和 status 上面都有单列的索引。常的执行计划是走 flow_id 的上面的索引,平均执行时间是 62 毫秒。出问题的时候,优化器选择 status 列索引,执行时间是 38 秒。在错误的执行计划中,对于条件 status=1,优化器估算为 0 行,所以选择 了 status 列上面的索引。我们尝试重现,对 status =1 的条件做一个 explain analyze,估算值是四万多,并没有出现估算等于 0 情况。
接着分析慢日志,63 个 TiDB 实例都出现这个错误的执行计划,一共有 94 个连接执行了错误的执行计划,也就是每个 TiDB 实例有一个或者两个连接执行过这个错误的执行计划。
select instance, count(*) from information_schema.cluster_slow_query where index_names like '%MQ_STATUS_INDEX%' group by instance;
select conn_id,instance, count(*) from information_schema.cluster_slow_query where index_names like '%MQ_STATUS_INDEX%' and digest = 'cca85ee01e54b3b37775c8b07c2808f306177d28fd0376b2d8c5dd5663f488ec' group by instance,conn_id;
基于以上的分析我们怀疑错误的估算跟 TiDB 异步加载统计信息的行为相关。统计信息 Lazy Load 的 feature 是对于列上详细的统计信息,比如 (histogram/cm_sketch 等),只有等到第一次被用到之后,后台任务才会异步加载的。为了验证,我们重启一个 TiDB 实例,然后对 status=1 进行 explain analyze,依然没有重现 status=1 估算为 0 的情况。
通过 stats_histograms.update_time 检查上一次统计信息更新时间可以确认跑负载之前表上的统计信息刚好被自动更新过 (注意:stats_meta.update_time 不代表上一次统计信息更新时间)。然而统计信息还是不准确,这是为什么呢?
通过偶然的机会我们发现,status=1 情况只存在于跑负载过程中。负载跑完以后,表里面没有status=1 的数据。所以自动收集统计信息时,因为上一轮的负载已结束,status=1 的数据已经被处理完了,表里没有 status=1 的数据,所以 status=1 的估计值为 0,status 列唯一值 (NDV, number of distinct values)只有 1。而正确的统计信息里,NDV 为 2。
左边为错误的的统计信息,右边为正确的统计信息
对于业务中消息中间表,数据是频繁变动的,统计信息是否具有代表性,取决于统计信息更新时,数据的状态。针对这种情况,TiDB 优化器需要支持手工锁定统计信息,避免 auto analyze 任务在错误的时间点搜集了非典型统计信息。在现有版本,需要通过 SQL Binding 手工绑定执行计划,确保正确的执行计划被选择。TPS 880 到 1200+
数据库优化之后,应用的 TPS 跟应用 jvm 的个数成正比。最终,使用一台 ARM 服务器,同样是 16 个 Numa,部署15个应用,每个应用 jvm 绑定一个 Numa,连接到 TiDB 集群。最优应用并发在 1200 左右,最大应用 TPS 为 1250 左右。应用服务器和数据库服务器 CPU 资源利用率在 70% 左右。优化总结
这个案例里面我们学到了什么?
TiDB 性能和稳定性的挑战
对于银行核心交易应用是 read heavy 负载,一个交易包含上百条小查询,如何保持高性能和稳定性是一个巨大的挑战。
分析 TiDB 的火焰图,CompilePreparedStatements 占了 18% 的 TiDB CPU,按照 alloc_objects 排序,TiDB 内存申请操作大约36% 来源于 CompilePreparedStatements 中的planner.Oplimzer。为什么开启了执行计划缓存(prepared plan cache),优化器还需要对于 prepared statements 进行解析和执行计划生成的操作,消耗大量的内存和 CPU?
通过 grafana 监控,可以确认 prepared plan cache 命中率为 72.7%, 27.3% 的 prepared statement 没有命中 plan cache 的 sql,会重复解析生成执行计划。因为这次测试使用了v5.1.1 版本,prepared-plan-cache 还是实验特性,部分 sql 语句还不支持缓存执行计划。
Queries Using Plan Cache OPS = 33.3k
StmtExecute = 45.8k
prepared plan cach 命中率 = 33.3/46.8 = 72.7%
在近期新版本 v5.3.0 中,prepared plan cache 这个 feature 已经正式 GA,解决了之前部分语句的执行计划无法缓存的问题,消除了重复解析 SQL、 生成执行计划带来的 CPU 和内存的消耗。正如对于运行在 Oracle 上的 OLTP 应用,使用绑定变量和软解析可以使性能得到数量级别的提升,随着 prepared plan cache 特性的 GA,TiDB 在银行核心负载中,性能和稳定性方面将有显著的提升。另外,应用使用 prepared statement 接口,还可以有效防止 SQL 注入攻击,提高整个系统的安全性。