Innodb从5.5开始使用linux的Native AIO(后面简称N-AIO),告别之前仿真的方式。我们下面从5.6.10的源码分析Innodb的Native AIO使用架构。
Innodb有N个io handler threads(N=1个ibuf_io_thread + 1个log_io_thread + innodb_read_io_threads个read_io_thread+ innodb_write_io_threads个write_io_thread),也是这些线程使用了N-AIO(及提交aio请求的线程)。首先我们看一下innodb的N-AIO核心数据结构及关系:
图1 N-AIO核心数据结构及关系
os_aio_array_t:表示某一类(ibuf,log,read,write)io handler所管理的innodb aio对象
mutex:该结构自身的mutex,下面我们会看到os_aio_array_t变量都是全局变量,并且对于read,write类型的os_aio_array_t可能会有多个线程并发访问(innodb_read_io_threads、innodb_write_io_threads),对于ibuf,log类型因为它们都只有一个线程,所以不存在并发访问问题
not_full:一个条件变量event,当这个os_aio_array_t的slot从full变成not full时它释放该条件变量,即there is space in the aioarray;显然该变量会在提交io的时候关心
is_empty:另一个条件变量event,当这个os_aio_array_t的slot从not empty变成empty的时候,会释放该条件变量,也即no pending aio in the aioarray;谁wait这个条件变量?
n_slots:该aio对象可容纳的pending aio event个数,也称为aio请求槽数,它等于该aio对象包括的所有线程(对于read,write类型则可能存在多个线程共处于该对象的管理内)可容纳的pending aio event个数,即=线程数 * 每个线程可支持的max pending aio event(256)
n_segments:该对象可处理的区间数,也是包括的线程数
cur_seg:当前区间号
n_reserved:已经被占用的pending aio event数
slots:n_slots个so_aio_slot_t(aio请求对象)的数组,也即n_segments 个线程共用n_slots个槽位来存放pending aio event
aio_ctx: n_segments个aio上下文的数组,即每个线程一个aio上下文
aio_event: n_slots个aio_event的数组,aio event完成后io_event保存的位置
os_aio_slot_t:一个innodb aio请求对象
is_read:bool,TRUE if a read operation
pos:该对象在os_aio_array_t->slots[]数组中的index
reserved: TRUE if this slot is reserved,被预留了或者叫被占用了
reservation_time:预定该slot的时间
len:io请求的长度
buf:io请求的buf
type:io operate type:OS_FILE_READ orOS_FILE_WRITE
offset:file offset in bytes
file:file where to read or write
name:file name or path
io_already_done:它的aio请求是否已经完成
message1:该aio操作的innodb文件描述符(f_node_t)
message2:额外的信息,这个信息也是在aio结束时每个处理函数使用的参数
control:该slot使用的aio请求控制块iocb,这也是该结构最重要的成员
n_bytes:bytes written/read
ret:AIO return code
f_node_t:innodb管理的在表空间或log space下的文件描述符(File node of a tablespaceor the log data space)
fil_space_t:上面的文件所属的空间(Tablespace or log data space),又称为name space【这两个结构我们放在讨论innodb文件管理方式讨论】
io_context:aio的上下文
iocb:io请求控制块,表示一个aio请求【这个结构是libaio,而不是linux内的aio_abi.h/iocb,后者是没有data field】
data:用来保存用户的数据,这里用来保存该aio请求对应的os_aio_slot_t
io_event:linux aio中用来保存完成的io_event
data:用来保存iocb的data内容,但是这里没有使用
obj:提交该IO的iocb
res:表示io完成的状态,这里对于io成功的操作表示完成bytes数,即被保存到os_aio_slot_t-> n_bytes
res2:表示io完成的状态,这里可以表示出错时的错误码,即被保存到os_aio_slot_t-> ret
【上面3个结构是aio自身的结构,可以从http://www.cnblogs.com/hustcat/archive/2013/02/05/2893488.html了解linux aio】
我们再看下一张图,这张图从整体来展示io handle thread与os_aio_array_t的关系:
图2 io thread与aio_array
【注】图2我们假设有N个read_io_threads,M个write_io_threads,并且每个thread能够管理的最多pend io数为io_limit。可见对于ibuf及log类型因为它们各自只有一个线程,所以aio_ctx都为1,而slots和aio_event数则为io_limit数。
接下来我们看一下innodb N-AIO的初始化过程:os_aio_init,该函数实质就是调用os_aio_array_create完成对各种io handler的os_aio_array_t的初始化,最后再通过os_aio_linux_create_io_ctx(io_setup(max_events, io_ctx))完成对各种os_aio_array_t内的所有线程的aio上下文进行初始化。这些os_aio_array_t都是全局变量(os_aio_log_array、 os_aio_ibuf_array、os_aio_write_array、os_aio_read_array),下面我们以os_aio_read_array为例(2个innodb_read_io_threads),解释它的对象成员值:n_slots=2*256=512,n_segments=2,slots=so_aio_slot_t[512],aio_ctx= io_context_t [2],aio_event= io_event[512]。即2个read thread每个可以监控的aio event个数为256,换句话:每个线程有自己的aio上下文,每个上下文管理256个io event。 while (srv_shutdown_state != SRV_SHUTDOWN_EXIT_THREADS) {
fil_aio_wait(segment);
}
而fil_aio_wait的主要工作就是通过os_aio_linux_handle检察该线程所监控的aio event是否有完成的(
线程与它的监控
aio event
的映射关系
[
见图
2]
:首先每个线程都有一个
global_seg
,每一类
os_aio_array_t
内线程又通过
segment
标志,即通过
global_seg
可以确定当前线程所属的
os_aio_array_t
对象,然后再通过
segment
可以获得
os_aio_array_t
对象内该线程的
aio
上下文(
os_aio_array_t->aio_ctx[segment]
),而该线程监控的
event
也就是该
aio
上下文所监控的
io event
,当一个
aio
请求被完成的时候,它的
event
信息被保存在
os_aio_array_t->aio_events[segment *seg_size]
(
io_getevents(io_ctx, 1, seg_size, events,&timeout);
));如果有io event完成了则判断该完成的io操作是属于fil_node->space->purpose == FIL_TABLESPACE?如果是则执行buf_page_io_complete完成buf page??否则调用 log_io_complete完成log的??【注:这两个函数我们放在后面一个主题讨论】
我们看一下上面几个函数的主要处理逻辑:
fil_aio_wait(ulint segment){
ret = os_aio_linux_handle(segment, &fil_node, &message, &type); //获得segment线程(相当于图2的global_seg)之前等待的执行完成的io event,io的执行结果被保存到fil_node,message
…
if (fil_node->space->purpose == FIL_TABLESPACE) { //该io is for buf page
srv_set_io_thread_op_info(segment, "complete io for buf page");
buf_page_io_complete(static_cast(message));
} else {
srv_set_io_thread_op_info(segment, "complete io for log");
log_io_complete(static_cast(message));
}
}
os_aio_linux_handle逻辑:
os_aio_linux_handle(ulint global_seg, fil_node_t**message1, void** message2, ulint* type){
segment = os_aio_get_array_and_local_segment(&array, global_seg); //获得该global_seg线程在array内部的线程标识,这个标识也是从0开始的见图2
n = array->n_slots / array->n_segments; //获得一个线程可监控的io event数也就是图2的io_limit
/* Loop until we have found a completed request. */
for (;;) {
ibool any_reserved = FALSE;
os_mutex_enter(array->mutex);
for (i = 0; i < n; ++i) { //遍历该线程所管理的所有slot
slot = os_aio_array_get_nth_slot(
array, i + segment * n);
if (!slot->reserved) { //该slot是否被占用
continue;
} else if (slot->io_already_done) { //该slot已经done,即它表示的io请求已经被完成
/* Something for us to work on. */
goto found;
} else {
any_reserved = TRUE;
}
}
os_mutex_exit(array->mutex);
//到这里说明没有找到一个完成的io,则再去collect
os_aio_linux_collect(array, segment, n);
found: //找到一个完成的io,将内容返回
*message1 = slot->message1;
*message2 = slot->message2; //这个信息也是用于结果处理函数的参数
*type = slot->type;
if (slot->ret == 0 && slot->n_bytes == (long) slot->len) {
ret = TRUE;
}
…
}
等待io请求完成os_aio_linux_collect
os_aio_linux_collect(os_aio_array_t* array, ulint segment, ulint seg_size){
events = &array->aio_events[segment * seg_size]; //用来保存完成的io event的数组
/* Which io_context we are going to use. 获得该线程的Aio 上下文*/
io_ctx = array->aio_ctx[segment];
/* Starting point of the segment we will be working on. */
start_pos = segment * seg_size;
/* End point. */
end_pos = start_pos + seg_size;
ret = io_getevents(io_ctx, 1, seg_size, events, &timeout); //阻塞等待该io_cio上下文所监控的某个aio完成
retry:
if (ret > 0) {
for (i = 0; i < ret; i++) { //其实现在在这里ret只能是1
/*这里面的slot最终指向的是该aio的os_aio_slot_t 对象,主要完成一些判断及io操作返回值的保存*/
os_aio_slot_t* slot;
struct iocb* control;
control = (struct iocb*) events[i].obj; //获得完成的aio的iocb,即提交这个aio请求的iocb
ut_a(control != NULL);
slot = (os_aio_slot_t*) control->data; //通过data获得这个aio iocb所对应的os_aio_slot_t,显然这个值是在io提交的时候赋的,我们后面再介绍;
/* Some sanity checks. */
ut_a(slot != NULL);
ut_a(slot->reserved);
os_mutex_enter(array->mutex);
slot->n_bytes = events[i].res; //将该io执行的结果保存到slot里
slot->ret = events[i].res2;
slot->io_already_done = TRUE; //标志该io已经完成了,这个标志也是外层判断的条件
os_mutex_exit(array->mutex);
}
return;
}
…
}
从上面我们可以看到其实io handler thread就是完成io完成之后的相应信息的更新及处理buf_page_io_complete、log_io_complete。
我们再看一下一个aio请求提交的过程,这里我们不去关心谁来提交这些请求(innodb有各种定时器任务来提交这些请求),innodb的io请求最终(好像)都是调用到fil0fil.cc:fil_io/9接口,该函数做了很多参数判断,及查找该io请求所属的space_id下的fil_node_t(这里可以就把它当作一个文件描述符),然后调用宏os_aio,非PFS_IO最终调用os_aio_func,该函数首先通过mode及type确定该请求将被放置到哪个os_aio_array_t,然后再通过os_aio_array_reserve_slot找到一个可用的slot,最后通过os_aio_linux_dispatch把该aio请求提交。下面我们详细看一下os_aio_array_reserve_slot函数os_aio_array_reserve_slot(ulint type, os_aio_array_t* array, fil_node_t* message1, void* message2, os_file_t file, const char* name, void* buf, os_offset_t offset, ulint len){
/* 获得每个线程的最大io pending数,图2的io_limit */
slots_per_seg = array->n_slots / array->n_segments;
local_seg = (offset >> (UNIV_PAGE_SIZE_SHIFT + 6))
% array->n_segments; //调整该io请求能够存放的最佳segment(这个array里的哪个线程)
loop:
os_mutex_enter(array->mutex);
if (array->n_reserved == array->n_slots) { //判断该os_aio_array_t的slots是否都被占用了
os_mutex_exit(array->mutex);
if (!srv_use_native_aio) {
/* If the handler threads are suspended, wake them
so that we get more slots */
os_aio_simulated_wake_handler_threads();
}
//这里表示没有可用的slot,所以等待not_full条件变量
os_event_wait(array->not_full);
goto loop;
}
//到这里表示已经有一个可用的slot,这里从最合适的那个segment开始遍历查找一个可用的slot
for (i = local_seg * slots_per_seg, counter = 0;
counter < array->n_slots;
i++, counter++) {
i %= array->n_slots; //如果在local_seg内没有找到,则可能回去从它前面的segment找
slot = os_aio_array_get_nth_slot(array, i);
if (slot->reserved == FALSE) {
goto found;
}
}
/* We MUST always be able to get hold of a reserved slot. */
ut_error;
//找到可用的slot
found:
ut_a(slot->reserved == FALSE);
array->n_reserved++;
if (array->n_reserved == 1) { //如果这个slot是当前整个array->slots内第一个使用的,则重新设置is_empty条件变量
os_event_reset(array->is_empty);
}
if (array->n_reserved == array->n_slots) { //如果现在的slots变量全部被占用,则重新设置not_full条件变量
os_event_reset(array->not_full);
}
slot->reserved = TRUE; //该aio已经被占用
slot->reservation_time = ut_time();
slot->message1 = message1; //将提交该请求的fil_node_t信息保存到slot里
slot->message2 = message2; //额外的信息,这个信息也是在io结束时每个处理函数使用的参数信息
slot->file = file; //保存其它信息到slot
slot->name = name;
slot->len = len;
slot->type = type;
slot->buf = static_cast(buf);
slot->offset = offset;
slot->io_already_done = FALSE; //该aio还没完成
aio_offset = (off_t) offset;
ut_a(sizeof(aio_offset) >= sizeof(offset)
|| ((os_offset_t) aio_offset) == offset);
iocb = &slot->control;
//初始化该aio的iocb
if (type == OS_FILE_READ) {
io_prep_pread(iocb, file, buf, len, aio_offset);
} else {
ut_a(type == OS_FILE_WRITE);
io_prep_pwrite(iocb, file, buf, len, aio_offset);
}
iocb->data = (void*) slot; //将该slot保存到该aio的iocb->data,也就是它自己的slot->control里,供aio结束时提取;这里有点像闭合关系,slot拥有一个iocb,而这个iocb又保存这个slot的地址,这是因为aio自己只认识iocb,而一个event结束的时候这个iocb会被保存到event->obj里,所以等待结束的进程才能够从这个event中获得该iocb所属的slot
slot->n_bytes = 0;
slot->ret = 0;
}
初始化完成slot之后,当前的进程就可以把aio请求最终投递给操作系统os_aio_linux_dispatch(array, slot):
os_aio_linux_dispatch(os_aio_array_t* array, os_aio_slot_t* slot){
iocb = &slot->control; //获得iocb
io_ctx_index = (slot->pos * array->n_segments) / array->n_slots; //获得该slot应该属于哪个线程segment
ret = io_submit(array->aio_ctx[io_ctx_index], 1, &iocb); //向操作系统操作该aio请求
}
这样我们就把innodb使用aio的方式及架构介绍完成了,它其实使用的是多线程的方式。而aio的另一种比较常见的应用是与epoll结合见: http://www.pagefault.info/?p=76。