Part1 背景

InnoDB的自适应哈希索引(Adpative Hash Index,以下简称AHI),是一种建立在B树索引结构上的索引结构,目的是为了进一步降低BTree的查询代价。

在B树中搜索一个记录时,需要从根节点下降到叶子结点,同时在每个节点中还需要使用二分查找定位。而AHI对此的改进在于它对BTree索引频繁访问的叶子的行记录建立哈希索引,这样在执行B树查询时,通过AHI就可能能定位到叶子结点上的记录位置,避免B树根节点到叶子结点的下降过程,减少了CPU开销。

由于AHI的构建是一个自适应且动态的过程,需要根据查询负载访问模式的变更、页面的换入和淘汰等情况做AHI对应清理或者重建,所以本质上来说AHI也是一个cache,具体的构建逻辑网络上也有很多文章讲解,不是本文讨论的重点。

本文要讨论的是一个鲜为人知的AHI构建锁冲突问题以及相应优化。

Part2 问题

TXSQL 5.7版本在跑sysbench时,我们观察到一个非常有意思的现象。

实验环境是这样的,2台96-core的机器,分别作为sysbench client和mysql server,我们配置buffer pool大小为200GB,同时生成一张120GB的sysbench table。

如下图所示,我们执行128并发的oltp_read_only负载时,观察到QPS首先有一个上升的坡,这段时间我们发现系统有大量的读IO,正在填充buffer pool,属于正常状态。

然后过了100s时突然出现了一个急剧的下降,在400s后开始系统QPS开始缓慢上升,直到800s后达到峰值。

Part1 背景-LMLPHP

通过perf工具抓取系统在QPS剧降时间点的状态,结果如下图:

Part1 背景-LMLPHP

分析堆栈,可以发现,大量CPU花费在在AHI的hash table的锁竞争上。

仔细分析不难发现,这个时候大多数页面基本上还没有建立AHI,然后多个线程同时需要对页面建立AHI索引,而这个构建过程需要对同一个AHI hash table加X锁,因此造成了大量等待。

从QPS变化的角度,可以有如下图所示的分析:

Part1 背景-LMLPHP

Part3 优化

我们注意到,对于一个BTree索引来说,其AHI构建是在BTree叶子结点定位完毕后发生的,对应调用链如下:

btr_cur_search_to_nth_level→ btr_search_info_update→ btr_search_info_update_slow→ btr_search_build_page_hash_index

在btr_search_info_update_slow中,根据统计信息作出决定,调用btr_search_build_page_hash_index把当前页面的记录加入AHI的hash table,这个过程需要独占hash table的X锁。

既然只能有一个线程对hash table进行修改,那么其他并发构建AHI线程等待这个hash table的X锁是相当不明智的,因为这样block住了查询的关键路径,同时只有一个线程在做这个构建工作。

同时我们又注意到AHI只是一个辅助cache,其实用BTree也是能够正确处理查询的。

那么很自然的,我们可以想到如下的优化方式:

  1. 当我们在BTree查询路径上经过分析后决定要对某一页构建AHI索引时,我们首先看一下该BTree所对应的hash table的锁是否被其他线程拿住了写锁;

  2. 如果被拿住了写锁,我们取消这次针对页的AHI索引构建任务,等待下次再次访问到该页时再尝试去构建,fallback到普通的BTree查询。

Part4 具体实现

从实现角度来说,其实非常简单:在btr_search_info_update_slow根据统计信息判断要对一页的记录建立AHI索引时,我们加入一个条件判断:如果当前有并发AHI构建线程拿住了hash table的X锁,我们直接返回即可。

代码只有几行,大致如下:

Part1 背景-LMLPHP

有人可能会担心这样直接跳过会不会影响代码正确性?

答案是否定的,因为我们这里没有清除该页面关于AHI的任何统计信息,只是推迟了构建时机,即推迟到hash table锁冲突不严重的时候再进行。

Part5 效果

应用上述的优化后,我们重新执行上述实验,得到如下的结果图:

Part1 背景-LMLPHP

其中,红线(开启AHI+Contention Avoidance优化)是我们实现上述优化后结果,经过100s左右的预热后,性能稳定,锁瓶颈消失。

Part6 灵感来源

其实在原始的AHI查询路径上已经有一个类似的优化了:

在btr_cur_search_to_nth_level中执行AHI查询前,如果发现AHI的hash table被其他线程X锁住了,直接fallback到BTree查询。

这里的优化考量是类似的:与其等待AHI的hash table的X锁,不如直接走btree搜索,代价很可能比等待X锁更低,并发度更高。

Part1 背景-LMLPHP

Part7 总结

该优化目前已经在TXSQL5.7最新版本中上线,将会有效缓解AHI构建的锁竞争问题,可能的场景包括不限于:系统启动、AHI开关刚开启、主备切换时,所有页面都还没有AHI记录,高并发可能导致大量的AHI构建工作。

同时经过我们验证,在官方MySQL的5.7和8.0最新版本中都存在该问题,因此我们也已经将这个优化思路贡献给了官方, https://bugs.mysql.com/bug.php?id=100512 ,目前正在评估,相信不久将合入主线。

08-09 16:06