- 运行中的MySQL服务端和客户端本质上都是计算机上的一个进程
- 所以客户端进程想服务器进程发送请求并得到响应的过程本质上是一个进程间通信过程
- MySQL支持多种进程间通信方式
客户端和服务端连接的过程
TCP/IP
- 在真实环境中,数据库服务器进程和客户端进程可能运行在不同的主机中,它们之间必须通过网络进行通信
- MySQL采用TCP作为服务端和客户端之间的网络通信协议,客户端是通过IP地址+端口来唯一标识要连接到哪台服务器上的
- 默认情况下,MySQL服务端会监听本机的
3306
端口
问题:如果3306端口被占用了怎么办?
- 服务端:在启动服务器时,可以通过-P指定要监听的端口,比如如下服务器监听3307:
$ mysqld -P3307
问题:如果客户端和服务端不位于同一台主机上
- 启动客户端时必须通过-h命令指定要连接的服务器的IP地址,比如:
$ mysql -h192.168.0.21 -uroot -P3307 -p
UNIX域套接字
- 如果服务器和客户端在同一台主机上,那么可以通过UNIX域套接字进行进程间通信
服务端怎么做?
- 启动服务器后,服务器默认会监听
/tmp/sock.sock
这个域套接字 - 如果想监听别的域套接字,可以启动服务器时指定:
mysqld --socket=/tmp/a.txt
客户端怎么做?
-
启动客户端时,有如下三种情况之一发生时:
- 不指定主机名
- 指定主机名为localhost
- 指定了
--protocol=socket
的启动参数
-
客户端进程就会去连接本机目录下的
/tmp/sock.sock
。 -
如果想连接别的域套接字:
mysql -hlocalhost -uroot --socket=/tmp/a.txt -p
命名管道和共享内存
- windows下还支持服务器进程和客户端进程之间通过命名管道进行通信
小结
无论客户端和服务器进程采用哪种方式进行通信:
- 最后实现的效果都是客户端进程向服务器进程发送一段文本(MySQL语句)
- 服务器进程处理后再向客户端进程返回一段文本(处理结果)。
应用软件开发设计
为什么频繁创建链接会造成响应时间慢呢?
用tcpdump -i bond0 -nn -tttt port 4490
命令抓取了线上MySQL建立连接的网络包来分析。从抓包结果来看,整个MySQL的连接过程可以分为两部分:
- 第一部分是前三个数据包。第一个数据包是客户端向服务端发送的一个SYN包,第二个包是服务端会给客户端的ACK包以及一个SYN包,第三个包是客户端回给服务端的ACK包。这是一个TCP的三次握手过程
- 第二部分是MySQL服务端校验客户端密码的过程。其中第一个包是服务端发给客户端要求认证的报文,第二和第三个包是客户端将加密后的密码发送给服务端的包,最后两个包是服务端会给客户端认证OK的报文
从图中,你可以看到整个连接过程大概消耗了4ms(969012-964904)。
那么单条SQL执行时间是多少呢?
- 我们统计了一段时间的SQL执行时间,发现SQL的平均执行时间大概是1ms,也就是说相比于SQL的执行,MySQL建立连接的过程是比较耗时的。
- 这在请求量小的时候其实影响不大,因为无论是建立连接还是执行SQL,耗时都是毫秒级别的。
- 可是请求量上来之后,如果按照原来的方式建立一次连接只执行一条SQL的话,1s只能执行200次数据库的查询,而数据库建立连接的时间占了其中4/5.
解决方法很简单:只要使用连接池将数据库预先建立好,这样在使用时就不需要频繁创建连接了
用连接池预先创建数据库连接
数据库连接池有着两个最重要的配置:最小连接数和最大连接数,它们控制着从连接池中获取连接的流程
- 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求
- 如果连接池有空闲连接则复用空闲连接
- 如果空闲池没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求
- 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0的连接池配置是checkoutTimeout)等待旧连接可用
- 如果等待超过了这个设定时间则向用户抛出错误
一般在线上我建议最小连接数控制在 10 左右,最大连接数控制在 20~30 左右即可
这里还需要注意,连接可能会变得不可用,其原因可能如下:
- 数据库域名对应的IP发生了变更,池子的连接还是用旧的IP,当旧的IP下的数据库服务关闭后,再使用这个连接查询就会报错
- MySQL中有个参数是
wait_timeout
,控制着当数据库连接闲置多长时间后,数据库会主动的关闭这条连接。这个机制对库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误。
那怎么解决这些问题呢?
- 启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送
select 1
的命令给数据库看看是否会抛出异常,如果抛出异常就将这个连接从连接池中移除,并且尝试关闭。推荐这种方式 - 在获取到连接之后,先校验连接是否可用,如果可用才执行SQL语句。这种方式在获取链接时会引入多余的开销,不建议线上系统中使用
更进一步的,可以创建多个线程来并行处理和数据库之间的交互,这样速度就能更快了。但是在高并发阶段,频繁创建线程的开销也会很大,所以可以用线程池来解决
用线程池预先创建线程
JDK 1.5 中引入的 ThreadPoolExecutor 就是一种线程池的实现,线程池也有两个参数:coreThreadCount 和 maxThreadCount,这两个参数控制着线程池的执行过程:
- 如果线程池中的线程数少于coreThreadCount时,处理新的任务时会创建新的线程
- 如果线程数大于coreThreadCount,则把任务丢到一个队列里面,由当前空闲的线程执行
- 如果队列中的任务堆积满了的时候,则继续创建线程,直到达到 maxThreadCount;
- 当线程数达到 maxTheadCount 时还有新的任务提交,那么我们就不得不将它们丢弃了。
这个任务处理流程看似简单,实际上有很多坑,你在使用的时候一定要注意。
- 首先,JDK实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它比较适合执行CPU密集型的任务,也就是需要执行大量CPU运算的任务。这是为什么呢?
- 因为执行CPU密集型的任务时CPU比较繁忙,因此只需要创建和CPU核数相当的线程就i好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当当前线程数超过了核心线程数时,线程池不会增加线程,而是放在队列中等待核心线程空闲下来
- 但是,一般Web系统通常都有大量的IO操作,比如查询数据库等,任务在执行IO操作时CPU就空闲了下来,这时如果增加执行任务的线程数而不是把任务暂存在队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐率。所以Tomcat 使用的线程池就不是 JDK 原生的线程池,而是做了一些改造,当线程数超过coreThreadCount 之后会优先创建线程,直到线程数到达maxThreadCount,这样就比较适合于 Web 系统大量 IO 操作的场景了
- 其次,线程池中使用的队列的堆积量也是我们需要监控的重要指标,对于实时性要求比较高的任务来说,这个指标尤为关键。
- 最后,使用线程池一定不要使用无界队列(即没有设置固定大小的队列)。也许使用了无界队列之后,任务就永远不会被丢弃,只要任务对实时性要求不高,就早晚会消费完。但是,大量的任务堆积会占用大量的内存空间,一旦内存空间被占满了就会频繁触发GC,造成服务不可用
池化技术的优缺点
对于上面两种技术,它们有一个共同点:它们所管理的对象,无论是连接还是线程,都是创建过程比较耗时。所以,我们把它们放在一个池子里面统一管理起来,以达到提升性能和资源复用的目的。
这是一种常见的软件设计思想,叫做池化技术,它的核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用成本。
不过也有其缺陷,比如说存储池子中的对象肯定需要消耗多余的内存,如果对象没有被频繁使用,就会造成内存上的浪费。再比方说,池子中的对象需要在系统启动的时候就预先创建完成,这在一定程度上增加了系统启动时间。
可这些缺陷相比池化技术的优势来说就比较微不足道了,只要我们确认要使用的对象在创建时确实比较耗时或者消耗资源,并且这些对象也确实会被频繁地创建和销毁,我们就可以使用池化技术来优化。
注意:
- 池子的最大值和最小值的设置很重要,初期可以根据经验来设置,后面还是需要根据实际的运行情况调整
- 池子中的对象需要在使用之前就预先初始化完成,这个叫做池子的预热,比如说使用线程池时就需要先初始化所有的核心线程。如果池子未经过预热可能会导致系统重启后产生比较多的慢请求
- 池化技术的核心是一种空间换时间的优化方法实践,所以要关注空间占用情况,避免出现空间过度使用出现内存泄漏或者频繁垃圾回收等问题
小结
- 如何用池化技术优化系统性能?
- 一:用数据库连接池预先创建数据库链接:
- 一般在线上我建议最小连接数控制在 10 左右,最大连接数控制在 20~30 左右即可(如果有特殊需求当然特殊分析)
- 其预先创建的链接可能会变得不可用(MySQL中有个参数是
wait_timeout
,控制着当数据库连接闲置多长时间后,数据库会主动的关闭这条连接。这个机制对库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误。),建议启动一个线程来定期检测连接是否可用,比如使用连接发送select 1
的命令给数据库看看是否会抛出异常,如果抛出异常就将这个连接从连接池中移除,并且尝试关闭。
- 二:用线程池时需要注意哪些地方?
- 一定不要用无界队列,因为它会造成大量的任务堆积,所以一定要注意监控任务堆积量
- 线程池大小怎么界定:
- 对于I/O密集型的应用:
- 线程池的大小设置为2N+1
- 当任务超过线程数时,优先创建线程(直到达到maxThreadCount),这样就可以在单位时间内执行更多的任务
- 对于CPU密集型的应用:
- 线程池的大小设置为N+1
- 当任务超过线程数时,优先把任务放入队列中暂存起来。
- 对于I/O密集型的应用:
- 这种设置方式适合于一台机器上的应用的类型是单一的,并且只有一个线程池,实际情况还需要根据实际的应用进行验证。
在I/O优化中,以下的估算公式可能更合理:- 最佳线程数量 = ((线程等待时间+线程CPU时间)/ 线程CPU时间)* CPU个数
- 由公式可得:
- 线程等待时间所占比例越高,需要越多的线程。
- 线程CPU时间所占比例越高,所需的线程数越少。
- 一:用数据库连接池预先创建数据库链接: