MySQL容器反复OOMKilled?一个Infinity参数如何让优化器全表扫描

MySQL容器OOMKilledInfinity参数
于 2026-10-01 06:08:44 修改
·本内容遵循CC 4.0 BY-SA版权协议

先说明一下背景:银河麒麟 V10 上跑 MySQL 容器,业务高峰期容器不定时被杀,状态一会儿 OOMKilled 一会儿 Exited(137),Docker 自动重启后又能撑一阵子。这种"薛定谔的可用"最折磨人。我花了差不多一个下午加一个晚上才把根因捞出来——不是内存配小了,不是连接数打满,也不是慢查询堆积,而是业务 SQL 的绑定参数里有一个 Infinity,一行看起来人畜无害的"无穷大"。

这个排错过程其实很有代表性:容器 OOM 的现场非常容易误判,日志也分散在宿主内核、cgroup、Docker 事件和 MySQL 自身日志四层,少看一眼都会把方向带偏。所以我把完整的排查链路、根因原理和修复加固方案整理出来,给同样在国产系统上跑容器数据库的朋友一个参考。

1. 故障现场:容器反复 OOMKilled,服务间歇性瘫痪

1.1 第一现场:容器状态与重启记录

先说环境。宿主机是银河麒麟 V10 SP2,内核 4.19 系列,Docker 20.10,MySQL 用的是官方 8.0.32 镜像,容器加了 -m 4g(内存上限 4G),数据目录挂载在宿主机,生产环境跑了半年一直挺稳。

出问题那天的现象很典型:监控面板上 MySQL 实例的 RSS 内存曲线像过山车——缓慢爬到接近 4G,然后瞬间跌到零,再过一两分钟又拉起来。业务的报错集中在 Lost connection to MySQL server、CR_SERVER_GONE_ERROR(错误码 2013)、Can't connect to MySQL server(错误码 2003)这几类。

第一件事不是看业务日志,而是看容器本身的状态:

BASH
docker ps -a | grep mysql
docker inspect <container_id> --format '{{.State.Status}} {{.State.OOMKilled}} {{.State.ExitCode}}'

输出基本就是 running true 137,State.OOMKilled 是 true,退出码 137。这两个信号放在一起,几乎可以直接断定:容器的进程是被内核 OOM Killer 杀掉的,而不是 MySQL 自己崩溃退出。注意 137 = 128 + 9,即 SIGKILL,凡是看到 137 先往内存方向想,别急着查 MySQL error log 的 crash 信息。

1.2 最开始的误判方向:以为只是内存配置不足

我承认,第一反应也是"4G 是不是太小了"。因为这个库每天有上千万行交易流水,InnoDB buffer pool 给的是 2G,按常规经验 4G 容器装 MySQL 8.0 确实偏紧。当时差点就直接改 -m 8g 重启完事。

还好没这么干。因为有一个细节不对劲:这个问题是突然出现的,不是一步步恶化。配置没动过、数据量没有明显突增、连接数也没有上涨,如果是单纯的容量不足,内存曲线应该是缓慢爬坡,而不是以某天为分界突然开始被杀。第二个不对劲的地方是:容器被杀的时间窗口集中在每天上午 10 点到 11 点,下午也有小高峰,偏偏凌晨没事。这说明存在一个"特定业务触发"的规律,而容量不足不会挑时间段。

1.3 抓现场:关闭容器自动重启,锁定复现窗口

排查的第一步,我把容器的 --restart=always 给去掉了。理由很简单:OOM 之后自动重启会把现场覆盖掉,尤其是容器里的 /proc、内核日志、临时文件全都恢复正常,你再回头看就是一个"健康"的 MySQL,什么痕迹都找不到。

改成手动启动之后,第二天上午 10 点 20 分左右,容器再次被打死。这次我不急着拉起来,先让现场保持"尸体"状态,然后从上到下逐层取证。过程中注意一点:排查期间建议把业务流量摘掉或切到只读备份,避免反复 OOM 造成数据页损坏。MySQL 8.0 在崩溃恢复上做得不错,但没必要冒这个险。

2. 排查链路:从内核日志到慢日志的层层递进

2.1 dmesg 与系统日志:OOM Killer 留下的痕迹

第一层证据在宿主机内核日志里。容器被杀的瞬间,内核会留下非常明确的 OOM 记录:

BASH
dmesg -T | grep -i -E "oom|killed process" | tail -50

符合预期的输出大概是这样的(时间、进程号已脱敏):

TEXT
[2024-06-11 10:21:37] mysqld invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
[2024-06-11 10:21:37] Memory cgroup out of memory: Killed process 31441 (mysqld) total-vm:8234567kB, anon-rss:3678921kB, file-rss:102234kB, shmem-rss:8876kB, UID:999 pgs:1

两个关键信息点:

  • Memory cgroup out of memory 说明触发点是 cgroup 内存限制,而不是宿主机物理内存耗尽。所以你看宿主机 free -h 可能还有十几个 G 空闲,这很正常,不要被误导。
  • total-vm:8.2G 与 anon-rss:3.6G 之间的差距也值得注意。total-vm 是进程虚拟内存映射,很大很唬人,但真正算进 cgroup 的是 anon-rss(匿名内存)和一部分 page cache。也就是说进程实际只用了 3.6G,但容器的 4G 上限里还有其他开销(mysqld 之外还有 init、慢日志缓冲、Docker 层),所以在边界上被挤死了。

麒麟 V10 上还可以追加确认:

BASH
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.oom_control

memory.oom_control 里的 oom_kill_disable 是 0 表示允许 OOM Killer 动手,under_oom 在故障瞬间会变成 1。这一层能让你确认"确实是容器被限流了",但还回答不了"为什么突然吃这么多内存"。

2.2 MySQL 侧日志:什么在吃内存

第二层证据在 MySQL 的 error log 和慢日志里。先看 error log:

BASH
docker logs <container_id> 2>&1 | tail -100

MySQL 8.0 在崩溃前后一般会留下类似这样的片段:

TEXT
[Warning] [MY-013602] InnoDB: Using a single tablepace...
[ERROR] [MY-013183] InnoDB: Out of memory! (needed X bytes)
...
[Note] InnoDB: 1 second(s) of flushing completed, max consecutive flushes: 0

有 Out of memory 关键字,可惜它并不是根因,只是结果——内存不够了,InnoDB 想做啥都做不成。不过这里有个经验:不要因为看到 Out of memory 就停止排查,MySQL 的错误信息只告诉你"内存不够用了",没告诉你"谁用掉的"。

接着查慢日志。因为 MySQL 是容器跑的,慢日志大概率也是写到挂载目录或 stdout。我们在 my.cnf 里确认过 slow_query_log=ON、long_query_time=2、log_queries_not_using_indexes=ON,所以理论上超过 2 秒的查询都会留痕。结果一翻,发现问题了:故障时段有好几条大查询,每一条跑了 200 到 400 秒不等,Rows_examined 全是几百万甚至上千万。

但这些查询的 SQL 文本看起来并不复杂,也没有明显的笛卡尔积,甚至条件里还有金额范围、商户 ID、日期。这引起我的警惕——SQL 文本正常并不代表执行计划正常。

2.3 EXPLAIN 的意外发现:一行 infinity 的"威力"

从慢日志里捞出一条最典型的查询(已脱敏):

SQL
SELECT
merchant_name,
channel_code,
pay_type,
COUNT(*) AS txn_cnt,
SUM(txn_amt) AS txn_sum,
MAX(txn_time) AS last_time
FROM txn_record
WHERE settle_date = '2024-06-11'
AND merchant_id IN (200188, 200189, 200190, 200191, 200192)
AND txn_amt <= ?
GROUP BY merchant_name, channel_code, pay_type
ORDER BY merchant_name;

按照平时经验,这个 SQL 只需要走 (merchant_id, settle_date, txn_amt) 联合索引做范围扫描,金额条件能过滤掉绝大多数行。但实际执行计划让我大跌眼镜:

SQL
EXPLAIN FORMAT=TREE
SELECT ... 同上 ...;

输出关键片段:

TEXT
- > Sort: txn_record.merchant_name
-> Table scan on txn_record (cost=1837261.80 rows=19463820)
-> Pseudo index 'Group index' on temporary table

注意到没有:possible_keys 里有索引,但优化器一个都没用,直接 Table scan on txn_record,扫了 1900 多万行。我当时的第一反应是统计信息过期了,但 ANALYZE TABLE 跑完再 EXPLAIN 依旧如此。直到我通过 performance_schema.events_statements_history_long 把这条语句的绑定参数抓出来,才发现问题:

SQL
SELECT THREAD_ID, SQL_TEXT, NORMALIZED_STATEMENT,
STATEMENT_ID, EXECUTION_ENGINE,
(SELECT GROUP_CONCAT(CAST(ARG? AS CHAR)) FROM ... ) AS param_values
FROM performance_schema.events_statements_history_long
WHERE SQL_TEXT LIKE 'SELECT ... %txn_amt <= ?%'
ORDER BY START_TIME DESC LIMIT 10;

参数显示:

TEXT
param_value: inf

那一刻,所有线索都接上了。txn_amt <= inf 这个条件等价于 txn_amt <= 无穷大,对任何数值都成立。优化器的条件过滤性评估直接给了 100%(恒真),于是它思考的路径变成了"反正金额条件过滤不掉任何行,不如全表扫,然后用 GROUP BY 建临时表去分组"。执行计划就从"范围扫描 + 快速聚合"退化成"全表扫描 + 临时表 + 排序",Rows_examined 翻了百倍不止。

3. 根因深挖:为什么一个 infinity 能让优化器"发疯"

3.1 先弄清楚 infinity 是怎么进到 MySQL 的

先说这个 inf 是从哪儿来的。顺着业务代码查,发现是前端做"交易金额区间筛选"时,最大金额输入框留空,业务后端为了图省事,在拼查询条件时把这个空值转换成了 float('inf')(Python)或者说 Double.POSITIVE_INFINITY(Java),然后通过 ORM 绑定参数传给了 MySQL。

IEEE 754 双精度浮点数是支持 +inf、-inf、NaN 这三类特殊值的,MySQL 的 DOUBLE 类型也原生兼容它。你用下面这条语句就能复现:

SQL
SELECT 1e999 AS v, 1e999 > 0 AS cmp;

输出:

TEXT
v cmp
inf 1

MySQL 在解析 1e999 时发现超出 DOUBLE 表示范围,不直接报错,而是向上溢出成 inf。这就是问题能落地的机制:MySQL 的 DOUBLE 可以存 infinity,比较运算也遵循 IEEE 754 语义。所以 txn_amt <= inf 在 MySQL 眼里就是一个恒真条件,没有任何过滤性。

3.2 优化器对恒真条件的处理逻辑

MySQL 优化器有一个"条件化简"阶段,职责之一是把逻辑上无意义的谓词识别出来并移除。常量化简(constant condition)会做这几件事:

  • WHERE 1=1 直接移除;
  • WHERE col IS NOT NULL AND col IS NULL 识别为永远为假(impossible WHERE),直接返回空结果;
  • WHERE col <= MAX_DOUBLE_OR_INF 这类条件,如果值是常量且包含 inf,会被认为恒真。

但这里有个容易忽略的细节:它只是移除了条件本身,并没有重新评估这个条件原本能提供的"索引可用性"。正常情况下 txn_amt <= 具体值 可以走 idx_merchant_date_amt 做范围扫描,过滤出几千行;一旦改成 <= inf,条件被移除后,索引里那个"金额范围"元素就失去了匹配机会,优化器能依赖的只有 merchant_id 和 settle_date。而 IN 列表配合日期字段,在优化器看来过滤度远远不够,它最终选择全表扫描。

更隐蔽的问题是 GROUP BY。原 SQL 里带了 GROUP BY merchant_name, channel_code, pay_type,在全表扫描的基础上,这个 GROUP BY 无法利用索引做有序分组(因为这些字段不构成索引的最左前缀),于是 MySQL 必须建内部临时表。1900 万行数据要一一映射到分组桶里,内存临时表立刻膨胀。

3.3 内存临时表、排序缓冲与 cgroup 上限的关系

接下来是内存为什么会被打爆。MySQL 8.0 的内存临时表默认使用 Temptable 引擎,相关参数有 tmp_table_size(默认 16MB)、max_heap_table_size(默认 16MB)、temptable_max_ram(默认 1GB)、temptable_max_mmap(默认 1GB)。

很多人以为超过 tmp_table_size 就会把临时表写到磁盘,内存占用就降下来了。这只说对了一半。Temptable 引擎在超过 temptable_max_ram 之后,会优先把部分数据映射到内存映射文件(mmap),mmap 占用的匿名映射空间照样计入容器的 cgroup 内存,而且它不会立刻归还给内核。只有在超过 temptable_max_mmap 之后,才真正落盘到 tmpdir 的物理文件。

换句话说,一个大查询在 GROUP BY 阶段,可以合法地吃掉 1G 的 RAM 内存 + 1G 的 mmap 映射空间,而 MySQL 并不会觉得有问题。再加上排序用的 sort_buffer_size(如果是多路归并,每个归并轮次都要额外分配内存)、连接线程自身的栈空间,单条查询撑到 1.5G 到 2G 是很现实的事。

但真正致命的还不是单条查询,是并发放大。故障时段恰好是业务报表导出高峰,同一个接口被调用了几次,等于好几条这样的大查询同时进入执行阶段。MySQL 的线程内存是各自独立的:A 查询在跑全表扫描,B 查询在建临时表,C 查询在做排序,D 查询在等磁盘 IO,它们内存互不共享。最后 4G 容器的内存账本大致是:

内存去向 估算占用
InnoDB Buffer Pool(固定 2G) 2048MB
连接线程栈(默认 1MB × 当前活跃连接) 150MB 左右
大查询临时表 + mmap 每条 500MB ~ 1.2G
排序缓冲 每条 64MB ~ 256MB
其他(日志、缓存、Docker 层开销) 100MB+

一到高峰,两三条大查询同时存活,4G 直接见底,cgroup OOM Killer 开枪。注意这里我没有提到 max_connections=200 带来的静态内存,因为 MySQL 8.0 的连接线程是相对轻量的,真正的重头戏从来都是 buffer pool 和大查询的临时结构。

3.4 还有一类隐藏问题:函数计算与类型转换

排查中我还顺手验证了另一个疑惑——如果绑定参数是字符串 "Infinity" 而不是 IEEE 754 的 inf,情况会不会不一样?

实测结论是更糟。当应用层传的是字符串 "Infinity",MySQL 在绑定参数阶段会做隐式类型转换,把字符串转成 DOUBLE。"Infinity" 转完之后恰好也是 inf,但这次它还会往错误日志里写一条 Warning (Code 1265): Data truncated for column 或者 Incorrect DOUBLE value: 'Infinity',具体取决于版本和 sql_mode。

更麻烦的是,如果这个字段本身是 VARCHAR,而 SQL 里写的是 txn_amt <= ?,字段类型和参数类型不匹配,MySQL 会选择把字段转成 DOUBLE 再比较。这意味着 txn_amt 列上即使有索引,也因为函数/类型转换而完全失效,连"范围扫描"的念想都没了。这种场景下,一条 inf 参数不只是让条件恒真,还顺手把索引废了,两条路都堵死。

4. 修复与加固:从业务代码到数据库到容器的三层处置

4.1 业务侧:入参校验把 infinity 挡在门外

根因已经确认,第一刀必须砍在业务代码上。这是所有修复里成本最低、收益最直接的。

业务侧的做法应该是:查询条件里的"不限上限"不要用无穷大来占位,而是动态拼 SQL。原本:

PYTHON
# 不推荐的写法
if amount_max is None:
amount_max = float('inf')
sql = "SELECT ... WHERE txn_amt <= %s"

改成:

PYTHON
# 正确的写法:不传上限时直接不拼这个条件
conditions = ["settle_date = %s", "merchant_id IN (...)"]
params = [settle_date, merchant_ids]
if amount_max is not None:
conditions.append("txn_amt <= %s")
params.append(amount_max)
sql = "SELECT ... WHERE " + " AND ".join(conditions)

核心原则一句话:用什么值表达"没有限制",都不如直接让这个条件不存在。因为优化器对"条件本身"和"条件恒真"的代价评估并不是等价的,与其让优化器去化简一个假条件,不如从源头不生成它。

如果因为框架限制确实无法动态拼 SQL,那就必须做入参白名单校验:当 amount_max 是 inf、NaN、1e999、或者超过 9.9E15 这种明显不合理的值时,直接抛参数异常或者改写为 txn_amt >= 0。千万别把荒谬值传给数据库再期待它"聪明地"处理。

4.2 SQL 侧:改写查询与索引调整

业务代码改完需要回归测试,但数据库侧不能只等业务上线。我这边同时做了两层兜底。

第一层:把同类报表查询改成有界查询。报表接口本来就该带分页或 LIMIT,至少在应用层强制最终的返回行数。如果业务上确实要全量导出,那也得分批跑(比如按 5 分钟一个时间片循环拉取),而不是一次性 GROUP BY 上千万行。

第二层:调整索引设计,让优化器在"金额条件恒真"的情况下也不至于选择全表扫描。(merchant_id, settle_date, txn_amt) 这个联合索引对当前 SQL 是有效的,但它有个盲区:当 txn_amt 条件被剥离后,剩下的过滤条件只有 merchant_id IN (5个值) 和 settle_date = 一天,优化器认为单商户单日的数据量可达几十万,不足以让它放弃全表扫描。

所以我把索引改成了 (settle_date, merchant_id),让日期作为最左前缀。这样即便金额条件被移除,优化器仍然可以通过 settle_date = '2024-06-11' AND merchant_id IN (...) 拿到一个相对窄的范围,Rows 从 1900 万降到接近百万级,配合 GROUP BY 还是能接受。如果后续业务继续扩大,还可以考虑把大商户单独拆分区表或汇总表,这属于架构层面的优化,这次先不急。

执行计划对比:

场景 索引使用 扫描行数 执行耗时
正常金额值(如 500000) idx_merchant_date_amt 范围扫描 约 3000 行 0.2s
inf 参数 + 原索引 无(Table scan) 1946 万行 386s
inf 参数 + 新索引 idx_date_merchant 范围扫描 约 96 万行 12s
业务修复后(无金额条件) idx_date_merchant 范围扫描 约 96 万行 11s

即使业务侧修复没完全上线,数据库侧的新索引也能把最坏情况从"打爆内存"降级为"慢但可用",这是可接受的兜底。

4.3 容器与实例侧:内存水位控制

业务和 SQL 都改完后,容器侧也做了调整。这里不是简单地把 -m 4g 改成 8g 就完事,而是重新核算了内存预算。

MySQL 容器内内存分配,我通常用这个粗略公式:

TEXT
容器内存 = innodb_buffer_pool_size
+ 排序/临时表/连接缓冲(每个活跃连接按 10MB~20MB 估算,再乘最大并发)
+ InnoDB 日志和其他开销(200MB~500MB)
+ 容器运行时和系统缓存(100MB~300MB)

原来的问题是 innodb_buffer_pool_size=2G 占了 4G 容器的一半,余量看似有 2G,但大查询的临时表按并发叠起来很容易突破。调整后:

INI
[mysqld]
innodb_buffer_pool_size = 1536M
innodb_log_buffer_size = 32M
tmp_table_size = 96M
max_heap_table_size = 96M
sort_buffer_size = 4M
join_buffer_size = 4M
max_connections = 300
max_execution_time = 60000

几个参数变动的理由说一下:

  • tmp_table_size 和 max_heap_table_size 从 16M 提到 96M,表面看是放宽了,实际上是为了让中小查询能在内存里完成聚合,避免频繁落盘导致 IO 抖动。真正防爆的是 max_execution_time,它让超过 60 秒的查询直接报错,避免大查询常驻。
  • sort_buffer_size 和 join_buffer_size 保持小值,这是 per-thread 内存,宁可多一次磁盘归并也不让它在内存里积压太多,并发场景下必须保守。
  • innodb_buffer_pool_size 降到 1.5G,牺牲一点缓存命中率,换取容器内存水位更健康。

容器启动参数也做了微调:

BASH
docker run -d \
--name mysql \
--memory=6g \
--memory-swap=6g \
--oom-kill-disable=false \
--restart=on-failure:3 \
-p 3306:3306 \
-v /data/mysql:/var/lib/mysql \
mysql:8.0.32

注意这里有三个细节:

  1. --memory-swap=6g 和 --memory=6g 相等,等于禁用了 swap,避免 MySQL 进程被换到 swap 后性能横向崩塌。容器内开了 swap 反而会让问题从"OOM 快速暴露"变成"卡死慢响应",后者更难排查。
  2. --oom-kill-disable=false,保持内核能杀进程。OOM Kill 虽然粗暴,但至少可用性还能通过重启恢复;如果把 OOM Kill 禁掉,MySQL 会直接 hang 住,业务连不上、kill 不掉,更麻烦。
  3. restart=on-failure:3,限制最大重启次数,防止无限重启把问题掩盖。排查期最好还是手动启动。

4.4 验证:压测与复现测试

修完之后必须做验证,而且不能只靠"看起来好了"。我的做法是三步:

第一步,构造复现 SQL。用之前的 inf 参数重新执行一次,确认在新索引下执行计划走的是 idx_date_merchant,Rows 明显下降,且 max_execution_time 生效后超过 60 秒直接被拒掉。

SQL
EXPLAIN ANALYZE
SELECT merchant_name, channel_code, pay_type, COUNT(*), SUM(txn_amt), MAX(txn_time)
FROM txn_record
WHERE settle_date = '2024-06-11'
AND merchant_id IN (200188, 200189, 200190, 200191, 200192)
AND txn_amt <= 1e999
GROUP BY merchant_name, channel_code, pay_type
ORDER BY merchant_name;

计划输出里 actual time 应该在百毫秒到秒级,并且没有 Table scan on txn_record 这种字样。

第二步,压测。用一个简单的并发脚本模拟 20 个线程同时执行上面的查询,观察容器内存曲线的波峰。修完后峰值稳定在 4G 以内,不再触发 OOM。

第三步,观察一个完整的业务高峰周期(至少 24 小时)。确认没有新的 OOM 记录,慢日志里大查询数量明显下降,业务报错消失。这一步不能省,很多问题在低峰期不显形,必须跨一个业务周期才算数。

5. 复盘总结:这类内存异常以后怎么快速定位

5.1 监控指标清单:全链路要看哪些点

这次排错最大的教训是"容器 OOM 不能只盯着容器看",必须把宿主内核、cgroup、Docker 事件、MySQL 日志、慢日志、执行计划、绑定参数串成一条链路。我列一张清单,下次遇到类似问题按顺序查:

层级 查看内容 关键命令/工具
Docker 层 容器状态、OOMKilled、ExitCode、重启次数 docker inspect、docker events --since 30m
宿主内核 OOM Killer 记录、cgroup 内存使用 dmesg -T | grep -i oom、/sys/fs/cgroup/memory/...
系统层 物理内存、swap、磁盘 IO free -h、iostat -x 1、top
MySQL error log 崩溃前最后的错误/警告 docker logs、<datadir>/*.err
慢日志 大查询、Rows_examined、扫描行数 mysqldumpslow -s t
执行计划 是否走索引、扫描行数、临时表 EXPLAIN FORMAT=TREE、EXPLAIN ANALYZE
参数绑定 是不是传了 inf、NaN、超大值 performance_schema.events_statements_history_long、general_log
业务代码 查询条件的默认值、"不限"的占位方式 代码审查、接口入参日志

这套链路里最容易漏掉的是倒数第二个——绑定参数。慢日志里看到的 SQL 文本通常是 txn_amt <= ? 这种参数化形式,? 的具体值不会直接显示。如果不知道从 performance_schema 或者 general log 里把参数值抓出来,你很可能在"为什么走了全表扫描"这一步卡很久。

5.2 排查 OOM 的黄金命令组合

把这次实际用到的命令整理一下,方便直接复制:

BASH
# 1. 查看容器状态和 OOM 标记
docker inspect <cid> --format '{{.State.Status}} OOM={{.State.OOMKilled}} Exit={{.State.ExitCode}}'
 
# 2. 查看容器内存 cgroup 限制和使用
cat /sys/fs/cgroup/memory/docker/<cid>/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/docker/<cid>/memory.max_usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<cid>/memory.oom_control
 
# 3. 查看内核 OOM 记录
dmesg -T | grep -i -E "oom|killed process" | tail -50
 
# 4. 开 MySQL 慢日志(临时,定位后记得关)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;
SET GLOBAL log_queries_not_using_indexes = ON;
 
# 5. 从 performance_schema 抓绑定参数
SELECT THREAD_ID, SQL_TEXT, START_TIME, TIMER_WAIT / 1e12 AS secs
FROM performance_schema.events_statements_history_long
ORDER BY START_TIME DESC LIMIT 20;

在 MySQL 8.0 里,如果想让绑定参数直接显示在 general log 中,也可以临时开 general_log,但生产环境不建议长时间开,因为日志量会非常大,只适合在定位高峰期临时开几分钟。

5.3 从这次故障里总结的几条规范

最后说几条踩完坑之后的沉淀,未必多高深,但每一条都是真金白银换的:

第一,数据库查询条件永远不要用 inf / NaN 这类非业务值做占位。如果你的代码里出现 float('inf')、Double.POSITIVE_INFINITY、1e999、"Infinity" 这类东西,基本可以认定为设计味道不好。表达"不限"的正确方式就是动态 SQL 里不出现该条件,或者数据库层面对这类值做等价改写。

第二,大查询一定要有兜底手段。max_execution_time、LIMIT、分页、批处理,至少占一样。一个没有时间限制、没有行数限制、还带 GROUP BY + ORDER BY 的报表查询,本质上就是一颗定时炸弹,遇到数据量波动或者参数异常就会爆。

第三,容器内存规划要考虑"最坏并发"而不是"日常均摊"。我见过很多 MySQL 容器,buffer pool 给得很大,余量算得很极限,结果一遇到大查询并发就 OOM。buffer pool 再大,命中率最多帮你省磁盘 IO,给不了你大查询的临时表那么大的内存缺口。宁可 buffer pool 小一点,也要给临时表、排序、连接缓冲留出可呼吸的空间。

第四,在国产操作系统(麒麟 V10 这类)上排查容器问题,别默认所有路径都和 CentOS 完全一致。比如某些工具、内核参数的可调项、/sys/fs/cgroup 的布局可能不同,dmesg 的权限要求也可能更严格。排查时优先用 Docker 自身提供的信息(docker inspect、docker stats)和 MySQL 日志,少依赖宿主机上的第三方工具,这样能少踩很多环境差异的坑。

最后再说一个实际操作中的细节:排查这种不定时 OOM,我强烈建议先把容器的 restart 策略关掉,并且在监控面板上把"容器重启次数"作为一个独立告警项。自动重启是可用性的保护伞,也是根因排查的隐形敌人。只有当容器死在当场、现场完整保留时,你才有机会沿着日志一路挖到那个看起来人畜无害的 inf。

MySQL 优化器深度解析:回表 vs 全表扫描 vs Skip Scan,范围查询为什么经常改变结局
本文深入剖析MySQL优化器在范围查询下的决策逻辑,重点对比回表、全表扫描与Skip Scan三种访问路径的成本权衡。核心指出:优化器依据总代价而非是否使用索引做选择;范围查询导致索引从精确定位退化为区间扫描,削弱后缀列的定界、过滤及保序能力;覆盖索引可显著降低成本,而回表、多轮Skip Scan和宽范围则大幅抬升成本,易触发全表扫描。强调应基于cost model理性分析执行计划。
BackendLab
399
会引起全表扫描的几种SQL 以及sql优化 (转)
本文探讨了多种SQL查询优化策略,包括避免全表扫描、优化模糊查询、改进OR语句使用、减少IN和NOT IN的使用等,旨在提升数据库查询效率。
FFITD
1089
小表加索引反而更慢?MySQL优化器代价模型深度解析
本文深入剖析MySQL优化器在小表场景下选择全表扫描或索引扫描的代价计算逻辑,重点对比IO与CPU成本构成,揭示回表开销、过滤比例、页数估算对执行计划的影响,并结合EXPLAIN和optimizer_trace实操验证。核心聚焦索引是否生效的本质原因,而非表大小本身。
苏黎世贝勒爷
196
索引失效避坑: 明明是等值查询,为何EXPLAIN显示走了全表扫描?
本文系统梳理MySQL中索引失效的12种典型场景,涵盖SQL写法(如函数操作、隐式类型转换、前导模糊查询、OR条件滥用、最左前缀违规)、优化器决策(回表成本高、统计信息不准、小数据量)及索引设计缺陷(低区分度、列过长)。重点结合EXPLAIN的type字段与诊断步骤,指导开发者精准识别与修复索引未命中问题。
Seal^_^
1485
MySQL全表扫描生命周期解析:从触发到收尾的完整路径
本文深入剖析MySQL全表扫描的完整生命周期,涵盖触发场景(如无索引、隐式转换、函数包裹等)、优化器基于IO/CPU成本模型的决策逻辑、InnoDB执行阶段的预读机制与MVCC快照读、以及收尾阶段的排序、临时表和结果返回。重点聚焦性能瓶颈根源与可落地的优化策略,适用于慢查询调优与数据库内核理解。
抹茶柚子冰
283
反向存储大法:破解LIKE后缀模糊查询导致的索引失效与全表扫描
本文详解如何通过反向存储(即新增反转字符串列并建索引)将低效的LIKE后缀模糊查询(如'%abc')转化为高效的前缀查询(如'cba%'),从而避免全表扫描、重用B+树索引。内容涵盖原理(索引有序性与匹配区间关系)、落地步骤(加列、回填、建索引、SQL改写)、关键避坑点(MySQL REVERSE中文乱码、函数误加在列上、隐式类型转换)及适用场景判断,适用于订单号尾号、手机号后四位等高区分度后缀查询场景。
Forest Hu
352
从全表扫描到覆盖索引:一次SQL调优的完整复盘
山峰哥
521
MySQL IN和NOT IN的效率对比,该用哪一个
本文深入剖析MySQL中IN和NOT IN操作符的执行逻辑差异:IN通过正向查找可高效利用索引并支持优化器转为哈希连接;NOT IN则因求差集特性易触发全表扫描,且对NULL极度敏感,可能导致查询结果为空。推荐用NOT EXISTS替代NOT IN,因其规避NULL风险、支持早期退出及反半连接优化。
それども
408
索引策略实战:从全表扫描到高效检索的进阶之路
本文深入剖析MySQL InnoDB中B+树索引原理,揭示索引设计常见误区,提出5类生产环境可落地的索引策略(如联合索引设计、覆盖索引、前缀索引、低区分度字段组合、大表无锁加索引),并通过Explain执行计划对比直观展示优化效果,并建立索引生命周期管理规范,涵盖创建评审、冗余清理、灰度上线与回滚机制。
山峰哥
400
MySQL索引优化实战:从全表扫描3.2秒到毫秒级查询
本文系统讲解MySQL索引底层原理与实战优化方法,涵盖B+树结构、聚簇索引与二级索引、回表与覆盖索引、复合索引设计及最左前缀原则,并深入分析索引失效的八大典型场景。通过EXPLAIN完整排查链路,结合真实案例(80万行订单表查询从3.2秒降至40毫秒),提供索引选型、冗余清理、ICP优化等生产级建议,聚焦提升查询性能与避免常见陷阱。
第三世界的妖孽
221
21、MySQL查询优化器:原理与实践
本文围绕MySQL查询优化器展开,介绍了索引类型及使用场景、无法使用索引的情况。阐述了利用慢查询日志、查询优化器工作原理和目标。还给出帮助优化器利用索引的方法,分析了索引使用情况、多表连接时索引影响及优化器选择的特殊情况,并给出优化建议。
40
什么是MySQL的执行计划(Explain关键字)?
本文详细介绍了MySQL的Explain命令,用于分析SQL语句的执行计划,包括id、select_type、table、type、possible_keys、key、key_len、ref、rows和Extra列的含义。通过Explain可以找出查询性能瓶颈,例如全表扫描、索引使用不当等问题。文章还给出了多个示例,展示了如何根据Explain结果进行SQL优化,包括考虑索引覆盖、避免Usingtemporary和Usingfilesort等。
程序员小灰
1255
MySQL单表亿级数据查询优化:从全表扫描到毫秒级响应的实战指南
数据库性能优化是后端工程中的核心议题,当单表数据量达到亿级,查询响应时间往往会从毫秒级恶化到秒级,甚至拖垮整个业务链路。其根源通常不在于数据量本身,而在于SQL执行计划是否充分利用了B+树索引结构——全表扫描、随机IO回表、深分页偏移以及无法利用索引的排序,都是最常见的性能瓶颈。通过慢查询日志定位问题SQL,结合EXPLAIN解析type、rows、Extra等关键信号,并运用联合索引设计、覆盖索引消除回表、延迟关联与游标分页优化深分页等工程实践,即使在单表过亿的订单场景下,也能将接口耗时从2.8秒稳定降至
WGH100817
297
DBA老鸟才知道的MySQL性能优化冷技巧:从索引设计到Linux内核参数调优
本文聚焦MySQL高性能调优,涵盖索引设计(基数估算、跳跃扫描、不可见/降序索引)、查询优化器友好SQL编写(避免函数滥用、子查询改写、JOIN顺序控制)、InnoDB引擎层调优(Buffer Pool多实例与预热、Redo Log容量与脏页刷新策略)以及Linux系统层协同优化(I/O调度器选择、vm.swappiness调优、NUMA绑定与CPU亲和性)。所有技巧均面向高并发、大数据量生产场景,强调原理理解与实证验证。
931
MySQL之orderby详解
本文详细介绍了MySQL的SQL执行流程,包括分析器、优化器和执行器的角色。在涉及`ORDER BY`操作时,讲解了如何利用索引避免全表扫描和外部排序,以及内存排序和磁盘排序的策略。还提到了覆盖索引和RowID排序的优化技巧,强调了在数据量大时如何平衡时间和空间成本。
张孟浩_jay
11453
MySQL join大小表顺序真相:优化器如何决定驱动表与执行计划
本文深入解析MySQL优化器如何决定JOIN驱动表,指出SQL书写顺序不等于执行顺序,尤其在INNER JOIN中优化器基于代价模型重排连接顺序;LEFT JOIN则受语义约束,左表固定为驱动表。核心影响因素包括统计信息准确性、连接列索引存在性、过滤条件下推效果及MySQL 8.0引入的Hash Join机制。实测表明:索引对被驱动表的性能影响远超表顺序调整,EXPLAIN是定位驱动表与优化路径的关键工具。
黄泓毅
257
MySQL查询优化
本文详细介绍了MySQL的基本架构,包括连接器、查询缓存、分析器、优化器和执行器的工作原理。重点关注了查询缓存的使用场景和优化器如何决定查询执行方式。此外,还探讨了查询性能优化,如减少数据访问、重构查询方式,以及如何通过分析响应时间、扫描行数和返回行数来优化查询。最后,提出了将复杂查询拆分为多个简单查询、避免全表扫描和利用索引覆盖等策略来提升查询效率。
苏黔沫
220
mysql where group by 顺序_什么是MySQL的执行计划(Explain关键字)?
本文详细解析了MySQL的Explain命令,用于分析SQL查询的执行计划,包括id、select_type、table、type、possible_keys、key、key_len、ref、rows和Extra等列的含义。通过对Explain结果的解读,可以识别查询性能瓶颈,优化索引使用,提高查询效率。文章还介绍了全表扫描、索引覆盖、临时表和文件排序等概念,并提供了多个示例来展示不同查询类型的执行情况。
谈国平
402
mysql查询执行计划不更新如何处理_执行analyze table更新统计信息
MySQL查询执行计划未及时更新源于优化器依赖的统计信息非实时刷新,导致索引选择错误或全表扫描。需通过ANALYZE TABLE命令重采样索引页,更新innodb_table_stats等系统表中的基数、行数等元数据。该操作在InnoDB下为轻量只读锁,不影响DML,但对大表耗时显著。配合持久化统计(innodb_stats_persistent=ON)及MySQL 8.0+直方图可提升准确性。
m0_71643007
211