MySQL 中内存使用的分析
在评估任何软件的性能时,一直存在时间和空间之间的经典权衡。在评估 MySQL 查询性能的过程中,我们通常将执行时间(或查询延迟)作为主要指标来衡量查询性能。这是一个非常好的指标,因为最终目标是希望尽可能快地获得查询结果。
我最近发布了一篇关于如何识别和分析问题 MySQL 查询的博客,其讨论围绕通过执行时间和行读取来衡量性能问题展开。然而,在讨论中,内存消耗问题却被很大程度上忽略了。
虽然内存分析的需求相对较少,但 MySQL 内置了一些机制,可以让我们深入了解查询的内存使用情况以及具体用途。本文将深入探讨这些功能,看看如何进行实时监控以分析 MySQL 连接的内存使用情况。
内存统计
在 MySQL 中,系统的许多组件都可以单独进行检测。performance_schema.setup_instruments
表列出了所有这些组件,而其数量相当可观:
SELECT COUNT(*) FROM performance_schema.setup_instruments;
+----------+ | count(*) | +----------+ | 1255 | +----------+
表中包含许多可用于内存分析的检测项。你可以通过选择该表并按 memory/
过滤条件查看可用项:
SELECT name, documentation FROM performance_schema.setup_instruments WHERE name LIKE 'memory/%';
你会得到几百条结果。每一条数据都代表一个内存类别,这些类别可以在 MySQL 中单独检测。有些类别附带了简短的文档,描述其表示的内存类别或用途。如果你只想查看文档值非空的类别,可以运行:
SELECT name, documentation FROM performance_schema.setup_instruments WHERE name LIKE 'memory/%' AND documentation IS NOT NULL;
这些内存类别可以在多个不同粒度上进行采样。这些粒度存储在多个表中:
SELECT table_name FROM information_schema.tables WHERE table_name LIKE '%memory_summary%' AND table_schema = 'performance_schema';
+-----------------------------------------+ | TABLE_NAME | +-----------------------------------------+ | memory_summary_by_account_by_event_name | | memory_summary_by_host_by_event_name | | memory_summary_by_thread_by_event_name | | memory_summary_by_user_by_event_name | | memory_summary_global_by_event_name | +-----------------------------------------+
以下为各表的用途:
- memory_summary_by_account_by_event_name:基于账户(用户和主机的组合)汇总内存事件;
- memory_summary_by_host_by_event_name:以主机级别汇总内存事件;
- memory_summary_by_thread_by_event_name:以 MySQL 线程级别汇总内存事件;
- memory_summary_by_user_by_event_name:以用户级别汇总内存事件;
- memory_summary_global_by_event_name:内存统计信息的全局汇总。
请注意,此功能无法直接以查询级别跟踪内存使用情况。不过,这并不意味着我们不能分析查询内存使用!我们可以监控执行相关查询时所在连接的内存使用情况。因此,我们的重点将放在 memory_summary_by_thread_by_event_name
表上,因为它使 MySQL 连接和线程之间建立了便利的关联。
确定某个连接的内存使用情况
此时,你需要在命令行中设置两个 MySQL 服务器连接。第一个连接用于执行你想监控内存使用的查询,第二个连接则用于监控目的。
在第一个连接中,运行以下查询以获取连接 ID 和线程 ID:
SET @cid = (SELECT CONNECTION_ID()); SET @tid = (SELECT thread_id FROM performance_schema.threads WHERE PROCESSLIST_ID=@cid);
然后获取这些值。当然,你的值可能会与下述示例有所不同:
SELECT @cid, @tid;
+------+------+ | @cid | @tid | +------+------+ | 49 | 89 | +------+------+
接下来,执行你想分析内存使用的某些长时间运行的查询。例如,以下是从一个包含 1 亿行的大表中执行 SELECT 查询:
SELECT alias FROM chat.message ORDER BY alias DESC LIMIT 100000;
在查询执行期间,切换到另一个控制台连接并运行以下查询,并用你的线程 ID 替换 YOUR_THREAD_ID
:
SELECT event_name, current_number_of_bytes_used FROM performance_schema.memory_summary_by_thread_by_event_name WHERE thread_id = YOUR_THREAD_ID ORDER BY current_number_of_bytes_used DESC;
你应该看到类似以下的结果,虽然具体详情会因查询和数据的不同而有所变化:
+---------------------------------------+------------------------------+ | event_name | current_number_of_bytes_used | +---------------------------------------+------------------------------+ | memory/sql/Filesort_buffer::sort_keys | 203488 | | memory/innodb/memory | 169800 | | memory/sql/THD::main_mem_root | 46176 | | memory/innodb/ha_innodb | 35936 | ...
此输出指示在执行查询的确切时间点每个内存类别的使用情况。内存使用在整个查询执行过程中并不是恒定的,因此如果想观察内存使用随时间的变化,你需要进行多次采样。
采集随时间变化的内存使用情况
下一步是能够随时间采样内存使用情况。对于耗时较短的查询,这种方法可能用处不大,因为在查询执行时,只能运行一次或少数几次采样。不过,对于运行时间较长(耗时数秒或数分钟)的查询,此方法非常有用,这类查询往往会占用大量内存。
你可以完全使用 SQL 并通过存储过程实现此功能。不过,在本案例中,我们将用一个 Python 脚本来进行监控:
#!/usr/bin/env python3 import time import MySQLdb import argparse MEM_QUERY=''' SELECT event_name, current_number_of_bytes_used FROM performance_schema.memory_summary_by_thread_by_event_name WHERE thread_id = %s ORDER BY current_number_of_bytes_used DESC LIMIT 4 ''' parser = argparse.ArgumentParser() parser.add_argument('--thread-id', type=int, required=True) args = parser.parse_args() dbc = MySQLdb.connect(host='127.0.0.1', user='root', password='password') c = dbc.cursor() ms = 0 while(True): c.execute(MEM_QUERY, (args.thread_id,)) results = c.fetchall() print(f'\n## Memory usage at time {ms} ##') for r in results: print(f'{r[0][7:]} -> {round(r[1]/1024,2)}Kb') ms+=250 time.sleep(0.25)
此脚本的功能包括:
- 从命令行获取线程 ID,并监控其内存使用;
- 建立到 MySQL 数据库的连接;
- 每 250 毫秒执行一次查询,获取当前内存使用的前四大类别并输出结果。
通过执行以上脚本,你可以获取类似以下的结果:
... ## Memory usage at time 4250 ## innodb/row0sel -> 25.22Kb sql/String::value -> 16.07Kb sql/user_var_entry -> 0.41Kb innodb/memory -> 0.23Kb ...
虽然实用,但仍有改进空间。例如,通过某些可视化工具更方便地查看内存使用的总体情况,而不是直接输出结果。
内存使用的可视化
为使工具更加实用,同时提供可视化功能,我们可以对上述脚本做以下改动:
- 从命令行获取连接 ID,由脚本负责查找关联线程;
- 监控频率可通过命令行配置;
- 使用
matplotlib
库生成内存使用的堆栈图,并显示过去 50 次采样结果。
以下是完整示例代码(省略部分内容):
#!/usr/bin/env python3 import matplotlib.pyplot as plt import numpy as np import MySQLdb import argparse MEM_QUERY=''' SELECT event_name, current_number_of_bytes_used FROM performance_schema.memory_summary_by_thread_by_event_name WHERE thread_id = %s ORDER BY event_name DESC ''' # 可视化代码省略…
通过运行此脚本,你可以实时观察查询的内存使用增长,并查看每个内存分类对总体内存使用的贡献比例。
总结
尽管内存分析的需求可能不如查询延迟那样频繁,但当需要进行详细查询优化时,获取详细内存使用信息会非常有价值。这可以揭示 MySQL 在何时以及为何对系统造成内存压力,同时帮助评估是否需要为你的数据库服务器升级内存配置。通过 MySQL 提供的一系列基础功能,你可以构建更先进的工具来分析查询和工作负载中的内存使用。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接
本文链接:https://www.choupangxia.com/2025/09/14/profiling-memory-usage-in-mysql/