MySQL 全文索引触发 OOM 一例

2024-01-17 04:36

本文主要是介绍MySQL 全文索引触发 OOM 一例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

业务监控告警内存不足,笔者进行了全面系统的故障分析并给出解决方案。

作者:付祥,现居珠海,主要负责 Oracle、MySQL、mongoDB 和 Redis 维护工作。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

本文约 1700 字,预计阅读需要 6 分钟。

MySQL 版本 5.7.34

故障现象

某业务监控报警内存不足,发现 mysqld 进程由于内存不足被 kill 自动重启了。

[root@xxxxxx ~]# ps -ef|grep mysqld
root     17117 62542  0 20:26 pts/1    00:00:00 grep --color=auto mysqld
mysql    27799     1  7 09:54 ?        00:48:32 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid
[root@xxxxxx ~]## 操作系统日志记录 MySQL 被 OOM
Dec  8 09:54:42 xxxxxx kernel: Out of memory: Kill process 22554 (mysqld) score 934 or sacrifice child
Dec  8 09:54:42 xxxxxx kernel: Killed process 22554 (mysqld), UID 27, total-vm:11223284kB, anon-rss:7444620kB, file-rss:0kB, shmem-rss:0kB

故障分析

机器总内存 8G,还有其他应用占用了少许内存。尽管 MySQL 重启了,使用内存依然很高,内存监控数据如下:

内存监控

有效内存并不是一点一点的减少,而是突然下降的。内存监控数据是 5 分钟采集一次,MySQL 在 09:54 重启后,09:55:54 采样有效内存是 2869899264 字节,10:00:54 采集降低至 56885248 字节

2023-12-08 10:20:54    
963796992
2023-12-08 10:15:54    
93224960
2023-12-08 10:10:54    
111407104
2023-12-08 10:05:54    
113987584
2023-12-08 10:00:54    
56885248
2023-12-08 09:55:54    
2869899264

InnoDB Buffer 分配的内存为 1G:

| innodb_buffer_pool_size   | 1073741824     |

top 查看 MySQL 当前使用了 6GB 内存:

27799 mysql     20   0 8888376   6.1g   6120 S  26.2 80.5  30:19.01 mysqld

通过 gdb 调用 malloc_stats() 函数查看内存分配情况:

gdb -ex "call (void) malloc_stats()" --batch -p $(pidof mysqld)

查看 MySQL 日志:

MALLOC:      853070392 (  813.6 MiB) Bytes in use by application
MALLOC: +   6369394688 ( 6074.3 MiB) Bytes in page heap freelist
MALLOC: +      9771872 (    9.3 MiB) Bytes in central cache freelist
MALLOC: +       863232 (    0.8 MiB) Bytes in transfer cache freelist
MALLOC: +     25216616 (   24.0 MiB) Bytes in thread cache freelists
MALLOC: +     25559040 (   24.4 MiB) Bytes in malloc metadata
MALLOC:   ------------
MALLOC: =   7283875840 ( 6946.4 MiB) Actual memory used (physical + swap)
MALLOC: +    329924608 (  314.6 MiB) Bytes released to OS (aka unmapped)
MALLOC:   ------------
MALLOC: =   7613800448 ( 7261.1 MiB) Virtual address space used
MALLOC:
MALLOC:          12564              Spans in use
MALLOC:             52              Thread heaps in use
MALLOC:           8192              Tcmalloc page size
------------------------------------------------
Call ReleaseFreeMemory() to release freelist memory to the OS (via madvise()).
Bytes released to the OS take up virtual address space but no physical memory.

MySQL 当前使用 Tcmalloc 内存分配器,Bytes in page heap freelist 使用了将近 6GB 内存,猜测是有什么操作比较吃内存,操作完后 MySQL 释放了内存。但是 Tcmalloc 并没有将内存释放给操作系统,那到底是什么操作比较吃内存呢?分析相关时间段慢 SQL,发现有一个使用全文索引 SQL 比较可疑:

# Time: 2023-12-08T01:52:23.084854Z
# User@Host: xxxxxx @  [x.x.x.x]  Id: 259892877
# Query_time: 1.436714  Lock_time: 0.000049 Rows_sent: 1  Rows_examined: 0
SET timestamp=1702000343;
SELECT count(*) FROM `xx` inner JOIN (select id from xx_content  where  MATCH(content) AGAINST('\"Elasticsearch Cluster in 1 state\"' IN BOOLEAN MODE)) al ON xx.id = al.id WHERE (xx.handle_status in ('pending','processing','completed')) AND `xx`.`sub_type` = 1;
# Time: 2023-12-08T01:52:24.540847Z
# User@Host: xxxxxx @  [x.x.x.x]  Id: 259892879
# Query_time: 1.454352  Lock_time: 0.000052 Rows_sent: 0  Rows_examined: 0
SET timestamp=1702000344;
SELECT xx.*,SUBSTRING(xx.content, 1, 1024) as content,SUBSTRING(xx.sub_content, 1, 1024) as sub_content FROM `xx` inner JOIN (select id from xx_content  where  MATCH(content) AGAINST('\"Elasticsearch Cluster in 1 state\"' IN BOOLEAN MODE)) al ON xx.id = al.id WHERE (xx.handle_status in ('pending','processing','completed')) AND `xx`.`sub_type` = 1 ORDER BY xx.sub_time DESC LIMIT 50;
# Time: 2023-12-08T01:53:26.546353Z
# User@Host: xxxxxx @  [x.x.x.x]  Id: 259893335
# Query_time: 44.198100  Lock_time: 0.000041 Rows_sent: 1  Rows_examined: 48
SET timestamp=1702000406;
SELECT count(*) FROM `xx` inner JOIN (select id from xx_content  where  MATCH(content) AGAINST('\"Elasticsearch Cluster in \"' IN BOOLEAN MODE)) al ON xx.id = al.id WHERE (xx.handle_status in ('pending','processing','completed')) AND `xx`.`sub_type` = 1;
73 
# Time: 2023-12-08T01:56:35.790820Z
# User@Host: xxxxxx @  [x.x.x.x]  Id:  1671
# Query_time: 29.259303  Lock_time: 0.000042 Rows_sent: 1  Rows_examined: 48
SET timestamp=1702000595;
SELECT count(*) FROM `xx` inner JOIN (select id from xx_content  where  MATCH(content) AGAINST('\"Elasticsearch Cluster in \"' IN BOOLEAN MODE)) al ON xx.id = al.id WHERE (xx.handle_status = 'pending') AND `xx`.`sub_type` = 1;
# Time: 2023-12-08T01:56:36.350983Z
# User@Host: xxxxxx @  [x.x.x.x]  Id:  1672
# Query_time: 28.870504  Lock_time: 0.000050 Rows_sent: 1  Rows_examined: 48
SET timestamp=1702000596;
SELECT count(*) FROM `xx` inner JOIN (select id from xx_content  where  MATCH(content) AGAINST('\"Elasticsearch Cluster in \"' IN BOOLEAN MODE)) al ON xx.id = al.id WHERE (xx.handle_status in ('pending','processing','completed')) AND `xx`.`sub_type` = 1;

表结构及数据量如下:

root@3306 xxxxxx> show create table xx_content\G
*************************** 1. row ***************************Table: xx_content
Create Table: CREATE TABLE `xx_content` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`content` longtext,PRIMARY KEY (`id`),FULLTEXT KEY `ngram_content` (`content`) /*!50100 WITH PARSER `ngram` */ 
) ENGINE=InnoDB AUTO_INCREMENT=100377976 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)root@3306 xxxxxx> select count(*) from xx_content;
+----------+
| count(*) |
+----------+
|   360215 |
+----------+
1 row in set (0.11 sec)

全文索引相关参数均采用默认配置:

root@3306 (none)> show variables like '%ft%';
+---------------------------------+----------------+
| Variable_name                   | Value          |
+---------------------------------+----------------+
| ft_boolean_syntax               | + -><()~*:""&| |
| ft_max_word_len                 | 84             |
| ft_min_word_len                 | 4              |
| ft_query_expansion_limit        | 20             |
| ft_stopword_file                | (built-in)     |
| innodb_ft_aux_table             |                |
| innodb_ft_cache_size            | 8000000        |
| innodb_ft_enable_diag_print     | OFF            |
| innodb_ft_enable_stopword       | ON             |
| innodb_ft_max_token_size        | 84             |
| innodb_ft_min_token_size        | 3              |
| innodb_ft_num_word_optimize     | 2000           |
| innodb_ft_result_cache_limit    | 2000000000     |
| innodb_ft_server_stopword_table |                |
| innodb_ft_sort_pll_degree       | 2              |
| innodb_ft_total_cache_size      | 640000000      |
| innodb_ft_user_stopword_table   |                |
+---------------------------------+----------------+
17 rows in set (0.01 sec)

SQL 分别在 9:53 和 9:56 执行,正好在 MySQL 自动重启前后,和内存监控数据比较吻合(9:53 执行,9:54 机器内存不足 MySQL 被 OOM;9:56 执行后,10:00:54 采集降低至 56885248 字节)。这个环境还有一个从库,从库未承载业务,将 SQL 拿到从库执行,观察 MySQL 内存使用变化,开 2 个窗口,1 个窗口执行 SQL:

root@3306 xxxxxx> SELECT count(*) FROM `xx` inner JOIN (select id from xx_content  where  MATCH(content) AGAINST('\"Elasticsearch Cluster in 1 state\"' IN BOOLEAN MODE)) al ON xx.id = al.id WHERE (xx.handle_status in ('pending','processing','completed')) AND `xx`.`sub_type` = 1;
+----------+
| count(*) |
+----------+
|        3 |
+----------+
1 row in set (1 min 9.31 sec)

另一个窗口观察 mysqld 进程内存使用情况:

[root@xxxxxx ~]# ps aux|grep mysqld|grep -v grep|awk '{print $6}'
3453980
[root@xxxxxx ~]# while true;do ps aux|grep mysqld|grep -v grep|awk '{print $6}';sleep 1;done;
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3453980
3459064
3617600
3822828
3969212
4128056
4533612
4677028
4756868
4844452
5011176
5070292
5123844
5188556
5263880
5410368
5410368
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200
5412200

可以观察到 SQL 执行过程中,内存不断上涨,SQL 执行完后内存从 3453980 KB 涨到 5412200KB,但是 Tcmalloc 并没有将内存释放给操作系统。

到目前为止,总算定位到了问题,MySQL 并不擅长全文索引,可以交给 ElasticSearch 等数据库去做,那在业务不调整情况下,怎么解决问题呢?不妨换 Jemalloc 内存分配器试试:

[root@xxxxxx ~]# yum install -y jemalloc
[root@xxxxxx ~]# cat /etc/sysconfig/mysql
#LD_PRELOAD=/usr/lib64/libtcmalloc.so
LD_PRELOAD=/usr/lib64/libjemalloc.so.1
[root@xxxxxx ~]# systemctl restart mysqld

果然有惊喜,SQL 执行完后会释放内存,从 822948KB 涨到 2738040KB,最终回落到 916400KB:

[root@xxxxxx ~]# while true;do ps aux|grep mysqld|grep -v grep|awk '{print $6}';sleep 1;done;
822948
822948
822948
822948
822948
822948
822948
822948
822948
822948
822948
822948
822948
822948
874216
1057240
1273684
1443820
1662924
1873304
2177760
2502488
2738040
1296604
899580
900636
902412
903680
904384
......
914492
914492
915020
915284
915736
916524
916524
916524
916524
916524
916400
916400
916400
916400
916400
916400
916400
916400
916400

总结

线上 MySQL 都是使用 Tcmalloc 内存分配器一直很稳定,并未出现服务器内存不足问题。本次出现服务器内存不足,是因为使用了全文索引这种极少使用的场景触发,换成 Jemalloc 后,内存使用整体上得到了控制。

更多技术文章,请访问:https://opensource.actionsky.com/

关于 SQLE

SQLE 是一款全方位的 SQL 质量管理平台,覆盖开发至生产环境的 SQL 审核和管理。支持主流的开源、商业、国产数据库,为开发和运维提供流程自动化能力,提升上线效率,提高数据质量。

SQLE 获取

类型地址
版本库https://github.com/actiontech/sqle
文档https://actiontech.github.io/sqle-docs/
发布信息https://github.com/actiontech/sqle/releases
数据审核插件开发文档https://actiontech.github.io/sqle-docs/docs/dev-manual/plugins/howtouse

这篇关于MySQL 全文索引触发 OOM 一例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/614882

相关文章

mysql索引四(组合索引)

单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引;组合索引,即一个索引包含多个列。 因为有事,下面内容全部转自:https://www.cnblogs.com/farmer-cabbage/p/5793589.html 为了形象地对比单列索引和组合索引,为表添加多个字段:    CREATE TABLE mytable( ID INT NOT NULL, use

mysql索引三(全文索引)

前面分别介绍了mysql索引一(普通索引)、mysql索引二(唯一索引)。 本文学习mysql全文索引。 全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。 在MySql中,创建全文索引相对比较简单。例如:我们有一个文章表(article),其中有主键ID(

mysql索引二(唯一索引)

前文中介绍了MySQL中普通索引用法,和没有索引的区别。mysql索引一(普通索引) 下面学习一下唯一索引。 创建唯一索引的目的不是为了提高访问速度,而只是为了避免数据出现重复。唯一索引可以有多个但索引列的值必须唯一,索引列的值允许有空值。如果能确定某个数据列将只包含彼此各不相同的值,在为这个数据列创建索引的时候就应该使用关键字UNIQUE,把它定义为一个唯一索引。 添加数据库唯一索引的几种

mysql索引一(普通索引)

mysql的索引分为两大类,聚簇索引、非聚簇索引。聚簇索引是按照数据存放的物理位置为顺序的,而非聚簇索引则不同。聚簇索引能够提高多行检索的速度、非聚簇索引则对单行检索的速度很快。         在这两大类的索引类型下,还可以降索引分为4个小类型:         1,普通索引:最基本的索引,没有任何限制,是我们经常使用到的索引。         2,唯一索引:与普通索引

【服务器运维】MySQL数据存储至数据盘

查看磁盘及分区 [root@MySQL tmp]# fdisk -lDisk /dev/sda: 21.5 GB, 21474836480 bytes255 heads, 63 sectors/track, 2610 cylindersUnits = cylinders of 16065 * 512 = 8225280 bytesSector size (logical/physical)

SQL Server中,查询数据库中有多少个表,以及数据库其余类型数据统计查询

sqlserver查询数据库中有多少个表 sql server 数表:select count(1) from sysobjects where xtype='U'数视图:select count(1) from sysobjects where xtype='V'数存储过程select count(1) from sysobjects where xtype='P' SE

SQL Server中,always on服务器的相关操作

在SQL Server中,建立了always on服务,可用于数据库的同步备份,当数据库出现问题后,always on服务会自动切换主从服务器。 例如192.168.1.10为主服务器,12为从服务器,当主服务器出现问题后,always on自动将主服务器切换为12,保证数据库正常访问。 对于always on服务器有如下操作: 1、切换主从服务器:假如需要手动切换主从服务器时(如果两个服务

SQL Server中,isnull()函数以及null的用法

SQL Serve中的isnull()函数:          isnull(value1,value2)         1、value1与value2的数据类型必须一致。         2、如果value1的值不为null,结果返回value1。         3、如果value1为null,结果返回vaule2的值。vaule2是你设定的值。        如

SQL Server中,添加数据库到AlwaysOn高可用性组条件

1、将数据添加到AlwaysOn高可用性组,需要满足以下条件: 2、更多具体AlwaysOn设置,参考:https://msdn.microsoft.com/zh-cn/library/windows/apps/ff878487(v=sql.120).aspx 注:上述资源来自MSDN。

SQL Server中,用Restore DataBase把数据库还原到指定的路径

restore database 数据库名 from disk='备份文件路径' with move '数据库文件名' to '数据库文件放置路径', move '日志文件名' to '日志文件存放置路径' Go 如: restore database EaseWe from disk='H:\EaseWe.bak' with move 'Ease