问题描述
TXSQL 基于 MySQL 8.0.30。InnoDB Linux 原生 AIO 的完成处理路径中,
LinuxAIOHandler::find_completed_slot()(storage/innobase/os/os0file.cc,约 L2265)
在持有 AIO 数组互斥锁(m_array->m_mutex)的状态下返回已完成的槽位(原注释
"Note: We don't release the mutex."),随后整条完成处理链——
check_state() → AIOHandler::post_io_processing() → io_complete() →
对加密/压缩表空间调用 Encryption 的整页解密/解压——都在该锁内执行。
这把锁同时被所有 I/O 提交方使用(AIO::reserve_slot() 申请槽位也要拿它),
因此:同一时刻全库只有一个线程能处理完成请求,所有 IO handler 线程、以及
前台提交读/写请求的线程,全部在这把锁后面排队。整个数据库的 I/O 完成吞吐
被钉死在“1 ÷ 单页处理时间”,与 handler 线程数无关。
为什么上游从未暴露
AES-NI 硬件加速下解密一个 16KB 页约 16 微秒,锁内处理时间极短,影响不可见。
但对 TDE 使用较慢的软件密码实现(约 40+ 微秒/页)时,后果是全局性的:
读完成串行化把聚合读吞吐压到约 40 MB/s 量级,且页清洗(写盘提交)也会在
读完成的解密后面排队。
实测数据(同一份代码系,冷全表扫描负载:16 表 × 50 万行,buffer pool 128M,内存盘)
| 场景 |
8 线程 修复前 |
8 线程 修复后 |
提升 |
| 不加密 |
1.488 GB/s |
1.490 GB/s |
1.00x(对照组,不受影响) |
| TDE AES-NI |
0.491 GB/s |
1.450 GB/s |
2.95x |
| TDE 软件 SM4 |
0.038 GB/s |
1.180 GB/s |
31.0x |
单线程下 TDE 慢算法同样有 ~10 倍提升(前台提交请求也要抢这把锁,修复前
后台在锁内解密多久、前台就等多久)。
修复思路
采用“认领协议”把处理移出锁外:
find_completed_slot() 扫描到完成槽位时,在锁内将
slot->io_already_done 置为 false 完成认领,然后放锁再返回;
- 内核事件已由
collect() 收割,槽位不会被再次标记,其他 handler 线程
扫描到 false 会跳过——处理依然严格“恰好一次”;
- 调用方此后独占该槽位,完成处理(含页解密)在无锁状态执行;
poll() 仅在短读重提交(resubmit() 前后)和归还槽位
(AIO::release(slot) 前)重新拿锁。
这与仓库既有模式一致:写路径的页压缩与加密本来就在数组锁外执行
(AIO::reserve_slot() 显式放锁后加密),模拟 AIO 引擎的完成处理也不用这把锁,
本次修改只是让 Linux 原生 AIO 的读完成路径对齐同样的模式。
正确性验证(已在同源 8.0 代码上完成)
- 新旧二进制读同一张加密表,全表 CRC32 校验值完全一致;
- MTR encryption 套件在 debug 构建上全部通过(15/15);
- 不加密场景吞吐分毫未变(1.00x),证明提升仅来自锁释放;
- 已完成 TXSQL 8.0.30 分支的移植与
make innobase 编译验证。
稍后提交 PR,本 issue 作为关联说明。
问题描述
TXSQL 基于 MySQL 8.0.30。InnoDB Linux 原生 AIO 的完成处理路径中,
LinuxAIOHandler::find_completed_slot()(storage/innobase/os/os0file.cc,约 L2265)在持有 AIO 数组互斥锁(
m_array->m_mutex)的状态下返回已完成的槽位(原注释"Note: We don't release the mutex."),随后整条完成处理链——
check_state()→AIOHandler::post_io_processing()→io_complete()→对加密/压缩表空间调用
Encryption的整页解密/解压——都在该锁内执行。这把锁同时被所有 I/O 提交方使用(
AIO::reserve_slot()申请槽位也要拿它),因此:同一时刻全库只有一个线程能处理完成请求,所有 IO handler 线程、以及
前台提交读/写请求的线程,全部在这把锁后面排队。整个数据库的 I/O 完成吞吐
被钉死在“1 ÷ 单页处理时间”,与 handler 线程数无关。
为什么上游从未暴露
AES-NI 硬件加速下解密一个 16KB 页约 16 微秒,锁内处理时间极短,影响不可见。
但对 TDE 使用较慢的软件密码实现(约 40+ 微秒/页)时,后果是全局性的:
读完成串行化把聚合读吞吐压到约 40 MB/s 量级,且页清洗(写盘提交)也会在
读完成的解密后面排队。
实测数据(同一份代码系,冷全表扫描负载:16 表 × 50 万行,buffer pool 128M,内存盘)
单线程下 TDE 慢算法同样有 ~10 倍提升(前台提交请求也要抢这把锁,修复前
后台在锁内解密多久、前台就等多久)。
修复思路
采用“认领协议”把处理移出锁外:
find_completed_slot()扫描到完成槽位时,在锁内将slot->io_already_done置为 false 完成认领,然后放锁再返回;collect()收割,槽位不会被再次标记,其他 handler 线程扫描到 false 会跳过——处理依然严格“恰好一次”;
poll()仅在短读重提交(resubmit()前后)和归还槽位(
AIO::release(slot)前)重新拿锁。这与仓库既有模式一致:写路径的页压缩与加密本来就在数组锁外执行
(
AIO::reserve_slot()显式放锁后加密),模拟 AIO 引擎的完成处理也不用这把锁,本次修改只是让 Linux 原生 AIO 的读完成路径对齐同样的模式。
正确性验证(已在同源 8.0 代码上完成)
make innobase编译验证。稍后提交 PR,本 issue 作为关联说明。