Performance Schema (PFS)是MySQL 5.5引进的新特性,在MySQL5.6对这个特性的结构进行了大幅度的调整,从MySQL5.7至8.0,这个特性一直在持续改善。这是 MySQL 内置的诊断引擎,通过轻量级代码插桩(Instrumentation)实时采集数据库内部操作数据,为性能优化提供原子级观测能力。与传统慢查询日志不同,PFS 直接挂钩数据库内核,可追踪锁竞争、文件 I/O、内存分配等底层行为。
对于Performance Schema的使用,很多人持谨慎甚至是怀疑的态度,这主要是出于对其造成性能方面的消耗的疑虑。本文从第三方的测试报告、PFS的原理方面展示PFS对性能的影响,以及如何通过各种手段降低PFS对性能的消耗,使其在容忍的范围之内,使我们在不影响数据库的性能的情况下能享受PFS给我们带来的性能分析和诊断方面的收益。
1 从第三方的测试报告来看
oracle官方、Percona、AWS 都发布关于PFS对数据库性能影响的白皮书、测试报告或者生产基准,下面这个报告是在AWS上的测试数据,测试环境如下:

性能测试数据(256并发线程)见下面的表格:

上面的测试数据在基准情况下CPU利用率达到了78%,这是一个比较高的数值,大部分生产数据库CPU利用率经常在60%左右,平均延迟也不低,可以说是一个接近性能瓶颈的情况。在这种情况下基础监控的性能消耗在3%左右是一个可以接受的结果,开启全监控的性能消耗还是比较大的,达到14%,这个大概是无法接受的了。至于内存监控后系统消耗达到24%,DeepSeek从而得出了内存监控是高位项,是绝对不能开的。这应该是个靠不住的结论,如调整一下顺序,在基础监控后测试内存监控,可能会是另一个结果了。
2 默认情况下的性能消耗(MySQL 8)
PFS在MySQL 8 默认是打开的,事件监控则不是全开,打开的事件监控数量如下:
select
SUBSTRING_INDEX(name, '/', 1) ,
count(*),
sum(case ENABLED when 'YES' then 1 else 0 end) enabled_count
from
performance_schema.setup_instruments
group by
SUBSTRING_INDEX(name, '/', 1);
-----------------------------+--------+-------------+
SUBSTRING_INDEX(name, '/', 1)|count(*)|enabled_count|
-----------------------------+--------+-------------+
wait | 382| 53|
idle | 1| 1|
stage | 124| 16|
statement | 212| 212|
transaction | 1| 1|
memory | 498| 498|
error | 1| 1|
-----------------------------+--------+-------------+
这个配置下,PFS造成的性能损耗大概在2.5%到5%左右,所以有些小伙伴安装MySQL 8采用的默认配置也不用紧张,这个性能损耗比起PFS的收益来看,完全是值得的,Oracle的awr有没有性能损耗?理论上和实际上都有,没必要仅仅对MySQL PFS的性能损耗锱铢必较。
3 PFS性能开销分析
3.1 从原理分析
PFS的原理是在 MySQL 服务器代码的关键执行路径上插入轻量级的“检测点”(Instruments),通过这些检测点收集执行过程中的各种性能数据,并将这些数据存储在内存表中供用户查询分析。PFS对数据库的影响主要体现在一下三个方面
- 指令级开销 (CPU Cycles):在每个被插桩的代码路径上,P_S 添加了额外的函数调用(检测点代码)。这些代码要检查该检测点是否启用 (if (instrument_enabled) {…}),如果启用,执行记录操作:获取高精度时间戳 (clock_gettime()),更新计数器,将事件数据写入预分配的缓冲区。这些操作本身是轻量级的,但累积起来就是主要的 CPU 开销来源。
- 内存开销: PFS使用内存表存储数据。主要内存消耗在事件缓冲区内,也就是events_waits_current, events_history, events_history_long 等消费者表(消费者表实质上是预分配内存的环形缓冲区或链表。history_long 表通常占用最多内存)summary 等聚合汇总表(在内存中维护各种维度的聚合统计(哈希表结构),内部数据结构(维护检测点状态、线程状态、对象信息等)需要的少量内存。
- 并发与锁开销:线程本地存储 (TLS)( P_S 大量使用 TLS 来存储每个线程的当前事件状态 (events_*_current),这避免了线程间竞争,是高效的关键设计),缓冲区写入(当事件结束时,需要将记录从线程本地状态转移到历史缓冲区 或更新汇总表 。这些操作通常需互斥锁保护共享数据结构),汇总表更新( 对 summary 表的更新也需要锁保护)。
以上三种开销,比较负载和可以量化的CPU开销,用一个具体的例子(MUTEX)详细说明一下,MUTEX 插桩(instrument)由宏定义的,在文件mysql/psi/mysql_thread.h中(基于MySQL 8.0源码),代码如下:
#ifdef HAVE_PSI_MUTEX_INTERFACE
#define mysql_mutex_lock(M, F, L) \
inline_mysql_mutex_lock(M, F, L)
#else
#define mysql_mutex_lock(M, F, L) \
( (M)->lock() )
#endif
inline_mysql_mutex_lock函数的实现如下
static inline void
inline_mysql_mutex_lock(mysql_mutex_t *mutex, const char *src_file, uint src_line)
{
PSI_mutex_locker *locker = nullptr;
PSI_mutex_locker_state state;
if (PSI_MUTEX_CALL(start_mutex_wait)(&state, mutex->m_psi, PSI_MUTEX_LOCK, src_file, src_line)) {
locker = &state;
}
mutex->lock(); // 实际调用互斥锁的lock方法
if (locker != nullptr) {
PSI_MUTEX_CALL(end_mutex_wait)(locker, 0);
}
}
上面这段代码是插桩的核心:
- 首先,它声明了一个
PSI_mutex_locker指针和一个状态变量state。 - 然后,它调用
PSI_MUTEX_CALL(start_mutex_wait)开始记录等待事件。这个宏会展开为Performance Schema的调用,如果当前启用了对互斥锁的监控,则返回一个非空的locker(指向状态变量的指针),否则返回0(此时locker保持为nullptr)。 - 接着,执行实际的锁获取操作:
mutex->lock()。 - 最后,如果
locker非空(表示正在记录事件),则调用PSI_MUTEX_CALL(end_mutex_wait)结束事件记录。
如果没有插桩,每次锁操作只执行锁本身的指令(这本身也有开销,比如系统调用futex或原子操作)。如果有插桩,每次锁操作额外增加:
- 一个条件判断(检查是否启用插桩)。
- 两个函数调用(start和end)。
- 在start和end函数内部:两次时间戳获取(约20-100个周期,取决于系统),事件结构的初始化(几个内存写入操作)。
保守估计假设每次插桩额外消耗100个CPU周期,那么1000次锁操作将增加100,000个周期。在2GHz的CPU上,一个周期是0.5纳秒,所以总增加时间为50微秒。对于一个原本需要1毫秒的查询,这增加了5%的开销(50微秒/1000微秒)。如果锁操作更多(比如10,000次),开销就会达到50%。
3.2 记录事件时间的开销
插桩的开销大部分是记录事件的时间的开销,选择不同的时间单位,开销也有很大差异,不同的版本也有所不同
mysql> select version();--数据库版本
+-----------+
| version() |
+-----------+
| 8.4.5 |
+-----------+
1 row in set (0.00 sec)
---支持的timer
mysql> SELECT * FROM performance_schema.performance_timers;
+-------------+-----------------+------------------+----------------+
| TIMER_NAME | TIMER_FREQUENCY | TIMER_RESOLUTION | TIMER_OVERHEAD |
+-------------+-----------------+------------------+----------------+
| CYCLE | 1796616915 | 1 | 13 |
| NANOSECOND | 1000000000 | 1 | 27 |
| MICROSECOND | 1000000 | 1 | 25 |
| MILLISECOND | 1036 | 1 | 25 |
| THREAD_CPU | 358391564 | 1 | 1510 |
+-------------+-----------------+------------------+----------------+
其中TIMER_OVERHEAD是该timer下获取一次时间最少的CPU周期(cycle),TIMER_FREQUENCY是每秒的时间单元,名为cycle的timer的时间单元和cpu的主频有关,一个事件需要在开始和结束时共获取两次时间,因此,对每个事件,这个开销要乘2。
4 使用事件过滤避免不必要的性能开销
PFS的事件过滤有两种:Pre-filering和Post-filering。Pre-filering通过调整PFS的配置只收集需要的事件,只更新需要的消费者表。Post-filering是指用where语句查询消费者表。Post-filering影响的是查询的内容,不影响PFS本身的性能开销。使用Pre-filering却对PFS的性能开销造成显著的影响。
4.1 instrument过滤–只激活关注的instrument
更新setup_instrument表可以激活或者关闭特定的instrument,更新立即生效,比如要关闭所有的wait类事件
mysql> update setup_instruments set enabled='NO' where name like 'wait%';
Query OK, 54 rows affected (0.00 sec)
Rows matched: 402 Changed: 54 Warnings: 0
4.2 按账号过滤
更新setup_actors表可以按照用户来选择instrument和consumer,这个表缺省情况下最大支持100行,如果超过这个值,需要设置performance_schema_setup_actors_size,更改这个参数的值需要重启数据库。
mysql> select * from setup_actors;
+------+------+------+---------+---------+
| HOST | USER | ROLE | ENABLED | HISTORY |
+------+------+------+---------+---------+
| % | % | % | YES | YES |
+------+------+------+---------+---------+
1 row in set (0.00 sec)
默认情况下,PFS不对用户进行过滤,如果只需要对某些用户的进行监控,可以删除默认的条目,在这个表内插入相关条目。此表更新后会在用户下一次登录后生效。
4.3 按照对象过滤
setup_objects表控制是否对特定数据库对象启用监控,对象包括事件、函数、过程、表和触发器。
select * from setup_objects;
-----------+------------------+-----------+-------+-----+
OBJECT_TYPE|OBJECT_SCHEMA |OBJECT_NAME|ENABLED|TIMED|
-----------+------------------+-----------+-------+-----+
EVENT |mysql |% |NO |NO |
EVENT |performance_schema|% |NO |NO |
EVENT |information_schema|% |NO |NO |
EVENT |% |% |YES |YES |
FUNCTION |mysql |% |NO |NO |
FUNCTION |performance_schema|% |NO |NO |
FUNCTION |information_schema|% |NO |NO |
FUNCTION |% |% |YES |YES |
PROCEDURE |mysql |% |NO |NO |
PROCEDURE |performance_schema|% |NO |NO |
PROCEDURE |information_schema|% |NO |NO |
PROCEDURE |% |% |YES |YES |
TABLE |mysql |% |NO |NO |
TABLE |performance_schema|% |NO |NO |
TABLE |information_schema|% |NO |NO |
TABLE |% |% |YES |YES |
TRIGGER |mysql |% |NO |NO |
TRIGGER |performance_schema|% |NO |NO |
TRIGGER |information_schema|% |NO |NO |
TRIGGER |% |% |YES |YES |
-----------+------------------+-----------+-------+-----+
从这个表的默认内容来看,对MySQL系统自带的数据库,监控是关闭的,对其他数据库则是打开的,更新这个表的内容可以只对特定的数据库打开监控,删除user和host值为%的条目,加入需要监控的数据库(模式)的相关条目。
4.4 按线程过滤
PFS也支持按线程来启动监控,通过setup_threads表控制
select * from setup_threads ;
---------------------------------------------------+-------+-------+----------+----------+-------------+
NAME |ENABLED|HISTORY|PROPERTIES|VOLATILITY|DOCUMENTATION|
---------------------------------------------------+-------+-------+----------+----------+-------------+
thread/performance_schema/setup |YES |YES |singleton | 0| |
thread/sql/con_named_pipes |YES |YES
------省略多行
thread/sql/con_sockets |YES |YES |singleton | 0| |
|singleton | 0| |
thread/sql/replica_monitor |YES |YES |singleton | 0| |
---------------------------------------------------+-------+-------+----------+----------+-------------+
如不想监控某些线程,更新此表相关条目即可。
5 PFS过滤示例—只监控表的DDL操作
5.1 简介
有些数据库,需要关注的是对表的ddl操作,比如表drop了,或者是列的定义变了。这种需求,在Oracle和PG可以用系统触发器来实现,对于MySQL这种没有触发器的,使用PFS是比较好的实现方式,下面就是实现表的DDL跟踪的步骤。
5.2 调整setup_instruments,只激活表的ddl相关事件的插桩
有些PFS的插桩之间有依赖关系,statement/sql/类事件依赖statement/abstract事件就是一个例子,如果不激活相关statement/abstract事件,只激活statement/sql/事件,不会收集相关sql的信息。
1)关闭所有以statement开头的插桩
mysql> update performance_schema.setup_instruments set enabled='NO' where name like 'statement/%';
Query OK, 51 rows affected (0.01 sec)
Rows matched: 214 Changed: 51 Warnings: 0
2)激活statement/sql依赖的事件的插桩
mysql> update performance_schema.setup_instruments set enabled='yes' where name like 'statement/abstract/%';
Query OK, 4 rows affected (0.00 sec)
Rows matched: 4 Changed: 4 Warnings: 0
3)激活和表的ddl语句有关的事件的插桩
mysql> update performance_schema.setup_instruments set enabled='yes' where name like 'statement/%table';
Query OK, 5 rows affected (0.00 sec)
Rows matched: 5 Changed: 5 Warnings
4)检查statement类事件插桩设置
mysql> select name,enabled from setup_instruments where name like 'statement/%' and enabled='YES';
+---------------------------------+---------+
| name | enabled |
+---------------------------------+---------+
| statement/sql/create_table | YES |
| statement/sql/alter_table | YES |
| statement/sql/drop_table | YES |
| statement/sql/show_create_table | YES |
| statement/sql/rename_table | YES |
| statement/abstract/clone | YES |
| statement/abstract/Query | YES |
| statement/abstract/new_packet | YES |
| statement/abstract/relay_log | YES |
+---------------------------------+---------+
9 rows in set (0.00 sec)
5.3 效果验证
1)运行一些语句
mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| test |
| test_bak |
+----------------+
2 rows in set (0.00 sec)
mysql> select count(*) from test;
+----------+
| count(*) |
+----------+
| 3 |
+----------+
1 row in set (0.01 sec)
mysql> insert into test_bak values (5,'jack');
Query OK, 1 row affected (0.02 sec)
mysql> drop table test_bak;
Query OK, 0 rows affected (0.07 sec)
2)查询PFS记录的语句
mysql> use performance_schema;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select TIMER_START,sql_text,current_schema from performance_schema.events_statements_history_long where current_schema='TEST';
+------------------+---------------------+----------------+
| TIMER_START | sql_text | current_schema |
+------------------+---------------------+----------------+
| 7343067004258000 | drop table test_bak | test |
+------------------+---------------------+----------------+
1 row in set (0.00 sec)
可以看到,在执行的SQL语句中,只有drop table被记录了下来,这个示例扩展一下,可以限制记录的schema,达到都某个业务的DDL进行监控的目的。
6 小结
MySQL PFS的性能开销完全在可控范围内,8.0 以上版本采用默认配置其性能开销也在可以容忍的范围之内。要注意的是,PFS的开销与数据库的负载基本成正比。在要求严格生产环境内可以按照需求定义监控项,因为大部分监控项的启停都是立即生效的,完全可以只启用基本的监控项(如某个数据库的sql或者事务监控),在需要时在启用其他监控项。




