MySQL慢查询暴增,凶手竟是“明星功能”失效?

37 阅读10分钟

开篇:这个坑有多深

你有没有遇到过这样让人抓狂的情况,表面上啥都正常,索引完整,数据量也没波动,可慢查询日志突然从每3分钟十几条猛增到两千多条,TPS不断下降,用户投诉一个接一个,反复排查就是找不出问题,执行计划没变化,优化器像旁观者,DBA急得不行。

你可能没想到,这背后或许藏着一个隐蔽的元凶,自适应哈希索引意外失效。

作为InnoDB的一项核心特性,它被称为明星功能,通常可带来40%的性能增益,可一旦发生故障,却能在短短30秒内让数据库彻底瘫痪。 本文就来深入分析这一严重隐患。

现象:为什么慢查询突然爆炸了

你的监控面板里出现了可怕的一幕。3分钟内,慢查询日志从稳定的10条直线飙升到2000条,而且特别扎心的是——你没改任何索引,没改任何SQL,数据量也没暴增。

这时候大多数DBA会做一个标准动作:跑到表上面去看索引,结果是:"索引都在啊!"然后陷入迷茫。 再查一下表结构,再查一下最近的变更记录……什么都没有。这就是这个问题最狡猾的地方,它表现得像是"无中生有"。你可能会想:*那不就是流量突增了吗?*不完全对。流量虽然是触发条件,但真正的黑手隐藏在InnoDB内核深处。

定位:怎么在茫茫大海里找到凶手现在来到最关键的一步——定位根因。标准做法是靠猜或者靠经验,但我们要用数据说话。打开MySQL,查一个地方:information_schema.INNODB_METRICS这个视图。这里面藏着InnoDB的所有秘密。你要重点看一个指标:SELECT SUBSYSTEM, NAME, COUNT FROM INFORMATION_SCHEMA.INNODB_METRICSWHERE NAME = 'adaptive_hash_searches' AND COUNT IS NOT NULL;如果这个指标突然从平时的百万级暴跌到接近0,恭喜你,你找到问题了。adaptive_hash_searches****这个数字代表每秒有多少次命中了自适应哈希索引。 它掉下来意味着什么?意味着本来应该快速命中的查询,现在全部绕过了AHI,打回到B+树里去走老路了。而B+树的遍历成本,简直是折磨。这个时候再看一个伴生指标:btr_search_latch的等待情况。这是啥?这是保护自适应哈希索引的那把大锁。当它的竞争变得疯狂,AHI就会主动"破产"掉。

根因:AHI为什么会突然背刺你自适应哈希索引的工作原理先说好消息:AHI本质上是一个内存中的即时哈希表,专门加速热点查询。具体怎么玩的呢?InnoDB会持续监控B+树索引上的查询行为。当它观察到某个索引的某个页面(page)被频繁访问时,就会自动生成一个哈希表项。这个哈希表的结构很简单:

  • Key:索引的前缀值(不一定是完整的索引键值)
  • Value:这个值在Buffer Pool中对应的具体页面地址

下一次查询来临时,MySQL不用从B+树根节点一路遍历到叶子节点,而是直接用哈希表查一下,30纳秒内就能定位到数据页面,这比B+树遍历快几倍甚至几十倍。这就是为什么有文献说AHI能带来40%的性能提升。但这里有个关键前提:AHI的指针只指向Buffer Pool中已经加载的页面,一旦这个页面被淘汰出Buffer Pool,对应的哈希表项就失效了。所以AHI特别适合"热数据+大Buffer Pool"的场景,对于冷数据频繁轮转的场景就是鸡肋。

锁竞争问题的根源现在说坏消息。InnoDB在8.0.30之前使用了一个全局的哈希表保护锁,叫btr_search_latch。这意味着什么?无论你的AHI被分散在多少个热点索引页面上,所有的查询线程竞争的都是同一把锁。假设你有一个32核的机器,64个并发查询线程全部在高频访问同一张热表的索引。每次查询都需要:

  1. 获取btr_search_latch读锁
  2. 在哈希表中查找
  3. 释放锁

当下来设想一下,64个线程都在那儿排队等候这把锁,就算持有锁的时间仅仅是微秒级别,累积起来的等候链条也会变成毫秒级别乃至秒级别。并且更糟糕的是,当AHI的填充系数过高时,InnoDB会启动AHI重建(分裂)操作,这时候得持有排他锁,所有查询线程都会被阻碍。重建的时候,CPU的表现会相当奇特,表面上CPU使用率挺高,可实际上很多线程在那里空转等待锁,不是真正在工作。

这就是流量突增成为触发条件的原因:流量越大,并发的线程越多,锁竞争的激烈程度就越厉害。 数据会说话:当adaptive_hash_searches从高位暴跌到接近0时,往往不是AHI真的出现了性能问题,而是InnoDB自己判断"与其让所有查询排队等锁,不如直接关掉AHI,让大家走B+树"。这是InnoDB的自我保护机制。验证:一条命令检验真伪现在我用最直接的方式来验证这个假设:直接关掉AHI,看看TPS能不能回升。SET GLOBAL innodb_adaptive_hash_index = OFF;实施这个指令之后,监测的重要指标立刻就会产生改变:

  • 慢查询日志:瞬间掉下来(因为AHI虽然死了,但至少线程不用再争那把锁了)
  • TPS曲线:开始往上爬
  • CPU使用率:可能还会降低(锁竞争的开销没了)

一旦出现这种情况,基本可以确定是AHI在作祟。很多时候,只需关闭它,问题便迎刃而解。关闭之后,环境立刻恢复安静。不过,这只能算临时应对措施,并非最佳解决方案。解决:8.0.30+的正确打开方式官方为什么决定推出分区方案MySQL官方在8.0.30版本发布时,终于承认了这个设计缺陷有多严重。他们意识到一个无可奈何的事实:在高并发场景下,一把全局锁根本扛不住。关掉AHI能解决问题,但相当于放弃了性能收益。有没有办法既要锁的安全性,又要并发性能?答案是:分区化。官方推出了一个救命的参数:innodb_adaptive_hash_index_parts。这个参数的作用是什么?把本来的单个全局大锁,拆成多个独立的小分区锁。分区机制的具体作用设置成64的配置示例:-- 需要在my.cnf中配置,接下来重启[mysqld]innodb_adaptive_hash_index_parts = 64为什么拆分成64个分区就行了?因为多个小锁的竞争压力远低于一把大锁。假设原来64个并发线程都在争一把锁,现在平均分摊到64个锁上,每个锁只承载1个线程的访问,竞争立刻从地狱级降到天堂级。更深层的机制是:每个索引会被哈希分配到某个特定的分区,比如索引A的AHI落在分区1,索引B的AHI落在分区3。这样即使有多个线程在频繁访问不同的热索引,它们操作的也是不同分区的锁,天然就没有竞争。

参数的推荐值官方文档给的默认值是8,但这往往太保守了。根据你的硬件配置:

  • 16核以下:设置成8-16就够了
  • 16-32核:设置成32-64
  • 32核以上:设置成64,甚至128(最大512)

经验法则是:分区数≈你的核心数,或稍微多一点。这样可以充分利用多核,同时避免分区过多导致的哈希表碎片化。重启生效后,你会看到TPS直接反弹到正常水位,甚至更高。因为现在AHI既能干活加速查询,又不会形成杀伤性的锁竞争。这就是"既要又要"的完美解决方案。预防:把AHI监控加进Prometheus解决了十分紧急的问题,还需要做好预防工作。最为关键的是,将AHI的健康情况进行实时监控,并不能等到出现爆炸情况了才去发现。你可以写一个简单的Prometheus exporter脚本,周期性地采集这个指标:rate(mysql_global_status_adaptive_hash_searches[5m])这个指标所指的过去5分钟内AHI命中率的变化趋势。你不要设置两个告警的阈值:

  • 要是命中率忽然从较高水准降到接近0,那就意味着AHI也许要分裂或者已经瓦解,这时要马上发出警报,可能需要人工参与或者自动触发重启。
  • 如果命中率持续很低(比如长期在0.1以下):意味着打开AHI对你的业务基本没帮助,可以考虑关掉它,省点CPU和内存。

同其他指标一起查看,比如btr_search_latch_waits的等待次数,你就可以提早发现问题,甚至在用户发现到之前就主动去调整参数或者启动滚动重启。这样一来,你就从被动的"救火"变成了主动的"防火"。回顾与总结让我们把这个问题的全貌梳理一遍。

问题的根源,在于自适应哈希索引在旧版本中的设计缺陷。 它本意是作为性能提升的手段,但由于采用全局单一锁进行保护,在高并发环境下反而成了性能瓶颈。这把锁的竞争,激烈程度随着并发线程数量的增加而同步上升。一旦线程数急剧增多,锁争用会呈爆炸性增长,最终导致AHI自身无法正常运作。

诊断的方法:不要只看慢查询日志,要看INNODB_METRICS里的adaptive_hash_searches。从数据的陡降,可以直接推导出根本原因。处理问题的办法是,存在应急方案,也就是关闭AHI,以及基本方案,即更新到8.0.30+并且启动分区参数。前面那种是通过牺牲性能来保证稳定,后面这种不但能保留性能还能把题目给解决掉。

预防的策略:把AHI的命中率纳入持续监控,设置合理的告警阈值,让问题无所遁形。

最后的启示:数据库的每一次"无缘无故"的性能下滑,背后都是某个内核机制在起作用。学会看内部指标,学会用数据说话,你就赢了大多数DBA。

声明

本文内容90%为本人原创,少量素材经AI辅助生成,且所有内容均经本人严格复核;图片素材均源自真实素材或AI原创。文章旨在倡导正能量,无低俗不良引导,敬请读者知悉。