• 一次线上慢查询,竟然查出了 MySQL 溢出页问题


    有些线上问题,表面上看只是 SQL 变慢,实际上背后却隐藏着数据库底层存储结构的问题。

    前段时间,我们在生产环境就遇到过一次典型案例:
    一张访问量并不算高的业务表,查询RT(响应时间)却突然飙升,甚至出现了大量 IO 等待。最开始大家都以为是索引失效、慢查询或者buffer pool不够,但最终定位下来,真正的问题居然是 —— InnoDB 溢出页(Overflow Page)

    这篇文章通过真实排查思路,聊聊这个问题是怎么出现的,又是如何一步步被定位出来的。


    线上问题:查询越来越慢,但 SQL 看起来没问题

    业务里有一张订单扩展表,大概结构如下:

    CREATE TABLE order_ext (
        id BIGINT PRIMARY KEY,
        order_id BIGINT,
        ext_info LONGTEXT,
        create_time DATETIME,
        KEY idx_order_id(order_id)
    );
    

    其中 ext_info 用来存储订单扩展信息,包括:

    • 用户提交的 JSON
    • 第三方返回报文
    • 风控字段
    • 审计信息

    上线初期数据量不大,一切正常。

    但随着业务增长,问题开始逐渐暴露:

    • 根据 order_id 查询越来越慢
    • buffer pool命中率下降
    • 磁盘IO持续升高
    • 即使只查少量数据,响应时间依然不稳定

    更奇怪的是:

    SELECT id, order_id 
    FROM order_ext
    WHERE order_id = ?;
    

    这种只查普通字段、不查 LONGTEXT 的 SQL,也开始变慢。

    这就有点反常了。


    第一步排查:索引没问题,执行计划也正常

    首先查看执行计划:

    EXPLAIN SELECT id, order_id
    FROM order_ext
    WHERE order_id = 10001;
    

    结果显示:

    • 命中了二级索引
    • rows很小
    • type = ref

    看起来没任何异常。

    接着查看:

    • 慢日志
    • buffer pool使用情况
    • 磁盘IO
    • undo / redo 状态

    依旧没有发现明显问题。

    直到后来,我们注意到一个现象:

    这张表的单行记录异常大。


    真正的问题:大量字段进入了“溢出页”

    继续分析表结构后发现:

    LONGTEXT
    

    字段里存储了大量 JSON 数据。

    有些记录甚至超过了几十 KB。

    而 InnoDB 的数据页默认只有:

    16KB
    

    也就是说:

    一行数据根本塞不进一个普通数据页。

    这时候,InnoDB 就会启动一种机制:

    溢出页(Overflow Page)


    什么是溢出页?

    简单来说:

    当一行数据太大,普通页放不下时,InnoDB 会把超长字段拆出去,单独存储到其他页中。

    原始数据页里,只保留:

    • 前缀数据
    • 指针信息

    真正的大字段内容,则放在“溢出页”里。

    大概可以理解成这样:

    溢出页


    为什么溢出页会拖慢性能?

    很多人会有一个误区:

    “我又没查询 TEXT 字段,为什么也会慢?”

    原因在于:

    虽然 SQL 没查大字段,但:

    • 行记录本身仍然更大
    • 页能容纳的记录数变少
    • buffer pool缓存效率下降
    • 页分裂概率提升
    • 回表成本增加

    更关键的是:

    某些场景下,InnoDB仍然需要访问溢出页。

    比如:

    • MVCC版本读取
    • 行完整性校验
    • 回表读取
    • 行迁移

    一旦大量随机 IO 打到磁盘,性能就会迅速恶化。


    InnoDB 是如何决定使用溢出页的?

    这部分稍微深入一点。

    InnoDB 并不是所有 TEXT/BLOB 都直接放溢出页。

    它会根据:

    • 行格式(ROW_FORMAT)
    • 字段大小
    • 页剩余空间

    综合决定。

    常见行格式包括:

    • Compact
    • Dynamic
    • Compressed

    其中:

    Compact 行格式

    会在数据页中保留:

    768 字节前缀
    

    剩余部分放到溢出页。

    因此:

    即使字段非常大,主页里仍然会保留部分内容。


    Dynamic 行格式

    这是 MySQL 5.7/8.0 更推荐的方式。

    它会:

    • 尽量只保留 20 字节指针
    • 大字段完整放入溢出页

    这样能让主页更“轻”。

    很多线上库升级后性能改善,其实就和这个有关。


    为什么我们的线上问题越来越严重?

    后来继续分析发现:

    业务表存在几个典型问题。

    1. JSON 数据持续膨胀

    最初:

    {"status":"ok"}
    

    后来逐渐变成:

    {
      "risk": {...},
      "audit": {...},
      "third_response": {...},
      ...
    }
    

    单条数据越来越大。


    2. 热数据和冷大字段混在一起

    实际上:

    业务查询只关心:

    • order_id
    • status
    • create_time

    但每次回表时:

    大字段依然跟着存储在同一行逻辑结构里。

    导致热点数据缓存效率越来越差。


    3. 页利用率急剧下降

    正常情况下:

    16KB 页可以放很多记录。

    但有大量大字段后:

    一个页只能放几条记录。

    buffer pool很快被“低效占用”。


    最终解决方案

    最后,我们用了几个手段解决问题。


    方案一:拆分大字段

    这是最有效的方法。

    把:

    ext_info LONGTEXT
    

    拆到独立表:

    order_ext_detail
    

    主表只保留核心查询字段。

    效果非常明显:

    • buffer pool命中率提升
    • IO明显下降
    • 查询RT恢复正常

    这也是线上最推荐的方案。


    方案二:避免无意义的大 JSON

    很多业务喜欢把所有数据直接塞JSON。

    但实际上:

    • 很多字段不会查询
    • 很多字段不会回看
    • 很多字段只是日志性质

    这类内容:

    • 可以归档
    • 可以压缩
    • 可以放对象存储

    不要全部堆进MySQL。


    方案三:使用 Dynamic 行格式

    检查表:

    SHOW TABLE STATUS LIKE 'order_ext';
    

    如果不是:

    ROW_FORMAT=DYNAMIC
    

    建议调整:

    ALTER TABLE order_ext ROW_FORMAT=DYNAMIC;
    

    这样可以减少主页占用。


    方案四:避免频繁回表

    比如:

    SELECT *
    

    非常容易把大字段一起带出来。

    线上最好:

    • 明确字段列表
    • 使用覆盖索引
    • 避免无意义读取

    否则即使SQL看起来简单,也可能触发大量IO。


    一个容易被忽视的底层细节

    很多MySQL性能问题,本质并不是:

    • SQL写得差
    • 索引没建好

    而是:

    数据组织方式已经不适合当前业务规模。

    尤其在业务快速发展后:

    • JSON
    • TEXT
    • BLOB

    这些字段非常容易成为隐藏炸弹。

    平时数据量小时没感觉。

    一旦数据规模上来:

    • buffer pool
    • IO
    • 页分裂
    • 回表成本

    问题会集中爆发。


    总结

    溢出页本身并不是问题。

    真正的问题是:

    大字段被无节制地放进核心业务表。

    线上排查MySQL性能问题时,如果出现:

    • IO 异常升高
    • 查询越来越慢
    • buffer pool命中率下降
    • 单表数据膨胀明显

    除了索引和SQL之外,也建议关注:

    • 行大小
    • TEXT/BLOB字段
    • ROW_FORMAT
    • Overflow Page使用情况

    很多“查不出原因”的慢查询,最后根因都藏在这里。


  • 相关阅读:
    前端新手Vue3+Vite+Ts+Pinia+Sass项目指北系列文章 —— 第六章 样式格式化 (Sass配置)
    uCOSii系统的中断管理
    不学C语言能直接学C++吗?
    mfc140u.dll丢失的解决方法,以及mfc140u.dll解决方法的优缺点
    什么是单片机最小系统?
    flink时间处理语义
    【毕业设计】基于ESP32的在线墨水屏桌面摆件 -物联网 单片机 嵌入式
    当SCM遇见RPA:实现高效协调的供应链管理
    2022Python最新面试题整理
    RK3399平台开发系列讲解(内核调试篇)spidev_test工具使用
  • 原文地址:https://www.cnblogs.com/cqrocky/p/20063318