從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等其它方面的優化。
推薦:中華文學網