一、问题现象
数据展示系统出现异常,首页刷不出数据,查看日志后发现模块无法连接数据库(从库,以下数据库都表示从库),紧接着数据分析模块出现报警,服务器出现磁盘空间不足报警。
查看AWS RDS监控发现,在时间段 ‘08.31 19:15:00 – 09.01 01:19:00’ 内,数据库实例存储空间急剧下降
监控显示,数据库连接数量没有明显增加,但在该时间段内磁盘IO明显增大
磁盘IO
数据库连接数量
通过查看实例上磁盘的占用情况,发现磁盘临时表占用了大量空间(420G/500G),于是我们开始定位为何会有大量的临时表被持久化到磁盘上
二、问题分析与复现
2.1 临时表
MySQL中有两种临时表:外部临时表和内部临时表。其中外部临时表可有用户查询时手动创建保存一些中间数据,提升查询效率等;
内部临时表会在查询过程中由Mysql自动创建并存储某些中间操作的结果,这种操作可能出现在优化阶段或者执行阶段,所以这种临时表对用户不可见,一般通过EXPLAIN可以查看查询语句是否用到了内部临时表。
而内部临时表又可以被分为磁盘临时表和内存临时表,Mysql在使用内部临时表时会优先使用内存临时表,即将临时表存放在内存里,当临时表过大或内存不够用时,就会转化成磁盘临时表,存放在ibtmp1文件中。
在Mysql5.7中,内存临时表在查询结束后会被释放,而文件ibtmp1占用的空间不会被自动释放,但会被标记删除,等数据库重启时释放这部分空间。
(如何释放?数据库服务关闭时,会直接删除ibtmp1文件,等数据库重启时,会新建一个分配了初始空间新ibtmp1文件)
2.2 相关配置项
-
内存临时表可使用的内存空间(min(\’max_heap_table_size\’, \’tmp_table_size\’)):16M
-
初始ibtmp1文件大小(innodb_temp_data_file_path):12M且增长无上限
- ibtmp1文件信息(被扩展的总空间) = TOTAL_EXTENTS(拓展次数) * EXTENT_SIZE(每次扩展的大小): 204次 * 1M/次 = 204M
2.3 问题分析
查看mysql的monitor device_base和device_bt_base的rate暴涨,插入等正常,由此猜测是这两个表查询引起的临时表使用空间暴涨,查看是事故时间的query,pharos请求相较平成更加频繁,由此猜测是并发量太大导致,接着预本地模拟,尝试重现事故。
2.5 问题复现
- 搭建数据库实例Mysql5.7.31
-
创建测试表
CREATE TABLE `account` ( `user_id` bigint(20) NOT NULL, `address` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `email` varchar(255) NOT NULL, `nickname` varchar(255) DEFAULT NULL, `password` varchar(255) NOT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`user_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 插入测试数据约30W条,表大小为28.56M
-
开始测试
1.执行会使用临时表的查询语句,且不显示释放客户端,单线程查询3000次def groupData(k): i = 0 a = [] while i < 3000: i = i + 1 conn = pymysql.connect(host=\'127.0.0.1\', port=3307, user=\'root\', password=\'123456\', db=\'mysql\') cur = conn.cursor() group_sql = \'SELECT address, COUNT(*) as count from account where user_id > %s GROUP BY address\' % (3000 + i / 2 * 10000) print(group_sql) cur.execute(group_sql) for i in a: print(len(i)) time.sleep(100) print(\"stop\")
执行结果为:ibtmp1文件未进行任何扩展,查询结束后依然为12M。表明每次查询的临时表都使用了内存空间,执行完成之后空间被释放
-
2.依然使用上面的函数groupData进行查询,但改用多线程并发执行,查询总次数一致,用20个线程每个查询150次def groupData(k): i = 0 a = [] while i < 150: i = i + 1 conn = pymysql.connect(host=\'127.0.0.1\', port=3307, user=\'root\', password=\'123456\', db=\'mysql\') cur = conn.cursor() group_sql = \'SELECT address, COUNT(*) as count from account where user_id > %s GROUP BY address\' % (3000 + k / 2 * 10000) print(group_sql) cur.execute(group_sql) for i in a: print(len(i)) time.sleep(100) print(\"stop\")if __name__ == \'__main__\': with ThreadPoolExecutor(max_workers=40) as e: for k in range(40): e.submit(groupData, k)
执行结果为:ibtmp1文件进行了460次扩展,查询结束后ibtmp1文件大小为460M。表明查询时大量内部临时表使用了磁盘临时表
- 原因分析
不同客户端并发访问Mysql进行查询时,产生的内存临时表会暂时占据已分配的内存空间,后面查询产生的临时表因为没有足够的内存空间而改用磁盘临时表空间
而磁盘空间不会因为客户端断开而释放空间,最终导致磁盘空间被消耗殆尽。
2.6 问题解决
-
导致产生本次问题的SQL语句如下。device_type和config_model字段没有索引,查询会产生较大的临时表。需要对该字段添加索引避免产生临时表。
SELECT device_type, config_model, count(*) AS rowCount FROM device_base GROUP BY device_type, config_model
为何为忽然出现该问题?
系统中利用缓存限制访问数据库的次数,并发访问时会通过缓存获取数据。但由于更新缓存的操作没有加锁,当缓存大面积失效且有大量请求时,出现该问题
-
数据库实例参数设置
目前我们的数据库临时表空间的参数为默认参数,即初始大小为12M,可扩展大小无限制。可考虑设置扩展大小上限,避免磁盘被直接耗尽影响到所有的数据库操作。innodb_temp_data_file_path = ibtmp1:12M:autoextend:max:20000M