准备
MySQL内核版本: 8.0.19
simulated-AIO
simulated-AIO 是一套由 InnoDB 早先实现的异步 I/O 模型. 在 MySQL 的存储引擎 InnoDB 中分别实现了同步IO以及异步IO, Redo Log 的写入方式采用同步IO, 而数据页的写入由于 Redo Log 的保护则采用异步 IO 的写入方式. 在 Linux AIO 引入之前, InnoDB 实现了一套异步 IO 框架, 即 simulated-AIO. simulated-AIO 的原理类似于 libaio, 原理实现都较为简单.
在Linux平台, 假如安装了 libaio, MySQL 是默认使用 libaio, 只有在设置了innodb_use_native_aio = 0
的情况下才会使用 simulated-AIO.
InnoDB的异步IO主要是用来处理预读和数据Page的写请求,对于正常Page的数据读取则是通过同步 IO 进行.
simulated-AIO 原理
数据结构
simulated-AIO 预分配 n 个大小 slot 数组, 每个用户的读写请求通过申请数组中的 slot, 构造对应的 IO 类型、写入 offset 等等. 而 simulated-AIO 的工作线程则根据slot的内容来完成对应的 IO 请求.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
|
struct Slot { uint16_t pos{0};
bool is_reserved{false};
ib_time_monotonic_t reservation_time{0};
byte *buf{nullptr};
byte *ptr{nullptr};
IORequest type{IORequest::UNSET};
os_offset_t offset{0};
pfs_os_file_t file{ #ifdef UNIV_PFS_IO nullptr, #endif 0 };
const char *name{nullptr};
bool io_already_done{false};
fil_node_t *m1{nullptr};
void *m2{nullptr};
dberr_t err{DB_ERROR_UNSET};
ulint len{0};
ulint n_bytes{0};
uint32 original_len{0};
Block *buf_block{nullptr};
};
|
simulated-AIO 原理非常简单,可以理解为一个生产者-消费者模型, 示意图如下:
生产者(用户读写流程)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| -------------------- | buf_page_get_gen() | -------------------- | | | --------------------------------- --> | Buf_fetch_normal::single_page() | --------------------------------- | | | ------------------------- --> | buf_read_ahead_linear() | ------------------------- | | | --------------------- -> | buf_read_page_low() | --------------------- | | | ---------- --> | fil_io() | ---------- | | ---------------- --> | shard->do_io() | ---------------- | | | ---------- --> | os_aio() | ----------
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ------------------ | buf_flush_page() | ------------------ | | | ----------------------------- --> | buf_flush_write_block_low() | ----------------------------- | | ---------- --> | fil_io() | ---------- | | ---------------- --> | shard->do_io() | ---------------- | | | ---------- --> | os_aio() | ----------
|
无论是读操作还是写操作,都要交由os_aio()
处理, os_aio
是一个通用的接口, 在Linux平台封装了 libaio 和 simulated AIO. 具体的处理逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ---------- | os_aio() | ---------- | | | | --------------------- --> | AIO::reserve_slot() | | -------------------- | | | -------------------------------------- --> | AIO::wake_simulated_handler_thread() | --------------------------------------
|
根据IO类型选择对应的 I/O slot 数组(select_slot_array())
.
向 I/O slot 数组申请 slot (reserve_slot()
).
唤醒对应的异步IO线程处理IO请求(AIO::wake_simulated_handler_thread()
).
消费者(异步I/O处理流程)
在MySQL启动时,会分别创建1个ibuf处理线程, 1个log处理线程, n个(srv_n_read_io_threads
)读处理线程, n个(srv_n_write_io_threads
)写处理线程.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| ------------- | srv_start() | ------------- | | | --------------------- --> | io_handler_thread() | --------------------- | | | ---------------- -> | fil_aio_wait() | ---------------- | | | ------------------ --> | os_aio_handler() | ------------------ | | | ---------------------------- --> | os_aio_simulated_handler() | | ---------------------------- | | | ------------------------ --> | buf_page_io_complete() | ------------------------
|
io_handler_thread()
会持续监控 IO 请求,直到 MySQL shutdown:
1 2 3 4 5 6 7 8
|
static void io_handler_thread(ulint segment) { while (srv_shutdown_state.load() != SRV_SHUTDOWN_EXIT_THREADS || buf_flush_page_cleaner_is_active() || !os_aio_all_slots_free()) { fil_aio_wait(segment); } }
|
fil_aio_wait()
会调用os_aio_handler()
根据不同的IO模型选择不同的函数处理IO请求, simulated AIO 的处理函数是os_aio_simulated_handler()
:
根据 global segment id 选择对应I/O工作线程的event, 计算在该array的segment id.
检查是否有已经完成但状态尚未更新的IO请求:
- 假如存在已经完成但状态尚未更新的IO请求, 则调用
AIO::release()
更新slot状态.
需要判断是否MySQL准备shutdown, 假如需要shutdown则立即返回.
否则从AIO::m_slots
选择等待的IO请求:
假如目前没有待处理的IO请求,则进入wait状态.
处理选中的IO请求前,会调用merge()
进行IO合并, 选择文件偏移量offset连续的IO请求进行合并.
调用 simulated-AIO 封装的同步IO接口(pwrite()
/pread()
)完成IO操作.
源码分析
核心处理函数os_aio_simulated_handler()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
|
static dberr_t os_aio_simulated_handler(ulint global_segment, fil_node_t **m1, void **m2, IORequest *type) { Slot *slot; AIO *array; ulint segment; os_event_t event = os_aio_segment_wait_events[global_segment];
segment = AIO::get_array_and_local_segment(&array, global_segment);
SimulatedAIOHandler handler(array, segment);
for (;;) { srv_set_io_thread_op_info(global_segment, "looking for i/o requests (a)");
ulint n_slots = handler.check_pending(global_segment, event);
if (n_slots == 0) { continue; }
handler.init(n_slots);
srv_set_io_thread_op_info(global_segment, "looking for i/o requests (b)");
array->acquire();
ulint n_reserved;
slot = handler.check_completed(&n_reserved);
if (slot != NULL) { break;
} else if (n_reserved == 0 #ifndef UNIV_HOTBACKUP && !buf_flush_page_cleaner_is_active() && srv_shutdown_state.load() == SRV_SHUTDOWN_EXIT_THREADS #endif ) {
array->release();
*m1 = NULL;
*m2 = NULL;
return (DB_SUCCESS);
} else if (handler.select()) { break; }
srv_set_io_thread_op_info(global_segment, "resetting wait event");
os_event_reset(event);
array->release();
srv_set_io_thread_op_info(global_segment, "waiting for i/o request");
os_event_wait(event); }
if (slot == NULL) {
handler.merge();
srv_set_io_thread_op_info(global_segment, "consecutive i/o requests");
array->release();
srv_set_io_thread_op_info(global_segment, "doing file i/o");
handler.io();
srv_set_io_thread_op_info(global_segment, "file i/o done");
handler.io_complete();
array->acquire();
handler.done();
slot = handler.first_slot(); }
ut_ad(slot->is_reserved);
*m1 = slot->m1; *m2 = slot->m2;
*type = slot->type;
array->release(slot);
array->release();
return (DB_SUCCESS); }
|
Q & A
- 关于 simulated AIO 多个线程同时写入一个文件的问题?
simulated AIO 不能保证多线程同时写一个文件, 但 simulated AIO 底层调用的文件接口是 pwrite(), 通过指定参数 offset, 以及每次写的时候加上 Page 锁, 就能保证不写在同一个 offset.
总结
综上所述,通过源码分析我们详细的了解 MySQL 实现的模拟异步 I/O 的框架, 原理非常简单,由用户线程获取 slot 并记录相关的 I/O 信息,而 simulated-AIO 的后台工作线程则通过一定的策略来逐一处理 I/O 请求, 并且通过合并 I/O 的策略来对 I/O 读写做了一些优化.