MySQL容器凌晨OOM?一行Infinity字符串引发的内存惨案
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,那基本可以锁定方向。
第二步看系统内核日志。在宿主机上执行:
如果有类似这样的记录:
那就说明是内核的 OOM killer 选择了 mysqld 进程,把它干掉了。这个阶段别急着改代码,先把服务拉起来,让业务先恢复运行。
1.2 快速止血:先让业务恢复
我的处理顺序是先恢复服务,再找根因。当时容器里跑的 MySQL 配置了 innodb_buffer_pool_size=1G,再加上 2GB 的容器内存限制,正常情况下应该能跑。但我还是先把参数临时调小:
重启容器后,服务恢复正常。但我知道这只能管几个小时,因为如果真有数据或 SQL 层面的问题,内存迟早还会被打满。
果然,三个小时后,报警又来了。这说明问题不在配置文件,而是有某类查询在持续制造内存压力。
2. 根因定位:“一行 infinity”如何引爆内存
2.1 慢日志里的可疑 SQL
服务再次挂掉后,我直接打开 MySQL 慢查询日志,按运行耗时排序,很快找到了一条非常可疑的 SQL:
注意这个 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 容器的内存压力:
其中 memory.events 里的 oom_kill 计数如果持续增长,说明容器确实反复触发了内核回收和被杀进程,这个信息比 docker stats 里瞬时百分比要有用得多。
4. 修复落地:从数据库参数到业务代码
4.1 临时修复:把 MySQL 的“任督二脉”先控制住
查清根因后,我做的第一件事是把排序和临时表相关的内存参数压下来,防止同类查询再次把容器打挂。以下是我在 my.cnf 里临时调整的配置:
参数我都解释一下:
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 万行垃圾数据分批清理掉:
重复执行若干次,直到影响行数为 0。这个操作要放在业务低峰期做,并且提前在测试环境验证。
然后处理写入端的问题。上游服务不能再把 Float.POSITIVE_INFINITY 直接序列化进数据库,我建议在写入前做一轮校验和转换:
- 如果库存上限不存在或为无穷大,统一写成
null,或者显示指定为unlimited标签。 - 如果业务需要保留原始文本,建议改装 JSON 字段,MySQL 8.0 的 JSON 类型有严格的格式校验,凡是
Infinity这种非法 JSON 值根本写不进去,相当于在数据库入口加了一道防火墙。
4.3 根治方案:去掉正则,用可索引字段替代
SQL 层面,最直接的优化是不要对 TEXT 字段做正则匹配。这里有两种做法。
第一种是给表增加一个独立的标签列:
然后业务在写入时维护这个字段,查询时直接:
这个查询可以命中 idx_unlimited 索引,单次执行时间从原来的 8 秒下降到毫秒级。
第二种做法,如果必须保留 detail 里的 JSON 内容,可以加生成列:
需要注意的是,生成列依赖 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软件包)
计算机视觉图像处理-CV2与图像插值算法
用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int
基于水平集CV模型的图像分割(Matlab代码)
基于水平集CV模型的图像分割(Matlab代码),全套代码
导向滤波(guide_filter)的python实现
导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

