准备

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的内容来完成对应的I/O请求.

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
/** The asynchronous I/O context */
/** 异步 IO 请求单元 */
struct Slot {
/** 在array中的下标 */
uint16_t pos{0};

/** 是否已被申请分配 */
bool is_reserved{false};

/** 已被分配的时间长度 */
ib_time_monotonic_t reservation_time{0};

/** buffer used in i/o */
byte *buf{nullptr};

/** Buffer pointer used for actual IO. We advance this
when partial IO is required and not buf */
byte *ptr{nullptr};

/** IO类型 OS_FILE_READ or OS_FILE_WRITE */
IORequest type{IORequest::UNSET};

/** 在文件中的偏移量 */
os_offset_t offset{0};

/** 文件描述符 */
pfs_os_file_t file{
#ifdef UNIV_PFS_IO
nullptr, // m_psi
#endif
0 // m_file
};

/** 文件名 */
const char *name{nullptr};

/** IO是否已经完成 */
bool io_already_done{false};

/** fil_node_t节点 参考Fil_system */
fil_node_t *m1{nullptr};

/** the requester of an aio operation and which can be used
to identify which pending aio operation was completed */
void *m2{nullptr};

/** AIO 状态 */
dberr_t err{DB_ERROR_UNSET};

/** ... */

/** 读写的block长度 */
ulint len{0};

/** 读写字节数 */
ulint n_bytes{0};

/** 读写的block压缩前的长度 */
uint32 original_len{0};

/** block */
Block *buf_block{nullptr};

/** ... */
};

simulated-AIO原理非常简单,可以理解为一个生产者-消费者模型, 示意图如下:

simulated_aio

生产者(用户读写流程):

  • buf_page_get_gen()(预读):
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() |
-------------------------
|
| /* 读Page */
| ---------------------
-> | buf_read_page_low() |
---------------------
|
| /* 文件读写操作 */
| ----------
--> | fil_io() |
----------
|
| ----------------
--> | shard->do_io() |
----------------
|
| /* 异步IO接口 */
| ----------
--> | os_aio() |
----------
  • buf_flush_page()(写):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 /* 刷Page至文件 */
------------------
| buf_flush_page() |
------------------
|
| /* 刷Page */
| -----------------------------
--> | buf_flush_write_block_low() |
-----------------------------
|
| ----------
--> | fil_io() |
----------
|
| ----------------
--> | shard->do_io() |
----------------
|
| /* 异步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() |
----------
|
|
| /* 申请slot */
| ---------------------
--> | AIO::reserve_slot() |
| --------------------
|
| /* 唤醒simulated-AIO后台处理线程 */
| --------------------------------------
--> | 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

/* DB启动 */
-------------
| srv_start() |
-------------
|
| /* 根据srv_n_file_io_threads参数创建IO处理线程 */
| ---------------------
--> | io_handler_thread() |
---------------------
|
| /* 监控AIO请求 */
| ----------------
-> | fil_aio_wait() |
----------------
|
| /* 根据设定的 AIO mode 选择不同的AIO处理函数 */
| ------------------
--> | os_aio_handler() |
------------------
|
| /* simulated-AIO 负责处理异步IO的函数 */
| ----------------------------
--> | os_aio_simulated_handler() |
| ----------------------------
|
| /* 异步IO完成后的清理工作 */
| ------------------------
--> | buf_page_io_complete() |
------------------------

io_handler_thread()会持续监控IO请求,直到MySQL shutdown:

1
2
3
4
5
6
7
8
/* storage/innobase/srv/srv0start.cc */

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.

    1. 检查是否有已经完成但状态尚未更新的IO请求:
    • 假如存在已经完成但状态尚未更新的IO请求, 则调用AIO::release()更新slot状态.
    1. 需要判断是否MySQL准备shutdown, 假如需要shutdown则立即返回.
    1. 否则从AIO::m_slots选择等待的IO请求:
    • 选择策略是先选择一个等待时间超过2s的IO请求, 防止等待时间过长.

    • 否则选择写入偏移量最小的一个slot.

    1. 假如目前没有待处理的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
/* storage/innobase/os/os0file.cc */

/* 参数解释:
global_segment:
m1:
m2:
type:
*/
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 */
segment = AIO::get_array_and_local_segment(&array, global_segment);

/* 构造 simulated-AIO 的handler */
SimulatedAIOHandler handler(array, segment);

for (;;) {
srv_set_io_thread_op_info(global_segment, "looking for i/o requests (a)");

/* 检查目前的slots数量 */
ulint n_slots = handler.check_pending(global_segment, event);

if (n_slots == 0) {
continue;
}

/* 初始化handler */
handler.init(n_slots);

srv_set_io_thread_op_info(global_segment, "looking for i/o requests (b)");

array->acquire();

ulint n_reserved;

/* 检查是否有已经完成但状态尚未更新的IO请求 */
slot = handler.check_completed(&n_reserved);

if (slot != NULL) {
/* 存在已完成但状态未更新的slot */
break;

} else if (n_reserved == 0
#ifndef UNIV_HOTBACKUP
&& !buf_flush_page_cleaner_is_active() &&
srv_shutdown_state.load() == SRV_SHUTDOWN_EXIT_THREADS
#endif /* !UNIV_HOTBACKUP */
) {

/* 目前没有待处理的IO请求,并且MySQL准备shutdown, 则返回 */
array->release();

*m1 = NULL;

*m2 = NULL;

return (DB_SUCCESS);

} else if (handler.select()) {
/* 否则根据slot选择策略,选择对应的slot */
break;
}

/* 假如目前没有待处理的IO请求,则进入wait状态 */

srv_set_io_thread_op_info(global_segment, "resetting wait event");

/* We wait here until tbere are more IO requests
for this segment. */

os_event_reset(event);

array->release();

srv_set_io_thread_op_info(global_segment, "waiting for i/o request");

os_event_wait(event);
}

/** Found a slot that has already completed its IO */

if (slot == NULL) {
/* slot == NULL代表所有已完成的slot状态都已经更新,并且我们通过
* select()选择了合适的slot需要完成I/O处理 */

/* 合并I/O操作 */
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");

/* IO操作(pwrite/pread) */
handler.io();

srv_set_io_thread_op_info(global_segment, "file i/o done");

/* simulated-AIO中io_complete()为空实现 */
handler.io_complete();

array->acquire();

/* 设置slot->io_already_done = true即表示已完成,但其他状态尚未更新, 交由下次
* 循环更新其他状态 */
handler.done();

/* 返回handler的第一个slot */
slot = handler.first_slot();
}

/* 更新slot的状态 */
ut_ad(slot->is_reserved);

*m1 = slot->m1;
*m2 = slot->m2;

*type = slot->type;

array->release(slot);

array->release();

return (DB_SUCCESS);
}

总结

综上所述,通过源码分析我们详细的了解MySQL实现的模拟异步I/O的框架, 原理非常简单,由用户线程获取slot并记录相关的I/O信息,而 simulated-AIO 的后台工作线程则通过一定的策略来逐一处理IO请求, 并且通过合并IO的策略来对I/0读写做了一些优化.