一、Profiling局限性
在MySQL性能诊断领域,Profiling工具曾因简单易用成为开发者排查SQL耗时的常用选择,但随着MySQL版本迭代(5.7+起官方明确标记为弃用),其功能局限和性能损耗问题逐渐凸显。而Performance Schema作为MySQL原生的高性能监控框架,凭借低开销、多维度、细粒度的优势,已成为替代Profiling的最佳方案。
在深入Performance Schema之前,我们需先明确Profiling工具的核心缺陷——这也是选择替代方案的根本原因。
1.1 工作原理与痛点
Profiling通过会话级别的开关(SET profiling = 1)记录SQL执行过程中各阶段的耗时,本质是对SQL执行流程的“轻量快照”,但存在以下无法忽视的问题:
- 功能单一:仅能记录当前会话的
SQL耗时,无法监控全局、其他线程或历史SQL的性能数据; - 精度不足:时间单位仅支持到秒(部分版本支持毫秒),无法捕捉微秒级的性能瓶颈(如索引查找、锁等待);
- 性能损耗:开启后会对当前会话的
SQL执行速度产生约5%-10%的额外开销,高并发场景下可能加剧性能问题; - 数据易失:会话结束后所有
profiling数据自动清空,无法用于事后分析或长期性能趋势跟踪; - 官方弃用:
MySQL 5.7.22起将其标记为DEPRECATED,8.0版本中虽暂未移除,但已不再维护,未来存在被删除的风险。
1.2 与Performance Schema对比
为更直观体现替代价值,我们通过表格对比两者关键特性:
| 对比维度 | Profiling 工具 | Performance Schema |
|---|---|---|
| 监控范围 | 仅当前会话 | 全局、线程、用户、表、存储引擎多维度 |
| 时间精度 | 秒/毫秒级 | 微秒级(timer_wait字段支持1e-9秒精度) |
| 性能开销 | 会话级5%-10%额外损耗 | 极低(默认<0.5%,通过“采样+按需启用”控制) |
| 数据持久化 | 会话结束即清空 | 数据保存在内存中,支持长期查询(需配置保留策略) |
| 监控内容 | 仅SQL执行阶段耗时 | 包含SQL执行、锁等待、IO操作、内存使用、存储过程调用等20+类指标 |
| 官方支持 | 5.7+弃用,无维护 | 官方主推,持续迭代增强(8.0版本新增大量监控项) |
| 适用场景 | 临时排查单会话简单SQL问题 | 生产环境全局性能监控、瓶颈定位、长期优化分析 |
二、Performance Schema核心概念
要熟练使用Performance Schema,需先理解其底层架构——它本质是一套基于“事件驱动”的监控框架,通过预定义的“仪器(Instruments)”收集MySQL内部各类事件,再通过“消费者(Consumers)”将事件数据输出到用户可查询的表中。
2.1 核心组件解析
2.1.1 Instruments
Instruments(仪器)是Performance Schema的核心采集单元,对应MySQL内部的各类操作(如SQL解析、索引扫描、磁盘IO、锁等待等),每个Instrument都有唯一的名称(如statement/sql/select对应SELECT语句执行),并支持按需启用/禁用,以控制监控开销。
常见的Instruments分类:
- statement类:监控SQL语句执行(如
statement/sql/select、statement/sql/update); - stage类:监控SQL执行的细分阶段(如
stage/sql/Sending data、stage/sql/Creating sort index); - wait类:监控各类等待事件(如
wait/lock/table/sql/lock表锁等待、wait/io/file/innodb/innodb_data_file磁盘IO等待); - memory类:监控内存使用(如
memory/innodb/buffer_pool缓冲池内存分配)。
2.1.2 Consumers
Consumers负责将Instruments采集到的事件数据,整理并存储到对应的表中,供用户通过SQL查询。常用的Consumers表分类:
- 事件历史表:记录已发生的事件(如
events_statements_history记录当前线程的SQL执行历史,events_stages_history_long记录全局所有线程的SQL阶段耗时); - 汇总表:对事件数据按维度聚合(如
events_statements_summary_by_digest按SQL指纹汇总执行次数、平均耗时,events_stages_summary_by_thread_by_event_name按线程和事件类型汇总耗时); - 配置表:控制
Instruments和Consumers的启用状态(如setup_instruments配置仪器是否启用,setup_consumers配置消费者是否启用)。
2.2 配置监控开关
Performance Schema的所有配置都通过系统表修改,无需重启MySQL(配置仅在当前实例生效,重启后需重新配置或持久化到my.cnf)。核心配置表如下:
| 配置表名 | 作用 | 关键字段示例 |
|---|---|---|
setup_instruments |
启用/禁用指定Instrument |
NAME(Instrument 名称)、ENABLED(是否启用:YES/NO)、TIMED(是否记录时间:YES/NO) |
setup_consumers |
启用/禁用指定Consumer |
NAME(Consumer 名称)、ENABLED(是否启用:YES/NO) |
setup_actors |
控制对哪些用户/主机的事件进行监控 | HOST(主机名)、USER(用户名)、ROLE(角色) |
performance_timers |
配置时间戳精度(如使用CPU周期、微秒等) |
TIMER_NAME(时间类型)、TIMER_FACTOR(转换系数) |
三、监控SQL性能
掌握基础概念后,我们进入核心实操环节——从配置启用,到查询SQL执行各阶段耗时,再到定位性能瓶颈,全程覆盖生产环境常用场景。
3.1 基础配置
默认情况下,MySQL仅启用部分Instruments和Consumers,需手动配置以满足SQL性能监控需求。以下是针对“SQL执行阶段耗时监控”的基础配置:
3.1.1 启用Instrument
启用statement和stage类Instrument(采集SQL执行事件)
-- 启用所有sql语句执行监控(statement类)
update performance_schema.setup_instruments
set enabled = 'yes', timed = 'yes'
where name like 'statement/sql/%';-- 启用sql执行各阶段监控(stage类)
update performance_schema.setup_instruments
set enabled = 'yes', timed = 'yes'
where name like 'stage/sql/%';
3.1.2 启用Consumers
启用对应的Consumers(输出事件数据到表)
-- 启用sql语句历史记录(当前线程)
update performance_schema.setup_consumers
set enabled = 'yes'
where name = 'events_statements_history';-- 启用sql语句全局历史记录(所有线程,用于事后分析)
update performance_schema.setup_consumers
set enabled = 'yes'
where name = 'events_statements_history_long';-- 启用sql阶段历史记录(当前线程)
update performance_schema.setup_consumers
set enabled = 'yes'
where name = 'events_stages_history';-- 启用sql阶段全局历史记录(所有线程)
update performance_schema.setup_consumers
set enabled = 'yes'
where name = 'events_stages_history_long';
3.1.3 持久化配置
上述配置仅在当前实例生效,若需重启后保留,需在my.cnf(Linux)或my.ini(Windows)中添加以下内容:
[mysqld]
# 启用Performance Schema(默认已启用,无需修改)
performance_schema = ON# 启用statement和stage类Instrument
performance-schema-instrument='statement/sql/%=ON'
performance-schema-instrument='stage/sql/%=ON'# 启用对应的Consumers
performance-schema-consumer-events_statements_history=ON
performance-schema-consumer-events_statements_history_long=ON
performance-schema-consumer-events_stages_history=ON
performance-schema-consumer-events_stages_history_long=ON
3.2 查询SQL执行总耗时
Profiling工具中用SHOW PROFILES查看当前会话所有SQL的总耗时,Performance Schema可通过events_statements_history(当前线程)或events_statements_history_long(全局)表实现,且支持更丰富的筛选条件。
3.2.1 查看当前线程的SQL执行历史
-- 查看当前线程最近执行的10条SQL,包含执行时间、SQL内容、返回行数
selectevent_id as 事件id,event_name as sql类型, -- 如statement/sql/selecttimer_start as 开始时间戳,timer_end as 结束时间戳,-- 计算执行耗时(单位:毫秒,timer_wait默认单位为皮秒,需转换:1毫秒=1e9皮秒)round(timer_wait / 1e9, 3) as 执行耗时_毫秒,sql_text as sql语句,rows_affected as 影响行数,rows_sent as 返回行数
from performance_schema.events_statements_history
order by timer_start desc
limit 10;
3.2.2 查看全局所有线程的慢SQL
查询耗时超过500毫秒的SQL
-- 全局慢SQL查询(需启用events_statements_history_long)
selectt.processlist_id as 线程id,t.processlist_user as 执行用户,t.processlist_host as 客户端主机,e.event_id as 事件id,e.event_name as sql类型,round(e.timer_wait / 1e9, 3) as 执行耗时_毫秒,e.sql_text as sql语句,e.rows_sent as 返回行数,e.created_tmp_tables as 创建临时表数量, -- 辅助判断是否有性能问题e.sort_rows as 排序行数 -- 辅助判断排序开销
from performance_schema.events_statements_history_long e
join performance_schema.threads ton e.thread_id = t.thread_id
wheree.timer_wait > 500 * 1e9 -- 筛选耗时超过500毫秒的sql(1毫秒=1e9皮秒)and e.sql_text is not null -- 排除空sql
order by e.timer_wait desc;
3.3 查询SQL执行各阶段耗时
Profiling工具中用SHOW PROFILE CPU FOR QUERY 1查看指定SQL的阶段耗时,Performance Schema通过events_stages_history表(按事件ID关联)可实现更细粒度的阶段分析,且支持微秒级精度。
3.3.1 关联查询指定SQL的各阶段耗时
以“查询当前线程中EVENT_ID=101的SQL”为例,步骤如下:
-- 步骤1:先找到目标sql的event_id(可通过3.2中的sql查询)
-- 步骤2:关联events_stages_history,查看该sql的各阶段耗时
selects.event_id as 阶段事件id,s.event_name as 执行阶段, -- 如stage/sql/sending data-- 计算阶段耗时(单位:微秒,1微秒=1e6皮秒)round(s.timer_wait / 1e6, 2) as 阶段耗时_微秒,-- 计算该阶段耗时占总sql耗时的比例round(s.timer_wait / (select timer_wait from performance_schema.events_statements_history where event_id = 101) * 100, 2) as 耗时占比_百分比
from performance_schema.events_stages_history s
wheres.nesting_event_id = 101 -- nesting_event_id关联sql的event_idand s.event_name like 'stage/sql/%' -- 仅显示sql执行阶段
order by s.timer_start;
3.3.2 常见SQL执行阶段解析
通过上述查询,我们会得到类似如下的结果,不同阶段的耗时异常对应不同的性能问题:
| 执行阶段(EVENT_NAME) | 阶段含义 | 耗时过高的可能原因 | 优化建议 |
|---|---|---|---|
stage/sql/starting |
SQL 语句初始化阶段 | 通常耗时极短,若异常可能是线程资源不足 | 检查数据库连接数、线程池配置 |
stage/sql/Checking permissions |
权限校验阶段 | 若频繁耗时高,可能是权限表(mysql.user)无索引 | 确保 mysql.user 表的 Host、User 字段有索引 |
stage/sql/Opening tables |
打开表阶段 | 耗时高可能是表缓存不足或表锁等待 | 增大table_open_cache,排查表锁竞争 |
stage/sql/Init |
初始化查询计划阶段 | 耗时高可能是 SQL 语法复杂或统计信息过时 | 更新表统计信息(ANALYZE TABLE) |
stage/sql/System lock |
系统锁等待阶段 | 通常因 MyISAM 表的表锁竞争(写阻塞读 / 读阻塞写) | 迁移到 InnoDB 引擎,使用行锁 |
stage/sql/Optimizing |
优化器生成执行计划阶段 | 耗时高可能是 SQL 复杂(如多表 JOIN)或统计信息不准 | 简化 SQL,更新统计信息 |
stage/sql/Executing |
执行查询阶段 | 耗时高可能是索引失效或全表扫描 | 检查执行计划(EXPLAIN),优化索引 |
stage/sql/Sending data |
数据读取并返回客户端阶段(最常见瓶颈) | 全表扫描、索引失效、返回数据量过大 | 优化索引,减少返回字段(避免 SELECT *),分页查询 |
stage/sql/Creating sort index |
创建排序索引阶段(ORDER BY/GROUP BY) | 内存排序溢出到磁盘(sort_buffer 不足) | 增大sort_buffer_size,优化排序字段索引 |
stage/sql/End |
SQL 执行结束阶段 | 通常耗时极短,异常需排查服务器资源 | 检查 CPU、内存使用率 |
3.4 进阶分析
Performance Schema的优势不仅在于SQL阶段耗时,还能关联锁等待、IO操作等数据,定位Profiling无法覆盖的深层问题(如隐性锁等待、磁盘IO瓶颈)。
3.4.1 查询指定SQL的锁等待情况
-- 查看EVENT_ID=101的SQL在执行过程中发生的锁等待
selectw.event_id as 锁等待事件id,w.event_name as 锁类型, -- 如wait/lock/table/sql/lock(表锁)、wait/lock/innodb/row_lock(行锁)w.timer_wait / 1e6 as 锁等待耗时_微秒,-- 解析锁相关信息(如锁表名、锁模式)w.object_name as 被锁对象名, -- 表名或行idw.lock_type as 锁级别, -- table(表锁)、row(行锁)w.lock_mode as 锁模式, -- 如s(共享锁)、x(排他锁)、is(意向共享锁)w.lock_status as 锁状态, -- granted(已获得)、waiting(等待中)t.processlist_id as 持有/等待锁的线程id,t.processlist_user as 执行用户
from performance_schema.events_waits_history w
join performance_schema.threads ton w.thread_id = t.thread_id
wherew.nesting_event_id = 101 -- 关联目标sql的event_idand w.event_name like 'wait/lock/%' -- 仅显示锁等待事件
order by w.timer_start;
3.4.2 查询指定SQL的磁盘IO情况
-- 查看EVENT_ID=101的SQL在执行过程中产生的磁盘IO
selectio.event_id as io事件id,io.event_name as io类型, -- 如wait/io/file/innodb/innodb_data_file(innodb数据文件io)io.timer_wait / 1e6 as io耗时_微秒,io.object_name as io文件名, -- 如ibdata1、test.ibdio.operation as io操作类型, -- read(读)、write(写)、flush(刷新)io.number_of_bytes as io数据量_字节, -- 读写的字节数round(io.number_of_bytes / 1024, 2) as io数据量_kb
from performance_schema.events_waits_history io
whereio.nesting_event_id = 101 -- 关联目标sql的event_idand io.event_name like 'wait/io/file/%' -- 仅显示文件io事件
order by io.timer_start;
四、实战场景
理论结合实践才是掌握工具的关键,以下通过两个生产环境常见场景,演示如何用Performance Schema定位并解决问题。
4.1 场景1:排查“慢查询”背后的隐性锁等待
4.1.1 问题描述
某业务系统中,一条简单的update语句(update order set status=1 where id=100)执行耗时高达3秒,EXPLAIN显示使用了id主键索引,无全表扫描,但Profiling仅显示“System lock”阶段耗时2.8秒,无法进一步定位原因。
4.1.2 排查步骤
- 找到目标SQL的EVENT_ID:
select event_id, sql_text, round(timer_wait/1e9,3) as 耗时_毫秒
from performance_schema.events_statements_history
where sql_text like 'update order set status=1 where id=100%'
order by timer_start desc
limit 1;
-- 假设返回event_id=205
- 查询该SQL的锁等待情况:
selectw.event_name as 锁类型,w.timer_wait/1e6 as 锁等待耗时_微秒,w.object_name as 被锁表,w.lock_mode as 锁模式,w.lock_status as 锁状态,t.processlist_id as 等待锁的线程id,t2.processlist_id as 持有锁的线程id
from performance_schema.events_waits_history w
join performance_schema.threads ton w.thread_id = t.thread_id
-- 关联持有锁的线程(通过object_name和lock_mode匹配)
left join performance_schema.threads t2on t2.thread_id in (select thread_idfrom performance_schema.events_waits_historywhere object_name = w.object_nameand lock_mode = w.lock_modeand lock_status = 'granted')
wherew.nesting_event_id = 205and w.lock_status = 'waiting';
- 结果分析:
查询发现,该UPDATE语句在“wait/lock/innodb/row_lock”阶段等待了2.8秒,持有锁的线程ID为123。进一步查询线程123的SQL:
select processlist_id, processlist_info as 正在执行的sql
from performance_schema.threads
where processlist_id = 123;
发现线程 123 正在执行一条长时间未提交的事务(BEGIN; SELECT * FROM order WHERE id=100 FOR UPDATE;),持有id=100行的排他锁,导致后续UPDATE语句等待。
- 解决方案:
- 终止长时间未提交的事务(
KILL 123); - 在业务代码中添加事务超时机制(如
SET innodb_lock_wait_timeout = 5,避免无限等待); - 优化长事务逻辑,减少事务持有锁的时间。
4.2 场景2:定位“高并发统计查询”的IO瓶颈
4.2.1 问题描述
某电商平台的“商品评论数统计”接口(SELECT COUNT(*) FROM comments WHERE product_id=999)在高峰期耗时高达1.5秒,使用了(product_id, create_time)联合索引,但Profiling仅显示“Sending data”阶段耗时1.4秒,无法判断是索引问题还是IO问题。
4.2.2 排查步骤
- 查询该SQL的阶段耗时:
selects.event_name as 执行阶段,round(s.timer_wait/1e6,2) as 耗时_微秒,round(s.timer_wait/(select timer_wait from events_statements_history where event_id=310)*100,2) as 占比_百分比
from performance_schema.events_stages_history s
where s.nesting_event_id=310 -- 目标sql的event_id
order by s.timer_start;
结果显示“stage/sql/Sending data”阶段耗时1420000微秒(1.42秒),占总耗时的95%。
- 查询该SQL的磁盘IO情况:
selectio.event_name as io类型,io.timer_wait/1e6 as io耗时_微秒,io.object_name as 数据文件名,io.operation as io操作,io.number_of_bytes/1024 as io大小_kb
from performance_schema.events_waits_history io
whereio.nesting_event_id=310and io.event_name like 'wait/io/file/innodb/%';
结果显示,该SQL读取了comments.ibd文件(InnoDB表数据文件)的128个数据页(每个页16KB,共2048KB),IO总耗时1380000微秒(1.38秒),占“Sending data”阶段耗时的97%——说明瓶颈是磁盘IO(索引数据未完全加载到缓冲池)。
- 解决方案:
- 增大
innodb_buffer_pool_size(如从2GB调整为4GB),让更多索引数据常驻内存; - 采用“汇总表 + 定时更新”方案(如本文原博文提到的
comment_summary表),将实时COUNT(*)查询转为对汇总表的单行查询,彻底避免磁盘IO; - 对
comments表按product_id进行分表,减少单表数据量,降低IO开销。
五、最佳实践与性能优化
虽然Performance Schema开销极低,但在高并发生产环境中,仍需通过合理配置控制资源占用,避免“监控工具本身成为性能瓶颈”。
5.1 性能优化建议
- 按需启用Instrument:仅启用需要监控的
Instrument(如仅监控statement和stage类,禁用memory和wait类),减少采集开销;
-- 仅启用sql语句和阶段监控,禁用其他instrument
update performance_schema.setup_instruments
set enabled = 'no'
where name not like 'statement/sql/%'and name not like 'stage/sql/%';
- 控制历史数据保留量:通过
max_history参数限制历史表的记录数(如events_statements_history_long默认保留10000条,可根据需求调整);
[mysqld]
# 限制全局sql历史记录最多保留5000条
performance-schema-events-statements-history-long-size=5000# 限制当前线程sql历史记录最多保留100条
performance-schema-events-statements-history-size=100
- 避免频繁查询大表:
events_statements_history_long等全局表可能包含大量数据,查询时需添加WHERE条件(如时间范围、SQL类型),避免全表扫描; - 定期清理历史数据:若需长期监控,可定时(如每天凌晨)清理过期数据,避免内存占用过高;
-- 清理events_statements_history_long中1小时前的记录
delete from performance_schema.events_statements_history_long
where timer_start < (unix_timestamp(now() - interval 1 hour) * 1e12);-- 注:timer_start单位为皮秒,unix_timestamp返回秒级时间戳,需转换为皮秒(1秒=1e12皮秒)
5.2 常见误区避坑
- 误区1:启用所有Instrument:认为“监控越全面越好”,但实际上启用
memory、mutex等细粒度Instrument会增加约1%-2%的CPU开销,非必要不启用; - 误区2:忽视数据时效性:
Performance Schema数据保存在内存中,重启后清空,需结合监控系统(如Prometheus+Grafana)进行长期数据采集; - 误区3:依赖单一指标:仅看
SQL总耗时,不结合阶段耗时、锁等待、IO数据,可能遗漏深层问题(如隐性锁等待导致的慢查询); - 误区4:8.0版本无需配置:
MySQL8.0默认启用了更多Instrument,但仍需根据业务需求调整(如默认禁用events_stages_history_long,需手动启用)。
六、总结
Performance Schema不仅是Profiling工具的“替代品”,更是MySQL性能监控的“升级方案”——它解决了Profiling的功能局限和性能损耗问题,提供了从SQL执行、锁等待、IO操作到内存使用的全维度监控能力。
对于开发者和DBA而言,迁移到Performance Schema的核心价值在于:
- 更精准的瓶颈定位:微秒级精度+多维度数据,可定位
Profiling无法覆盖的隐性问题(如锁等待、IO瓶颈); - 更低的监控开销:按需启用机制+内存级数据存储,生产环境可长期启用而不影响业务性能;
- 更广泛的适用场景:支持全局监控、历史分析、高并发场景,满足从开发调试到生产运维的全流程需求;
- 官方长期支持:作为MySQL官方主推的监控框架,持续迭代增强(如8.0版本新增
statement_digest、lock_time等实用字段),无版本兼容风险。
未来,随着MySQL8.0的普及和Profiling工具的彻底移除,Performance Schema将成为MySQL性能诊断的“标准工具”。掌握它,不仅能提升SQL优化效率,更能为生产环境的稳定性提供关键保障。