1. 程式人生 > >innodb學習(一)——innodb如何使用aio

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_threadsread_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。
       在初始化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