MySQL容器凌晨OOM?一行Infinity字符串引发的内存惨案

MySQLDockerOOM 本内容遵循CC 4.0 BY-SA版权协议
McGL McGL 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要

1. 故障现场:MySQL 容器凌晨被“强制下课”

收到告警那会儿,我人还在吃早饭。生产环境的 MySQL 容器连着重启了两次,监控面板上一片飘红,眼瞅着 OOMKilled 四个字弹出来,我就知道这顿早饭是泡汤了。环境是银河麒麟 V10 SP3 服务器版,MySQL 8.0 跑在 Docker 容器里,内存限制 2GB。当时我还没想到,后面定位到的罪魁祸首,竟然是一行写进数据库里的 infinity 字符串。

1.1 第一反应:确认是不是被内核 OOM Kill 了

容器重启的原因有很多,配置错误、健康检查失败、MySQL 主动崩溃,都有可能。但我看到 STATUS 列显示 Restarting (1) 5 seconds ago,第一反应就是内存问题,因为容器重启通常不会让 MySQL 进程直接退出,除非是被外部强制杀掉。

两步确认,基本不用猜:

第一步看 Docker 侧的状态。执行 docker inspect <container_id>,在返回的 JSON 里找 State.ExitCode 和 State.OOMKilled。如果 OOMKilled 是 true,那基本可以锁定方向。

第二步看系统内核日志。在宿主机上执行:

BASH
dmesg -T | grep -i -E 'out of memory|killed process'

如果有类似这样的记录:

TEXT
[Fri May 17 10:35:12 2025] Out of memory: Killed process 12345 (mysqld) total-vm:3187652kB, anon-rss:2101248kB

那就说明是内核的 OOM killer 选择了 mysqld 进程,把它干掉了。这个阶段别急着改代码,先把服务拉起来,让业务先恢复运行。

1.2 快速止血:先让业务恢复

我的处理顺序是先恢复服务,再找根因。当时容器里跑的 MySQL 配置了 innodb_buffer_pool_size=1G,再加上 2GB 的容器内存限制,正常情况下应该能跑。但我还是先把参数临时调小:

INI
innodb_buffer_pool_size=512M
max_connections=80

重启容器后,服务恢复正常。但我知道这只能管几个小时,因为如果真有数据或 SQL 层面的问题,内存迟早还会被打满。

果然,三个小时后,报警又来了。这说明问题不在配置文件,而是有某类查询在持续制造内存压力。

2. 根因定位:“一行 infinity”如何引爆内存

2.1 慢日志里的可疑 SQL

服务再次挂掉后,我直接打开 MySQL 慢查询日志,按运行耗时排序,很快找到了一条非常可疑的 SQL:

SQL
SELECT id, product_name, detail
FROM product_sku
WHERE detail REGEXP 'infinity'
ORDER BY create_time DESC
LIMIT 10;

注意这个 REGEXP 'infinity',它在 MySQL 8.0 里走的是 ICU 正则引擎,对 TEXT 类型字段做全表扫描时,每一行都要把整个字段内容拉进内存做正则匹配。product_sku 表当时有 500 万行,detail 字段平均长度 2KB 左右,单次扫描光读数据就是 10GB 的 I/O,更别提正则匹配本身的开销。

用 EXPLAIN 看执行计划,type 是 ALL,rows 预估 500 万,Extra 里写着 Using where; Using filesort。也就是说,这条 SQL 要先把 500 万行的 detail 字段全部扫一遍,找出所有匹配 infinity 的行,还要对结果集做排序。一次这样的查询,内存占用就会非常难看。

但光有这条 SQL,还不足以让 2GB 的容器崩掉。真正的问题在于:匹配出来的结果集太大了。

2.2 数据源头:Infinity 是怎么进到 MySQL 的

顺着这条 SQL 往前查,我发现一个更隐蔽的问题:product_sku 表里至少有 130 万行的 detail 字段包含 infinity 这个字符串。

原因是上游一个库存服务在“不限量”的时候,用 Float.POSITIVE_INFINITY 作为库存上限,Jackson 序列化之后写成 {"stock_limit":"Infinity"},再通过消息队列写入 MySQL。字段类型是 TEXT,所以这个不是合法 JSON 的字符串也能顺利落库。

也就是说,原本只是“一行数据”的小毛病,被越滚越大:业务侧以为写的是“不限量”,数据库里存的却是字符串 Infinity,而查询侧恰好又写了一行正则去匹配这个值。三者一叠加,就变成了一场灾难。

2.3 为什么这条 SQL 能把容器内存打满

先看正则匹配本身。MySQL 8.0 的 REGEXP 用的是 ICU 正则库,它会在匹配前把待匹配字符串转换成 UTF-16 编码。2KB 的 TEXT 字段在转换时,内存开销大约是原始大小的 2 到 4 倍,500 万行读下来,单线程的内存峰值就可能超过 500MB。

再看排序和临时表。匹配出的 130 万行结果,如果要 ORDER BY create_time DESC,MySQL 的优化器如果判断无法用索引消除排序,就会走 filesort。排序缓冲区 sort_buffer_size 默认只有 256KB,排不下这么多行,MySQL 会把部分数据放到临时表里继续排。

MySQL 8.0 的默认临时表存储引擎是 TempTable,它和老的 MEMORY 引擎不一样,单个临时表的上限由 temptable_max_ram(默认 1GB)控制。也就是说,一条带排序的查询,理论上可以单独申请接近 1GB 的内存。更关键的是,TempTable 在内存中维护的是映射到磁盘文件的匿名映射,它会预留大量虚拟内存空间,虽然物理内存不一定会全部分配,但在内存水位已经很高的容器里,任何一次突增都可能是压垮骆驼的最后一根稻草。

如果此时恰好有三五个慢查询并发执行,比如运营同事在后台同步跑了好几个类似的分析任务,内存瞬间就能突破 2GB,容器被 OOM Kill 就成了必然。

3. 为什么偏偏是麒麟 V10 容器环境

3.1 容器内存限制与内核 OOM killer 的协作逻辑

这个故障在物理机上大概率不会发生,或者至少不会表现得这么极端。原因在于容器和物理机对内存的统计口径完全不同。

在银河麒麟 V10 这类基于 openEuler 22.03 内核的系统上,Docker 的 --memory=2g 参数最终会写进 cgroup 的 memory.max 文件(cgroup v2)或 memory.limit_in_bytes 文件(cgroup v1)。内核在统计容器内存时,会包含 cgroup 内所有进程的匿名内存、页缓存、内核栈等。当总内存超过限制时,内核会先尝试回收页缓存;如果回收后仍然超限,就会触发 OOM killer。

问题在于,MySQL 的 innodb_buffer_pool_size 申请的内存大部分是匿名内存,无法简单回收。当 Buffer Pool 已经占掉 1GB,再加上排序、临时表、连接线程的内存,容器内可用的空闲内存其实非常有限。此时任何一次稍微大点的查询,都会让内核判定容器内存超额。

3.2 物理机思维和容器思维的核心差异

很多 DBA 在物理机上管理 MySQL 久了,会有一种“2GB 内存配 1GB Buffer Pool 已经很保守”的感觉。这种思路放到容器里就很容易翻车,因为物理机上 MySQL 进程只能看到自己需要的那部分内存,而容器里 Kernel 统计的是整个 cgroup 的总占用。

所以要养成一个习惯:给 MySQL 容器分配内存时,不要把 Buffer Pool 当成唯一的耗内存大户。连接数、排序、临时表、日志缓冲,这些都是要算进去的。

我自己在排障时常用的一组参考占比是:容器内存 4GB,Buffer Pool 配 1.5GB,排序/临时表类参数控制在 512MB 以内,剩下的大约 2GB 留给页缓存、连接线程和系统开销。这个比例不能说最优化,但至少能在绝大多数业务场景下避免被 OOM Kill。

3.3 定位容器内存水位时常用的几条命令

排障过程中,以下几条命令可以快速帮你判断 MySQL 容器的内存压力:

BASH
# 查看容器 OOM 状态
docker inspect <container_id> | grep -i oom
 
# 动态观察容器内存
docker stats --no-stream
 
# 查看 cgroup v2 的内存配置(麒麟 V10 适用)
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.events
 
# 查看 MySQL 侧的内存分配情况
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'temptable_max_ram';
SHOW VARIABLES LIKE 'max_connections';

其中 memory.events 里的 oom_kill 计数如果持续增长,说明容器确实反复触发了内核回收和被杀进程,这个信息比 docker stats 里瞬时百分比要有用得多。

4. 修复落地:从数据库参数到业务代码

4.1 临时修复:把 MySQL 的“任督二脉”先控制住

查清根因后,我做的第一件事是把排序和临时表相关的内存参数压下来,防止同类查询再次把容器打挂。以下是我在 my.cnf 里临时调整的配置:

INI
[mysqld]
innodb_buffer_pool_size = 512M
temptable_max_ram = 256M
tmp_table_size = 64M
max_heap_table_size = 64M
sort_buffer_size = 2M
join_buffer_size = 2M
max_connections = 80
table_open_cache = 512

参数我都解释一下:

  • innodb_buffer_pool_size 从 1G 降到 512M,给排序和临时表留出空间。
  • temptable_max_ram 从 1G 降到 256M,让 TempTable 引擎在内存达到 256MB 后就直接转磁盘临时表,而不是继续吃内存。
  • tmp_table_size 和 max_heap_table_size 控制普通内存临时表的上限,设成 64M,避免 GROUP BY / DISTINCT 这类操作把内存吃满。
  • sort_buffer_size 和 join_buffer_size 这两个参数是按会话分配的,如果设置过大,比如 16MB,那 100 个连接同时跑,就是 1.6GB 出去了。所以容器环境下务必要保守。
  • max_connections=80 是为了控制连接线程总体占用,每个连接按 1MB 到 2MB 算,80 个连接大约占用 160MB。

调整后,同样的慢查询虽然还是很慢,但至少不会让容器 OOM 了。当然这只是临时止血,真正的修复在下面两步。

4.2 数据修复:清理脏数据,规范写入逻辑

修复 SQL 和数据是治本的关键。我先把 130 万行垃圾数据分批清理掉:

SQL
-- 每批只更新 1 万行,避免产生大事务和长时间锁表
UPDATE product_sku
SET stock_limit = NULL
WHERE stock_limit = 'Infinity'
LIMIT 10000;

重复执行若干次,直到影响行数为 0。这个操作要放在业务低峰期做,并且提前在测试环境验证。

然后处理写入端的问题。上游服务不能再把 Float.POSITIVE_INFINITY 直接序列化进数据库,我建议在写入前做一轮校验和转换:

  • 如果库存上限不存在或为无穷大,统一写成 null,或者显示指定为 unlimited 标签。
  • 如果业务需要保留原始文本,建议改装 JSON 字段,MySQL 8.0 的 JSON 类型有严格的格式校验,凡是 Infinity 这种非法 JSON 值根本写不进去,相当于在数据库入口加了一道防火墙。

4.3 根治方案:去掉正则,用可索引字段替代

SQL 层面,最直接的优化是不要对 TEXT 字段做正则匹配。这里有两种做法。

第一种是给表增加一个独立的标签列:

SQL
ALTER TABLE product_sku
ADD COLUMN is_unlimited TINYINT NOT NULL DEFAULT 0,
ADD INDEX idx_unlimited (is_unlimited);

然后业务在写入时维护这个字段,查询时直接:

SQL
SELECT id, product_name, detail
FROM product_sku
WHERE is_unlimited = 1
ORDER BY create_time DESC
LIMIT 10;

这个查询可以命中 idx_unlimited 索引,单次执行时间从原来的 8 秒下降到毫秒级。

第二种做法,如果必须保留 detail 里的 JSON 内容,可以加生成列:

SQL
ALTER TABLE product_sku
ADD COLUMN is_unlimited TINYINT
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(detail, '$.stock_limit')) = 'infinity' OR JSON_UNQUOTE(JSON_EXTRACT(detail, '$.stock_limit')) = 'Infinity') STORED,
ADD INDEX idx_unlimited (is_unlimited);

需要注意的是,生成列依赖 detail 能被 JSON_EXTRACT 正常解析。如果字段里存在大量非法 JSON,建议先把写入端修好,再做这一步。

5. 从这次 OOM 里沉淀下来的排查清单

5.1 遇到 MySQL 容器 OOM 的六步排查法

第一步,确认容器状态。执行 docker inspect 查看 OOMKilled 是否为 true。

第二步,看内核日志。执行 dmesg -T | grep -i -E 'out of memory|killed process',确认被杀进程的 PID 和虚拟内存大小。

第三步,看 MySQL 慢日志。找到 OOM 前最后执行的慢 SQL,重点关注涉及 REGEXP、LIKE '%xxx%'、ORDER BY、GROUP BY、大 IN 子查询的语句。

第四步,分析执行计划。用 EXPLAIN 查看扫描行数和是否触发 filesort/temp table,判断查询本身是否存在性能陷阱。

第五步,复盘内存参数。逐一核对 innodb_buffer_pool_size、temptable_max_ram、tmp_table_size、sort_buffer_size、join_buffer_size、max_connections 等。

第六步,追查数据规范。确认是否存在类似 Infinity、空字符串、超长文本这样的异常数据被反复扫描匹配。

5.2 常见问题速查表

现象 可能原因 排查方式
容器反复重启,日志无异常 内核 OOM killer 强制终止 docker inspect 查看 OOMKilled
MySQL 查询变慢,内存上涨 全表扫描 + filesort EXPLAIN 查看执行计划
单条查询内存瞬间暴涨 TempTable 临时表引擎预分配 调低 temptable_max_ram
容器内存缓慢爬升 Buffer Pool 过大或连接数泄漏 docker stats + SHOW PROCESSLIST
正则匹配导致 CPU 和内存双高 ICU 正则引擎 + TEXT 全表扫描 改用可索引字段或全文索引

5.3 长期预防:把监控和告警做在前面

这次故障之后,我给运维平台补了三层监控:

第一层,容器级内存告警。通过定时采集 docker stats 或读取 cgroup 的 memory.events,当内存使用率超过 80% 时报警。

第二层,慢查询分析。开启 slow_query_log,每周跑一次 pt-query-digest 分析慢日志,把 REGEXP、无索引大表扫描、大排序查询列为重点观察对象。

第三层,SQL 上线审查。凡是涉及新增查询的,必须看执行计划,凡是表里出现 TEXT 字段且查询条件无法命中索引的,必须提出替代方案才允许上线。

还有一个很多人忽略的点:写入端的数据质量要抓。我后来让负责库存服务的同事在序列化前就把 Infinity 转为 null,代码一行改动,数据库压力立减一半。很多时候,最贵的修复不是调参数,而是补上业务逻辑里的那个小洞。

这次排查给我的最大感受是,容器环境把 MySQL 的“内存天花板”直接明码标价了,Buffer Pool 配得再合理,也架不住一条烂查询加一批脏数据同时出现。排障时别急着调参,先顺着慢日志和数据源头把问题问清楚:谁写的这条 SQL?谁放进去的这个值?答案往往比参数本身更有价值。最后再分享一个小技巧:排查容器 OOM 时,多看 memory.events 里的 oom_kill 次数,它会很诚实地告诉你,这个容器到底被内核“处理”过多少次。上面这些参数和步骤,优先在测试环境完整验证一遍,再上生产操作,不然踩坑的代价可就大了。

相关推荐

基于Python+Open CV的手势识别算法设计

伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大方向;其中,手势识别是人机交互领域的一项关键技术,自此手势识别也迎来了一波高潮,近几年无论是在消费领域、电子领域、数码领域、家电领域甚至汽车领域上,都能或多或少的见到手势识别的身影。 手势识别在设计智能高效的人机界面方面具有至关重要的作用, 目前手势识别已应用到手语识别、智能监控、到虚拟现实等各个领域,手势识别的原理都是利用各种传感器(例如红外、摄像头等)对手部的形态进行捕捉并进行建模,形成一个模型信息的序列帧,而后将这些信息序列转换为机器能够识别的相对应指令(例如打开、切换菜单、移动等)来完成控制。 采用Python的集成开发环境Pycharm进行本次课程设计,在Pycharm中进行需要库(模块)的下载,调取电脑摄像头,按帧读取摄像头采集到的头像,形态学处理,图像旋转(由于摄像头采集到的图像是镜像,需要用cv2.flip将摄像头采集到的图像进行处理),选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。

努力学习前端的小陈

ROS-CV-ImageConverter:将ROS图像转换为OpenCV(cv :: Mat)类型的图像

ROS-CV图像转换器 依存关系 cv_bridge(ROS软件包)

Willis Wang

计算机视觉图像处理-CV2与图像插值算法

用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int

weixin_38697274

基于水平集CV模型的图像分割(Matlab代码)

基于水平集CV模型的图像分割(Matlab代码),全套代码

小宇QAQ

导向滤波(guide_filter)的python实现

导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

zachzhan_g
返回顶部
McGL
博客等级 码龄10年 47粉丝 1,466原创