從MySQL源碼看其網(wǎng)絡(luò)IO模型
從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上,如上面代碼所示,在筆者所處機器的環(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的超時機制),其源碼如下所示:
- vio_socket_io_wait
- |->vio_io_wait
- |-> (ret= select(fd + 1, &readfds, &writefds, &exceptfds,
- (timeout >= 0) ? &tm : NULL))
筆者在jdk源碼中看到j(luò)ava的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)化。
本文轉(zhuǎn)載自微信公眾號「解Bug之路」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系解Bug之路公眾號。