1. 程式人生 > 實用技巧 >從MySQL原始碼看其網路IO模型

從MySQL原始碼看其網路IO模型

從MySQL原始碼看其網路IO模型

前言

MySQL是當今最流行的開源資料庫,閱讀其原始碼是一件大有裨益的事情(雖然其程式碼感覺比較凌亂)。而筆者閱讀一個Server原始碼的習慣就是先從其網路IO模型看起。於是,便有了本篇部落格。

MySQL啟動Socket監聽

看原始碼,首先就需要找到其入口點,mysqld的入口點為mysqld_main,跳過了各種配置檔案的載入 之後,我們來到了network_init初始化網路環節,如下圖所示:

下面是其呼叫棧:

mysqld_main (MySQL Server Entry Point)
	|-network_init (初始化網路)
		/* 建立tcp套接字 */
		|-create_socket (AF_INET)
		|-mysql_socket_bind (AF_INET)
		|-mysql_socket_listen (AF_INET)
		/* 建立UNIX套接字*/
		|-mysql_socket_socket (AF_UNIX)
		|-mysql_socket_bind (AF_UNIX)
		|-mysql_socket_listen (AF_UNIX)

值得注意的是,在tcp socket的初始化過程中,考慮到了ipv4/v6的兩種情況:

// 首先建立ipv4連線
ip_sock= create_socket(ai, AF_INET, &a);
// 如果無法建立ipv4連線,則嘗試建立ipv6連線
if(mysql_socket_getfd(ip_sock) == INVALID_SOCKET)
 	ip_sock= create_socket(ai, AF_INET6, &a);

如果我們以很快的速度stop/start mysql,會出現上一個mysql的listen port沒有被release導致無法當前mysql的socket無法bind的情況,在此種情況下mysql會迴圈等待,其每次等待時間為當前重試次數retry * retry/3 +1秒,一直到設定的--port-open-timeout(預設為0)為止,如下圖所示:

MySQL新建連線處理迴圈

通過handle_connections_sockets處理MySQL的新建連線迴圈,根據作業系統的配置通過poll/select處理迴圈(非epoll,這樣可移植性較高,且mysql瓶頸不在網路上)。
MySQL通過執行緒池的模式處理連線(一個連線對應一個執行緒,連線關閉後將執行緒歸還到池中),如下圖所示:
對應的呼叫棧如下所示:

handle_connections_sockets
	|->poll/select
	|->new_sock=mysql_socket_accept(...sock...) /*從listen socket中獲取新連線*/
	|->new THD 連線執行緒上下文 /* 如果獲取不到足夠記憶體,則shutdown new_sock*/
	|->mysql_socket_getfd(sock) 從socket中獲取
		/** 設定為NONBLOCK和環境有關 **/
	|->fcntl(mysql_socket_getfd(sock), F_SETFL, flags | O_NONBLOCK);
	|->mysql_socket_vio_new
		|->vio_init (VIO_TYPE_TCPIP)
			|->(vio->write = vio_write)
			/* 預設用的是vio_read */
			|->(vio->read=(flags & VIO_BUFFERED_READ) ?vio_read_buff :vio_read;)
			|->(vio->viokeepalive = vio_keepalive) /*tcp層面的keepalive*/
			|->.....
	|->mysql_net_init
		|->設定超時時間,最大packet等引數
	|->create_new_thread(thd) /* 實際是從執行緒池拿,不夠再新建pthread執行緒 */
		|->最大連線數限制
		|->create_thread_to_handle_connection
			|->首先看下執行緒池是否有空閒執行緒
				|->mysql_cond_signal(&COND_thread_cache) /* 有則傳送訊號 */
			/** 這邊的hanlde_one_connection是mysql連線的主要處理函式 */
			|->mysql_thread_create(...handle_one_connection...)			

MySQL的VIO

如上圖程式碼中,每新建一個連線,都隨之新建一個vio(mysql_socket_vio_new->vio_init),在vio_init的過程中,初始化了一堆回掉函式,如下圖所示:
我們關注點在vio_read和vio_write上,如上面程式碼所示,在筆者所處機器的環境下將MySQL連線的socket設定成了非阻塞模式(O_NONBLOCK)模式。所以在vio的程式碼裡面採用了nonblock程式碼的編寫模式,如下面原始碼所示:

vio_read

size_t vio_read(Vio *vio, uchar *buf, size_t size)
{
  while ((ret= mysql_socket_recv(vio->mysql_socket, (SOCKBUF_T *)buf, size, flags)) == -1)
  {
    ......
    // 如果上面獲取的資料為空,則通過select的方式去獲取讀取事件,並設定超時timeout時間
    if ((ret= vio_socket_io_wait(vio, VIO_IO_EVENT_READ)))
        break;
  }
}

即通過while迴圈去讀取socket中的資料,如果讀取為空,則通過vio_socket_io_wait去等待(藉助於select的超時機制),其原始碼如下所示:

vio_socket_io_wait
	|->vio_io_wait
		|-> (ret= select(fd + 1, &readfds, &writefds, &exceptfds, 
              (timeout >= 0) ? &tm : NULL))

筆者在jdk原始碼中看到java的connection time out也是通過這,select(...wait_time)的方式去實現連線超時的。
由上述原始碼可以看出,這個mysql的read_timeout是針對每次socket recv(而不是整個packet的),所以可能出現超過read_timeout MySQL仍舊不會報錯的情況,如下圖所示:

vio_write

vio_write實現模式和vio_read一致,也是通過select來實現超時時間的判定,如下面原始碼所示:

size_t vio_write(Vio *vio, const uchar* buf, size_t size)
{
  while ((ret= mysql_socket_send(vio->mysql_socket, (SOCKBUF_T *)buf, size, flags)) == -1)
  {
    int error= socket_errno;

    /* The operation would block? */
    // 處理EAGAIN和EWOULDBLOCK返回,NON_BLOCK模式都必須處理
    if (error != SOCKET_EAGAIN && error != SOCKET_EWOULDBLOCK)
      break;

    /* Wait for the output buffer to become writable.*/
    if ((ret= vio_socket_io_wait(vio, VIO_IO_EVENT_WRITE)))
      break;
  }
}

MySQL的連線處理執行緒

從上面的程式碼:

mysql_thread_create(...handle_one_connection...)

可以發現,MySQL每個執行緒的處理函式為handle_one_connection,其過程如下圖所示:


程式碼如下所示:

for(;;){
	// 這邊做了連線的handshake和auth的工作
	rc= thd_prepare_connection(thd);
	// 和通常的執行緒處理一樣,一個無限迴圈獲取連線請求
	while(thd_is_connection_alive(thd))
	{
		if(do_command(thd))
			break;
	}
	// 出迴圈之後,連線已經被clientdu端關閉或者出現異常
	// 這邊做了連線的銷燬動作
	end_connection(thd);
end_thread:
	...
	// 這邊呼叫end_thread做清理動作,並將當前執行緒返還給執行緒池重用
	// end_thread對應為one_thread_per_connection_end
	if (MYSQL_CALLBACK_ELSE(thread_scheduler, end_thread, (thd, 1), 0))
		return;	
	...
	// 這邊current_thd是個巨集定義,其實是current_thd();
	// 主要是從執行緒上下文中獲取新塞進去的thd
	// my_pthread_getspecific_ptr(THD*,THR_THD);
	thd= current_thd;
	...
}

mysql的每個woker執行緒通過無限迴圈去處理請求。

執行緒的歸還過程

MySQL通過呼叫one_thread_per_connection_end(即上面的end_thread)去歸還連線。

MYSQL_CALLBACK_ELSE(...end_thread)
	one_thread_per_connection_end
		|->thd->release_resources()
		|->......
		|->block_until_new_connection

執行緒在新連線尚未到來之前,等待在訊號量上(下面程式碼是C/C++ mutex condition的標準使用模式):

static bool block_until_new_connection()
{	
	mysql_mutex_lock(&LOCK_thread_count);
	......
    while (!abort_loop && !wake_pthread && !kill_blocked_pthreads_flag)
      mysql_cond_wait(&x1, &LOCK_thread_count);
   ......
   // 從等待列表中獲取需要處理的THD
   thd= waiting_thd_list->front();
   waiting_thd_list->pop_front();
   ......
   // 將thd放入到當前執行緒上下文中
   // my_pthread_setspecific_ptr(THR_THD,  this)    
   thd->store_globals();
   ......
   mysql_mutex_unlock(&LOCK_thread_count);
   .....
}

整個過程如下圖所示:


由於MySQL的呼叫棧比較深,所以將thd放入執行緒上下文中能夠有效的在呼叫棧中減少傳遞引數的數量。

總結

MySQL的網路IO模型採用了經典的執行緒池技術,雖然效能上不及reactor模型,但好在其瓶頸並不在網路IO上,採用這種方法無疑可以節省大量的精力去專注於處理sql等其它方面的優化。

推薦:中華文學網