天天看點

從MySQL源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

從MySQL源碼看其網絡IO模型

前言

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

MySQL啟動Socket監聽

看源碼,首先就需要找到其入口點,mysqld的入口點為mysqld_main,跳過了各種配置檔案的加載

之後,我們來到了network_init初始化網絡環節,如下圖所示:

從MySQL源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

下面是其調用棧:

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源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

MySQL建立連接配接處理循環

通過handle_connections_sockets處理MySQL的建立連接配接循環,根據作業系統的配置通過poll/select處理循環(非epoll,這樣可移植性較高,且mysql瓶頸不在網絡上)。

MySQL通過線程池的模式處理連接配接(一個連接配接對應一個線程,連接配接關閉後将線程歸還到池中),如下圖所示:

從MySQL源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

對應的調用棧如下所示:

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的過程中,初始化了一堆回掉函數,如下圖所示:

從MySQL源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

我們關注點在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仍舊不會報錯的情況,如下圖所示:

從MySQL源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

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,其過程如下圖所示:

從MySQL源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

代碼如下所示:

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源碼看其網絡IO模型從MySQL源碼看其網絡IO模型總結

由于MySQL的調用棧比較深,是以将thd放入線程上下文中能夠有效的在調用棧中減少傳遞參數的數量。

總結

MySQL的網絡IO模型采用了經典的線程池技術,雖然性能上不及reactor模型,但好在其瓶頸并不在網絡IO上,采用這種方法無疑可以節省大量的精力去專注于處理sql等其它方面的優化。