1引言
在考虑任何软件的性能时,时间和空间之间都存在一个典型的权衡。在评估 MySQL 查询性能的过程中,我们经常关注执行时间(或查询延迟)并将其作为查询性能的主要指标。这是一个很好使用的指标,因为最终我们希望尽快获得查询结果。
我最近发布了一篇关于《如何识别和分析有问题的 MySQL 查询》[1] 的博客文章,其中讨论的重点是衡量执行时间和行读取方面的不良性能。然而,在这次讨论中,内存消耗很大程度上被忽略了。
尽管可能并不经常需要,但 MySQL 还具有内置机制,可以深入了解查询使用了多少内存以及该内存的用途。让我们深入研究一下这个功能,看看如何实时监控 MySQL 连接的内存使用情况。
2内存统计
在 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 中单独检测。其中一些类别包含一小段 documentation
描述该内存类别代表或用途的内容。如果您只想查看具有非空值的内存类型 documentation
,您可以运行:
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 连接和线程之间有一个方便的映射。
3查找连接的用途
此时,您应该在命令行上设置两个与 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 亿行的表中执行一个大型操作,这应该需要一段时间,因为在 alias
列上 SELECT 没有索引:
SELECT alias FROM chat.message ORDER BY alias DESC LIMIT 100000;
现在,在执行过程中,切换到另一个控制台连接并运行以下命令,将线程 ID 替换为您的连接中的线程 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 |
...
这指示执行此查询时每个类别正在使用的内存量。如果在执行另一个 SELECT alias...
查询时多次运行此查询,您可能会看到结果有所不同,因为查询的内存使用量在其整个执行过程中不一定是恒定的。该查询的每次执行都代表某个时刻的一个样本。因此,如果我们想了解使用情况如何随时间变化,我们需要采集许多样本。
memory/sql/Filesort_buffer::sort_keys
表中的 documentation
缺少 performance_schema.setup_instruments
。
SELECT name, documentation
FROM performance_schema.setup_instruments
WHERE name LIKE 'memory%sort_keys';
+---------------------------------------+---------------+
| name | documentation |
+---------------------------------------+---------------+
| memory/sql/Filesort_buffer::sort_keys | |
+---------------------------------------+---------------+
然而,该名称表明它是用于对文件中的数据进行排序的内存。这是有道理的,因为此查询的大部分费用将用于对数据进行排序,以便可以按降序显示。
4随着时间的推移收集使用情况
下一步,我们需要能够对一段时间内的内存使用情况进行采样。对于短查询,这不会那么有用,因为我们只能执行此查询一次,或者在执行分析查询时执行少量次。这对于运行时间较长的查询(需要数秒或数分钟的查询)更有用。无论如何,这些都是我们想要分析的查询类型,因为这些查询可能会使用大部分内存。
这可以完全用 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 毫秒执行一次查询以获取使用最多的 4 个内存类别并打印读数
这可以根据您的分析需求以多种方式进行调整。例如,调整对服务器的 ping 频率或更改每次迭代列出的内存类别数量。在执行查询时运行此命令会提供如下结果:
...
## Memory usage at time 4250 ##
innodb/row0sel -> 25.22Kb
sql/String::value -> 16.07Kb
sql/user_var_entry -> 0.41Kb
innodb/memory -> 0.23Kb
## Memory usage at time 4500 ##
innodb/row0sel -> 25.22Kb
sql/String::value -> 16.07Kb
sql/user_var_entry -> 0.41Kb
innodb/memory -> 0.23Kb
## Memory usage at time 4750 ##
innodb/row0sel -> 25.22Kb
sql/String::value -> 16.07Kb
sql/user_var_entry -> 0.41Kb
innodb/memory -> 0.23Kb
## Memory usage at time 5000 ##
innodb/row0sel -> 25.22Kb
sql/String::value -> 16.07Kb
sql/user_var_entry -> 0.41Kb
innodb/memory -> 0.23Kb
...
这很棒,但有一些弱点。很高兴看到超过前 4 个内存使用类别的内容,但增加该数字会增加这个已经很大的输出转储的大小。如果有一种更简单的方法可以通过一些可视化来一目了然地了解内存使用情况,那就太好了。这可以通过让脚本将结果转储到 CSV 或 JSON,然后在可视化工具中加载它们来完成。更好的是,当数据流入时,我们可以绘制实时结果。这提供了更新的视图,并允许我们实时观察正在发生的内存使用情况,所有这些都在一个工具中完成。
5绘制内存使用情况
为了使该工具更加有用并提供可视化,将进行一些更改。
- 用户将在命令行上提供连接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'''
TID_QUERY='''
SELECT thread_id
FROM performance_schema.threads
WHERE PROCESSLIST_ID=%s'''
class MemoryProfiler:
def __init__(self):
self.x = []
self.y = []
self.mem_labels = ['XXXXXXXXXXXXXXXXXXXXXXX']
self.ms = 0
self.color_sequence = ['#ffc59b', '#d4c9fe', '#a9dffe', '#a9ecb8',
'#fff1a8', '#fbbfc7', '#fd812d', '#a18bf5',
'#47b7f8', '#40d763', '#f2b600', '#ff7082']
plt.rcParams['axes.xmargin'] = 0
plt.rcParams['axes.ymargin'] = 0
plt.rcParams["font.family"] = "inter"
def update_xy_axis(self, results, frequency):
self.ms += frequency
self.x.append(self.ms)
if (len(self.y) == 0):
self.y = [[] for x in range(len(results))]
for i in range(len(results)-1, -1, -1):
usage = float(results[i][1]) / 1024
self.y[i].append(usage)
if (len(self.x) > 50):
self.x.pop(0)
for i in range(len(self.y)):
self.y[i].pop(0)
def update_labels(self, results):
total_mem = sum(map(lambda e: e[1], results))
self.mem_labels.clear()
for i in range(len(results)-1, -1, -1):
usage = float(results[i][1]) / 1024
mem_type = results[i][0]
# Remove 'memory/' from beginning of name for brevity
mem_type = mem_type[7:]
# Only show top memory users in legend
if (usage