从MySQL源码看它的网络IO模型).笔者阅读Server源码的习惯是从它的网络IO模型入手。于是,就有了这个博客。MySQL启动Socket监控查看源码。首先,您需要找到它的切入点。mysqld的入口点是mysqld_main。跳过各种配置文件的加载,我们来到network_init初始化网络链接,如下图:下面是它的调用栈:mysqld_main(MySQLServerEntryPoint)|-network_init(initializenetwork)/*establishtcpsocket*/|-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)值得注意的是,在tcpsocket的初始化过程中,考虑了ipv4/v6的两种情况:首先createipv4connectionip_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);如果我们停止/启动mysql,就会出现之前的mysql监听端口还没有释放,无法绑定当前mysqlsocket的情况。此时mysql会循环等待,每次等待的时间为当前重试次数retry*retry/3+1秒,直到设置的-port-open-timeout(默认为0),如图下图中:MySQLnewconnectionprocessingcycle通过handle_connections_sockets来处理mysqlnewconnectioncycle,根据操作系统的配置通过poll/selectLoop来处理(不是epoll,所以可移植性高,不存在mysql瓶颈在网络上)。MySQL通过线程池方式处理连接(一个连接对应一个线程,连接关闭后线程归还池),如下图:对应的调用栈如下:handle_connections_sockets|->poll/select|->new_sock=mysql_socket_accept(...sock...)/*从listensocket获取新的连接*/|->newTHD连接线程上下文/*如果内存不够,shutdownnew_sock*/|->mysql_socket_getfd(sock)fromsocket在/**设置为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级别保活*/|->.....|->mysql_net_init|->设置超时时间,最大包等参数|->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的socket设置为非阻塞模式(O_NONBLOCK)模式,所以在vio代码中采用了非阻塞代码编写模式,如下源码所示:vio_readsize_tvio_read(Vio*vio,uchar*buf,size_tsize){while((ret=mysql_socket_recv(vio->mysql_socket,(SOCKBUF_T*)buf,size,flags))==-1){...//如果上面获取的数据为空,则使用select方法获取读取事件并设置超时超时时间if((ret=vio_socket_io_wait(vio,VIO_IO_EVENT_READ)))break;}}即通过while循环读取socket中的数据。如果读取为空,则通过vio_socket_io_wait等待(借助源码如下:vio_socket_io_wait|->vio_io_wait|->(ret=select(fd+1,&readfds,&writefds,&exceptfds,(timeout>=0)?&tm:NULL))在jdk源码中可以看到java的连接超时也是通过这个,select(...wait_time)来实现连接超时。从上面的源码可以看出,mysql的read_timeout是针对每个socketrecv的(不是整个packet),所以可能会出现超过read_timeout后mysql不会报错的情况,如下图:vio_write的实现方式vio_write与vio_read的实现方式一致。超时时间的判断也是通过select实现的,如下源码所示:,size,flags))==-1){intererror=socket_errno;/*Theoperationwouldblock?*///处理EAGAIN和EWOULDBLOCK返回,NON_BLOCK模式必须处理if(error!=SOCKET_EAGAIN&&error!=SOCKET_EWOULDBLOCK)break;/*等待输出缓冲区变为可写。*/if((ret=vio_socket_io_wait(vio,VIO_IO_EVENT_WRITE)))break;}}Mysql连接处理线程从上面的代码:mysql_thread_create(...handle_one_connection...)可以发现每个线程的处理函数MySQL的handle_one_connection,其流程如下图:代码如下:for(;;){//这里完成了握手和auth连接的工作rc=thd_prepare_connection(thd);//同上和往常线程处理一样,无限循环获取Connectionrequestwhile(thd_is_connection_alive(thd)){if(do_command(thd))break;}//退出循环后,连接已被clientduend关闭或发生异常//此处进行连接销毁动作end_connection(thd);end_thread:...//这里调用end_thread进行清理,将当前线程返回到线程池中重新使用//end_thread对应one_thread_per_connection_endif(MYSQL_CALLBACK_ELSE(thread_scheduler,end_thread,(thd,1),0))return;...//这里的current_thd是一个宏定义,其实就是current_thd();//主要是获取newthreadcontextstuffedthd//my_pthread_getspecific_ptr(THD*,THR_THD);thd=current_thd;...}mysql的每个woker线程通过死循环处理request线程的返回过程。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++标准用法互斥条件模式):staticboolblock_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);......//从等待list获取THDthd=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等方面的优化。本文转载自微信公众号《Bug解决之路》,可通过以下二维码关注。转载本文请联系BUG解决公众号。
