导语
想想一下,如何快速研发出一套面向某个垂直领域的电商系统。在人手紧张的情况下,时间不足,为了能够完成任务可能就是使用JSP模板引擎等技术快速的开发出一套系统来。后台使用一台数据库服务器存储业务数据。如下图
这个架构图是我们每个人比较熟悉的,也是最简单的架构原型图,很多的系统在最开始的时候,都是这样子的,随着业务复杂度的提高,架构做了叠加,然后就看上去复杂起来了。
在说回上面提到的电商系统,系统一开始上线之后,虽然用户量不大,也运行平稳。很有成就感的一件事情。有一天运营的同学搞了一波活动进行了全网的推广。这个时候系统的访问速度就开始变慢了。
分析程序日志之后,发现了问题,访问慢的主要问题就集中在与数据库的频繁交互上,商品信息,商户信息,用户信息等等,都是从数据库中进行查询的,并且查询的速度非常慢,每一次的查询都与数据库建立的连接,操作完成之后关闭释放连接,这样的调用方式就会导致,每次操作SQL就要建立连接。那么就出现问题了,是不是因为频繁的建立数据库连接导致系统性能的损耗呢?
来看一个小例子
使用 tcpdump -I bond0 -nn -tttt port 4490 命令抓取了线上MySql建立连接的网络包,从抓取的结果来看,整个的MySQL的连接过程可以分成两个部分
- 第一部分是前三个数据包。第一个数据包是客户端向服务端发送一个SYN的数据包,第二个包是服务端回给客户端的ACK包以及一个SYN包,第三个包是客户端回给服务端的ACK包。其实这个就是TCP连接的三次握手机制。
- 第二个部分是MySQL服务端校验客户端密码的过程 其中第一个包是服务端发送给客户端要求认证的报文,第二个和第三个包是客户端将加密后的密码发送给服务端的包,最后两个包是服务端回给客户端认证OK的报文。从图中可以看到整个的过程大概消耗了4ms。整个抓包的过程是使用WireShack进行操作的。
那么单条SQL执行的之间是多少呢?每条SQL在执行的过程中平均时间可以消耗在1ms上,也就是说相比于SQL的执行,MySQL数据库连接的建立其实是比较耗时的一个过程。在请求量小的情况下影响并不是太大,但是一旦访问量增加的时候,无论是建立数据库连接还是执行SQL都是比较耗时的操作,但是相比而言,在一个1s的操作中建立连接占据了4/5,而执行SQL只占用的1/5。这样看其实,还是建立连接更耗时。
如何进行优化?
这就可以参考池化技术。将所有的数据库连接先通过数据库连接池来预先建立好,当一个SQL需要执行的时候就可以直接使用已经建立好的连接。由于是已经建立好的连接,所以说从连接的建立到销毁都是由数据库连接池来完成的,比较一个操作自己建立连接管理连接真的在性能上有了很大的提升。
如何用连接池预先建立连接
虽然池化技术在短时间内解决了具体出现的问题,那么它是如何管理整个的连接操作呢?
在平时的开发中会遇到很多池化技术解决的问题,例如数据库连接池、线程池、HTTP连接池。Redis连接池等等。这些连接池的管理核心其实就是连接池如何设计。以数据库连接池为例来说明一下。
首先数据库连接池有两个关键的配置,最大连接数和最小连接数,它们控制的是从连接池中获取连接的流程。
- 1、如果当前连接数小于最小连接数,则表示连接池中还有位置,可以创建新的连接请求数据库。
- 2、如果连接池中有空闲的连接,则就复用空闲连接
- 3、如果空闲池中没有连接并且连接数小于最大连接数,则创建新的连接处理请求;
- 4、如果当前连接数已经大于等于最大连接数,按照配置中设定的时间,等待旧连接的释放
- 5、如果等待超过了设定的时间,则需要向用户抛出错误。
主要是要理解这个流程中的设计思路,在后续的架构设计中会经常用到。
举个例子,加入在火车站有一家电动按摩椅小店,店里一共是10个椅子,这个类似于最大连接数,为了节省成本,平时的时候会保持有4个按摩椅是开着的,最小连接数,其他的6台都是关闭的。
有游客进来的时候,如果平时开启的4台椅子都是空的,那么直接去使用就可以了,但如果4台椅子已经都占满了,那么就需要新启动一台,直到10台按摩椅都被用完方停止。
那么当10台椅子都用完了,这个时候还有其他游客进来,那就要告诉游客在一段时间内会有椅子空出来,然后这个时候第11位游客就在等待,这个时候就会有两种结果,如果在一段时间内有椅子空出来那么这个顾客有可以直接过去使用,但是如果一段时间之后,没有空出来就需要让游客到其他地方试试了。
对于数据库连接池,一般在线上建议最小连接数控制在10左右,最大连接数控制在20~30左右就可以了。
&emsp 这个时候就需要对连接池中的连接进行维护的问题,就像是椅子一样,虽然是能用的但是保证不了会有故障发生,一般“按摩椅故障”原因有如下的几种
- 1、数据库的域名对应的IP发生的变化,池子中的连接还是用的旧的IP,当旧的IP下的数据服务关闭之后,再使用这个连接查询就会发生错误;
- 2、MySQL有个参数是wait_timeout ,控制这当数据库连接闲置多长时间后,数据库会主动关闭这条连接。这机制对于数据库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误。
但是作为老板,怎么保证你启动状态的椅子一定是可用的呢?
- 1、启动一个线程来定期的检测连接池中的连接是否可用,例如使用连接发送 “select 1” 的命令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭,目前C3P0连接池可以采用这种方式来检测连接是否可用。
- 在获取到连接之后,先校验连接是否可用,如果可用才会执行SQL语句,例如DBCP连接池的testOnBorrow配置项,就是控制是否开启这个验证。这种方式在获取连接时会引入多余开销。线上系统尽量不要开启。
到这里看上去一切都没有问题,但是在上面需求中有个问题,在一个非常重要的接口中,需要访问3次数据库,在高并发的场景下数据库访问次数增加,会增加系统开销,顺着这个思路。就又要对线程池进行优化了。
用线程池预先创建线程
在JDK1.5 中引入的ThreadPoolExecutor 就是这种线程池实现,它由两个重要参数:coreThreadCount 和maxTHreadCount ,这两个参数控制这线程池的执行过程。它的执行原理与上述的按摩椅原理相同。
- 如果线程池中的线程数少于coreThreadCount的时候,处理新的任务的时候会创建新的线程;
- 如果线程数大于coreThreadCount则把任务丢到一个队列中,由当前空闲的线程执行;
- 当队列中的任务堆积满的时候,则继续创建线程,直到达到maxThreadCount;
- 当线程数达到maxThreadCount的时候还有新的任务提交那么就不得不将其丢弃了;
首先JDK实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它比较适用于执行CPU密集型的任务,也就是需要执行大量CPU运算的任务,这是为什么呢?因为执行CPU密集型的任务时CPU比较繁忙,因此只需要创建和CPU核数相当的线程就好了,多个反而会造成线程上下文切换带来的效率低下的问题。所以当当前线程数超过核心线程数的时候,线程池不会增加线程,而是放在队列中等待核心线程空闲下来执行。
但,平时的开发的Web系统通常都有大量的IO操作,例如查询数据库、查询缓存等等。任务在执行IO操作的时候CPU就空闲下来了,这个时候如果增加执行任务的线程数而不是把任务暂存到队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐量,所以在Tomcat中的线程池就不是JDK原生的线程池,而是做了一些改造,当线程数超过coreThreadCount之后会优先创建线程,直到线程数达到maxThreadCount,这样就比较适合于Web系统大量IO操作的场景了,在实际使用的过程中可以进行参考。
其次,线程池中使用的队列的堆积量也是需要进行监控的重要指标,对于实时性要求比较高的任务来说,这个指标尤为关键。
在实际项目中曾经遇到过任务被丢给线程池中,长时间都没有执行的问题,这个问题是怎么出现的呢?就是coreThreadCount 和 maxThreadCount设置的比较小,导致任务在线程池里面大量的堆积,在调大了这两个参数之后问题得以解决。所以要将线程堆积量作为监控指标进行监控。
最后,如果使用线程池一定不要使用无界队列,开始认为使用无界队列可能会让任务永远都不会丢失,但是实际上,大量的数据还是会占用很多的内存空间,一旦空间被占满了就会频繁的触发Full GC,从而导致Stop-The-World ,然后就是服务不可用。
总结
池化技术有一个共同点:就是它们所管理的对象,无论是连接池还是线程池,它们在创建过程中都是比较耗时的操作,同样也消耗的很多的系统资源,所以要利用池化技术尽心管理,从而达到资源复用的目的。这是一种比较常见的软件设计思想。它的核心就是使用空间换取时间,使用预先创建好的内容来减少频繁创建带来的系统性能方面的开销,同时还可以对对象进行统一的管理,降低了对象的使用创建成本。
同样一个技术有好的方面,就会有瓶颈的地方。例如存储池子的对象肯定需要消耗多余的内存,如果对象没有被频繁使用到情况下,池子本身就是内存上的浪费,在如,池中的对象在系统启动的时候就创建好,这一个层面上其实增加了系统启动的时间。例如Spring 容器的装载对象,就是需要一个过程,这个过程就是一个比较消耗启动时间的过程。
可能这些缺陷对于优点来说,看上去消耗是微不足道的,所以说池化技术还是在实际开发中比较优化的一种方案。