从 MySQL源码分析网络IO模型

 更新时间:2023年06月02日 14:48:43   作者:无毁的湖光-Al  
这篇文章主要为大家介绍了从 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 等其它方面的优化。

以上就是从 MySQL源码分析网络IO模型的详细内容,更多关于 MySQL网络IO模型的资料请关注脚本之家其它相关文章!

相关文章

  • MySQL/MariaDB中如何支持全部的Unicode

    MySQL/MariaDB中如何支持全部的Unicode

    MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,那么MySQL/MariaDB中如何支持全部的Unicode,感兴趣的朋友可以了解一下
    2021-08-08
  • Mysql锁内部实现机制之C源码解析

    Mysql锁内部实现机制之C源码解析

    数据库之所以要加锁,因为数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性
    2022-08-08
  • CentOS6.4上使用yum安装mysql

    CentOS6.4上使用yum安装mysql

    这篇文章主要为大家详细介绍了CentOS6.4上使用yum安装mysql图文教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • MySQL索引知识的一些小妙招总结

    MySQL索引知识的一些小妙招总结

    这篇文章主要给大家总结介绍了关于MySQL索引知识的一些小妙招,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • 阿里云ECS centos6.8下安装配置MySql5.7的教程

    阿里云ECS centos6.8下安装配置MySql5.7的教程

    阿里云默认yum命令下的MySQL是5.17****,安装mysql5.7之前先卸载以前的版本。下面通过本文给大家介绍阿里云ECS centos6.8下安装配置MySql5.7的教程,需要的的朋友参考下吧
    2017-07-07
  • 关于MySQL与Golan分布式事务经典的七种解决方案

    关于MySQL与Golan分布式事务经典的七种解决方案

    本文介绍了分布式事务的一些基础理论,并对常用的分布式事务方案进行了讲解;在文章的后半部分还给出了事务异常的原因、分类以及优雅的解决方案;最后以一个可运行的分布式事务例子,将前面介绍的内容以简短的程序进行演示,需要的朋友可以参考一下文章具体内容
    2021-10-10
  • MySQL忘记了root用户密码如何重置的解决方案

    MySQL忘记了root用户密码如何重置的解决方案

    MySQL是当前被广泛使用的关系型数据库,MySQL需要设置root用户的密码,用于验证登录数据库服务器,但往往可能由于各种原因导致忘记了该密码,于是就有了本次分享的内容,MySQL忘记root密码的解决方案,需要的朋友可以参考下
    2024-05-05
  • MySQL8忘记密码的快速解决方法

    MySQL8忘记密码的快速解决方法

    这篇文章主要给大家介绍了关于MySQL8忘记密码的快速解决方法,文中通过示例代码以及图片介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • MySQL Count函数使用教程

    MySQL Count函数使用教程

    这篇文章主要介绍了MySQL Count函数,COUNT()是一个聚合函数,返回指定匹配条件的行数。开发中常用来统计表中数据,全部数据,不为NULL数据,或者去重数据
    2022-12-12
  • MySQL 创建三张关系表实操

    MySQL 创建三张关系表实操

    这篇文章主要介绍了MySQL 创建三张关系表实操,文章说先创建学生表然后科目表和分数表三张有着密切关系的表,下文实操分享需要的小伙伴可以参考一下
    2022-03-03

最新评论