從 MySQL源碼分析網(wǎng)絡(luò)IO模型
前言
MySQL 是當(dāng)今最流行的開源數(shù)據(jù)庫,閱讀其源碼是一件大有裨益的事情 (雖然其代碼感覺比較凌亂)。而筆者閱讀一個 Server 源碼的習(xí)慣就是先從其網(wǎng)絡(luò) IO 模型看起。于是,便有了本篇博客。
MySQL 啟動 Socket 監(jiān)聽
看源碼,首先就需要找到其入口點,mysqld 的入口點為 mysqld_main, 跳過了各種配置文件的加載 之后,我們來到了 network_init 初始化網(wǎng)絡(luò)環(huán)節(jié),如下圖所示:
下面是其調(diào)用棧:
mysqld_main (MySQL Server Entry Point) |-network_init (初始化網(wǎng)絡(luò)) /* 建立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 的兩種情況:
// 首先創(chuàng)建ipv4連接 ip_sock= create_socket(ai, AF_INET, &a); // 如果無法創(chuàng)建ipv4連接,則嘗試創(chuàng)建ipv6連接 if(mysql_socket_getfd(ip_sock) == INVALID_SOCKET) ip_sock= create_socket(ai, AF_INET6, &a);
如果我們以很快的速度 stop/start mysql, 會出現(xiàn)上一個 mysql 的 listen port 沒有被 release 導(dǎo)致無法當(dāng)前 mysql 的 socket 無法 bind 的情況,在此種情況下 mysql 會循環(huán)等待,其每次等待時間為當(dāng)前重試次數(shù) retry * retry/3 +1 秒,一直到設(shè)置的 --port-open-timeout (默認(rèn)為 0) 為止,如下圖所示:
MySQL 新建連接處理循環(huán)
通過 handle_connections_sockets 處理 MySQL 的新建連接循環(huán),根據(jù)操作系統(tǒng)的配置通過 poll/select 處理循環(huán) (非 epoll, 這樣可移植性較高,且 mysql 瓶頸不在網(wǎng)絡(luò)上)。
MySQL 通過線程池的模式處理連接 (一個連接對應(yīng)一個線程,連接關(guān)閉后將線程歸還到池中), 如下圖所示:
對應(yīng)的調(diào)用棧如下所示:
handle_connections_sockets |->poll/select |->new_sock=mysql_socket_accept(...sock...) /*從listen socket中獲取新連接*/ |->new THD 連接線程上下文 /* 如果獲取不到足夠內(nèi)存,則shutdown new_sock*/ |->mysql_socket_getfd(sock) 從socket中獲取 /** 設(shè)置為NONBLOCK和環(huán)境有關(guān) **/ |->fcntl(mysql_socket_getfd(sock), F_SETFL, flags | O_NONBLOCK); |->mysql_socket_vio_new |->vio_init (VIO_TYPE_TCPIP) |->(vio->write = vio_write) /* 默認(rèn)用的是vio_read */ |->(vio->read=(flags & VIO_BUFFERED_READ) ?vio_read_buff :vio_read;) |->(vio->viokeepalive = vio_keepalive) /*tcp層面的keepalive*/ |->..... |->mysql_net_init |->設(shè)置超時時間,最大packet等參數(shù) |->create_new_thread(thd) /* 實際是從線程池拿,不夠再新建pthread線程 */ |->最大連接數(shù)限制 |->create_thread_to_handle_connection |->首先看下線程池是否有空閑線程 |->mysql_cond_signal(&COND_thread_cache) /* 有則發(fā)送信號 */ /** 這邊的hanlde_one_connection是mysql連接的主要處理函數(shù) */ |->mysql_thread_create(...handle_one_connection...)
MySQL 的 VIO
如上圖代碼中,每新建一個連接,都隨之新建一個 vio (mysql_socket_vio_new->vio_init), 在 vio_init 的過程中,初始化了一堆回掉函數(shù),如下圖所示:
我們關(guān)注點在 vio_read 和 vio_write 上,如上面代碼所示,在筆者所處機(jī)器的環(huán)境下將 MySQL 連接的 socket 設(shè)置成了非阻塞模式 (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) { ...... // 如果上面獲取的數(shù)據(jù)為空,則通過select的方式去獲取讀取事件,并設(shè)置超時timeout時間 if ((ret= vio_socket_io_wait(vio, VIO_IO_EVENT_READ))) break; } }
即通過 while 循環(huán)去讀取 socket 中的數(shù)據(jù),如果讀取為空,則通過 vio_socket_io_wait 去等待 (借助于 select 的超時機(jī)制), 其源碼如下所示:
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) 的方式去實現(xiàn)連接超時的。
由上述源碼可以看出,這個 mysql 的 read_timeout 是針對每次 socket recv (而不是整個 packet 的),所以可能出現(xiàn)超過 read_timeout MySQL 仍舊不會報錯的情況,如下圖所示:
vio_write
vio_write 實現(xiàn)模式和 vio_read 一致,也是通過 select 來實現(xiàn)超時時間的判定,如下面源碼所示:
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...)
可以發(fā)現(xiàn),MySQL 每個線程的處理函數(shù)為 handle_one_connection, 其過程如下圖所示:
代碼如下所示:
for(;;){ // 這邊做了連接的handshake和auth的工作 rc= thd_prepare_connection(thd); // 和通常的線程處理一樣,一個無限循環(huán)獲取連接請求 while(thd_is_connection_alive(thd)) { if(do_command(thd)) break; } // 出循環(huán)之后,連接已經(jīng)被clientdu端關(guān)閉或者出現(xiàn)異常 // 這邊做了連接的銷毀動作 end_connection(thd); end_thread: ... // 這邊調(diào)用end_thread做清理動作,并將當(dāng)前線程返還給線程池重用 // end_thread對應(yīng)為one_thread_per_connection_end if (MYSQL_CALLBACK_ELSE(thread_scheduler, end_thread, (thd, 1), 0)) return; ... // 這邊current_thd是個宏定義,其實是current_thd(); // 主要是從線程上下文中獲取新塞進(jìn)去的thd // my_pthread_getspecific_ptr(THD*,THR_THD); thd= current_thd; ... }
mysql 的每個 woker 線程通過無限循環(huán)去處理請求。
線程的歸還過程
MySQL 通過調(diào)用 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 的標(biāo)準(zhǔn)使用模式):
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放入到當(dāng)前線程上下文中 // my_pthread_setspecific_ptr(THR_THD, this) thd->store_globals(); ...... mysql_mutex_unlock(&LOCK_thread_count); ..... }
整個過程如下圖所示:
由于 MySQL 的調(diào)用棧比較深,所以將 thd 放入線程上下文中能夠有效的在調(diào)用棧中減少傳遞參數(shù)的數(shù)量。
總結(jié)
MySQL 的網(wǎng)絡(luò) IO 模型采用了經(jīng)典的線程池技術(shù),雖然性能上不及 reactor 模型,但好在其瓶頸并不在網(wǎng)絡(luò) IO 上,采用這種方法無疑可以節(jié)省大量的精力去專注于處理 sql 等其它方面的優(yōu)化。
以上就是從 MySQL源碼分析網(wǎng)絡(luò)IO模型的詳細(xì)內(nèi)容,更多關(guān)于 MySQL網(wǎng)絡(luò)IO模型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在MySQL中使用Sphinx實現(xiàn)多線程搜索的方法
這篇文章主要介紹了在MySQL中使用Sphinx實現(xiàn)多線程搜索的方法,修改Sphinx的搜索引擎配置即可,需要的朋友可以參考下2015-06-06RedHat6.5/CentOS6.5安裝Mysql5.7.20的教程詳解
這篇文章主要介紹了RedHat6.5/CentOS6.5安裝Mysql5.7.20的教程詳解,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-11-11MySQL計劃任務(wù)(事件調(diào)度器) Event Scheduler介紹
MySQL5.1.x版本中引入了一項新特性EVENT,顧名思義就是事件、定時任務(wù)機(jī)制,在指定的時間單元內(nèi)執(zhí)行特定的任務(wù),因此今后一些對數(shù)據(jù)定時性操作不再依賴外部程序,而直接使用數(shù)據(jù)庫本身提供的功能2013-10-10