innodb学习(一)——innodb如何使用aio

      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的关系:

innodb学习(一)——innodb如何使用aio_第1张图片

图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。
       在初始化aio后,就会创建相应个数的io handler thread。每个io handler thread的入口函数都是io_handler_thread,然后
	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。

你可能感兴趣的:(IO,InnoDB)