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<buf_page_t*>(message)); } else { srv_set_io_thread_op_info(segment, "complete io for log"); log_io_complete(static_cast<log_group_t*>(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<byte*>(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。