从MySQL源码看其网络IO模型

数据库 MySQL
MySQL是当今最流行的开源数据库,阅读其源码是一件大有裨益的事情(虽然其代码感觉比较凌乱)。而笔者阅读一个Server源码的习惯就是先从其网络IO模型看起。于是,便有了本篇博客。

 从MySQL源码看其网络IO模型

前言

MySQL是当今最流行的开源数据库,阅读其源码是一件大有裨益的事情(虽然其代码感觉比较凌乱)。而笔者阅读一个Server源码的习惯就是先从其网络IO模型看起。于是,便有了本篇博客。

MySQL启动Socket监听

看源码,首先就需要找到其入口点,mysqld的入口点为mysqld_main,跳过了各种配置文件的加载

之后,我们来到了network_init初始化网络环节,如下图所示:

 

下面是其调用栈:

  1. mysqld_main (MySQL Server Entry Point) 
  2.     |-network_init (初始化网络) 
  3.         /* 建立tcp套接字 */ 
  4.         |-create_socket (AF_INET) 
  5.         |-mysql_socket_bind (AF_INET) 
  6.         |-mysql_socket_listen (AF_INET) 
  7.         /* 建立UNIX套接字*/ 
  8.         |-mysql_socket_socket (AF_UNIX) 
  9.         |-mysql_socket_bind (AF_UNIX) 
  10.         |-mysql_socket_listen (AF_UNIX) 

值得注意的是,在tcp socket的初始化过程中,考虑到了ipv4/v6的两种情况:

  1. // 首先创建ipv4连接 
  2. ip_sock= create_socket(ai, AF_INET, &a); 
  3. // 如果无法创建ipv4连接,则尝试创建ipv6连接 
  4. if(mysql_socket_getfd(ip_sock) == INVALID_SOCKET) 
  5.      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通过线程池的模式处理连接(一个连接对应一个线程,连接关闭后将线程归还到池中),如下图所示:

 

对应的调用栈如下所示:

  1. handle_connections_sockets 
  2.     |->poll/select 
  3.     |->new_sock=mysql_socket_accept(...sock...) /*从listen socket中获取新连接*/ 
  4.     |->new THD 连接线程上下文 /* 如果获取不到足够内存,则shutdown new_sock*/ 
  5.     |->mysql_socket_getfd(sock) 从socket中获取 
  6.         /** 设置为NONBLOCK和环境有关 **/ 
  7.     |->fcntl(mysql_socket_getfd(sock), F_SETFL, flags | O_NONBLOCK); 
  8.     |->mysql_socket_vio_new 
  9.         |->vio_init (VIO_TYPE_TCPIP) 
  10.             |->(vio->write = vio_write) 
  11.             /* 默认用的是vio_read */ 
  12.             |->(vio->read=(flags & VIO_BUFFERED_READ) ?vio_read_buff :vio_read;) 
  13.             |->(vio->viokeepalive = vio_keepalive) /*tcp层面的keepalive*/ 
  14.             |->..... 
  15.     |->mysql_net_init 
  16.         |->设置超时时间,最大packet等参数 
  17.     |->create_new_thread(thd) /* 实际是从线程池拿,不够再新建pthread线程 */ 
  18.         |->最大连接数限制 
  19.         |->create_thread_to_handle_connection 
  20.             |->首先看下线程池是否有空闲线程 
  21.                 |->mysql_cond_signal(&COND_thread_cache) /* 有则发送信号 */ 
  22.             /** 这边的hanlde_one_connection是mysql连接的主要处理函数 */ 
  23.             |->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

  1. size_t vio_read(Vio *vio, uchar *buf, size_t size
  2.   while ((ret= mysql_socket_recv(vio->mysql_socket, (SOCKBUF_T *)buf, size, flags)) == -1) 
  3.   { 
  4.     ...... 
  5.     // 如果上面获取的数据为空,则通过select的方式去获取读取事件,并设置超时timeout时间 
  6.     if ((ret= vio_socket_io_wait(vio, VIO_IO_EVENT_READ))) 
  7.         break; 
  8.   } 

即通过while循环去读取socket中的数据,如果读取为空,则通过vio_socket_io_wait去等待(借助于select的超时机制),其源码如下所示:

  1. vio_socket_io_wait 
  2.     |->vio_io_wait 
  3.         |-> (ret= select(fd + 1, &readfds, &writefds, &exceptfds, 
  4.               (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来实现超时时间的判定,如下面源码所示:

  1. size_t vio_write(Vio *vio, const uchar* buf, size_t size
  2.   while ((ret= mysql_socket_send(vio->mysql_socket, (SOCKBUF_T *)buf, size, flags)) == -1) 
  3.   { 
  4.     int error= socket_errno; 
  5.  
  6.     /* The operation would block? */ 
  7.     // 处理EAGAIN和EWOULDBLOCK返回,NON_BLOCK模式都必须处理 
  8.     if (error != SOCKET_EAGAIN && error != SOCKET_EWOULDBLOCK) 
  9.       break; 
  10.  
  11.     /* Wait for the output buffer to become writable.*/ 
  12.     if ((ret= vio_socket_io_wait(vio, VIO_IO_EVENT_WRITE))) 
  13.       break; 
  14.   } 

MySQL的连接处理线程

从上面的代码:

  1. mysql_thread_create(...handle_one_connection...) 

可以发现,MySQL每个线程的处理函数为handle_one_connection,其过程如下图所示:

 

代码如下所示:

  1. for(;;){ 
  2.     // 这边做了连接的handshake和auth的工作 
  3.     rc= thd_prepare_connection(thd); 
  4.     // 和通常的线程处理一样,一个无限循环获取连接请求 
  5.     while(thd_is_connection_alive(thd)) 
  6.     { 
  7.         if(do_command(thd)) 
  8.             break; 
  9.     } 
  10.     // 出循环之后,连接已经被clientdu端关闭或者出现异常 
  11.     // 这边做了连接的销毁动作 
  12.     end_connection(thd); 
  13. end_thread: 
  14.     ... 
  15.     // 这边调用end_thread做清理动作,并将当前线程返还给线程池重用 
  16.     // end_thread对应为one_thread_per_connection_end 
  17.     if (MYSQL_CALLBACK_ELSE(thread_scheduler, end_thread, (thd, 1), 0)) 
  18.         return
  19.     ... 
  20.     // 这边current_thd是个宏定义,其实是current_thd(); 
  21.     // 主要是从线程上下文中获取新塞进去的thd 
  22.     // my_pthread_getspecific_ptr(THD*,THR_THD); 
  23.     thd= current_thd; 
  24.     ... 

mysql的每个woker线程通过无限循环去处理请求。

线程的归还过程

MySQL通过调用one_thread_per_connection_end(即上面的end_thread)去归还连接。

  1. MYSQL_CALLBACK_ELSE(...end_thread) 
  2.     one_thread_per_connection_end 
  3.         |->thd->release_resources() 
  4.         |->...... 
  5.         |->block_until_new_connection 

线程在新连接尚未到来之前,等待在信号量上(下面代码是C/C++ mutex condition的标准使用模式):

  1. static bool block_until_new_connection() 
  2.     mysql_mutex_lock(&LOCK_thread_count); 
  3.     ...... 
  4.     while (!abort_loop && !wake_pthread && !kill_blocked_pthreads_flag) 
  5.       mysql_cond_wait(&x1, &LOCK_thread_count); 
  6.    ...... 
  7.    // 从等待列表中获取需要处理的THD 
  8.    thd= waiting_thd_list->front(); 
  9.    waiting_thd_list->pop_front(); 
  10.    ...... 
  11.    // 将thd放入到当前线程上下文中 
  12.    // my_pthread_setspecific_ptr(THR_THD,  this) 
  13.    thd->store_globals(); 
  14.    ...... 
  15.    mysql_mutex_unlock(&LOCK_thread_count); 
  16.    ..... 

整个过程如下图所示:

 

由于MySQL的调用栈比较深,所以将thd放入线程上下文中能够有效的在调用栈中减少传递参数的数量。

总结

MySQL的网络IO模型采用了经典的线程池技术,虽然性能上不及reactor模型,但好在其瓶颈并不在网络IO上,采用这种方法无疑可以节省大量的精力去专注于处理sql等其它方面的优化。

本文转载自微信公众号「解Bug之路」,可以通过以下二维码关注。转载本文请联系解Bug之路公众号。

 

责任编辑:武晓燕 来源: 解Bug之路
相关推荐

2020-06-17 16:43:40

网络IO框架

2022-03-03 08:01:41

阻塞与非阻塞同步与异步Netty

2021-07-14 09:48:15

Linux源码Epoll

2015-02-10 10:54:45

2022-02-21 10:21:17

网络IO模型

2021-03-10 08:20:54

设计模式OkHttp

2022-04-12 08:00:17

socket 编程网络编程网络 IO 模型

2024-07-08 12:03:41

2021-07-07 23:38:05

内核IOLinux

2018-02-02 15:48:47

ChromeDNS解析

2021-07-15 14:27:47

LinuxSocketClose

2019-03-17 16:45:09

RSA 2019网络安全

2020-10-10 07:00:16

LinuxSocketTCP

2021-06-10 09:52:33

LinuxTCPAccept

2017-04-05 20:00:32

ChromeObjectJS代码

2023-02-27 07:22:53

RPC网络IO

2020-09-07 14:30:37

JUC源码CAS

2022-08-09 12:34:22

网络安全企业安全

2012-12-28 09:53:22

网络安全电子商务

2021-12-30 08:55:41

Log4j2FastJson漏洞
点赞
收藏

51CTO技术栈公众号