從MySQL源碼看其網絡IO模型

從MySQL源碼看其網絡IO模型

前言

MySQL是當今最流行的開源數據庫,閱讀其源碼是一件大有裨益的事情(雖然其代碼感受比較凌亂)。而筆者閱讀一個Server源碼的習慣就是先從其網絡IO模型看起。因而,便有了本篇博客。java

MySQL啓動Socket監聽

看源碼,首先就須要找到其入口點,mysqld的入口點爲mysqld_main,跳過了各類配置文件的加載 以後,咱們來到了network_init初始化網絡環節,以下圖所示:

下面是其調用棧:mysql

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的兩種狀況:react

// 首先建立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)爲止,以下圖所示: sql

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的超時機制),其源碼以下所示:socket

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仍舊不會報錯的狀況,以下圖所示: tcp

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的鏈接處理線程

從上面的代碼:oop

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等其它方面的優化。

原文連接

https://my.oschina.net/alchemystar/blog/3048760

相關文章
相關標籤/搜索