在人手紧张、时间不足的情况下,为了能够完成任务,一般会采用最简单的架构:前段一台web服务器运行业务代码,后端一台数据库服务器存储业务数据。
但是当用户出现大幅度增长时,系统的访问速度开始变慢。这时候慢的原因大概率出现在和数据库的交互上。因为数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。在这种调用方式下,每次执行SQL都需要重新建立连接。
为什么频繁的创建连接会遭横响应时间慢呢?看一个实际测试:
用"tcpdump -i bond0 -mm -tttt port 4490"命令抓取线上MySQL建立连接的网络包来做分析,从抓包结果来看,整个MySQL的连接过程可以分为两部分:
第一部分是前三个数据包。第一个数据包是客户端向服务端发送的一个“SYN”包,第二个包是服务端返回客户端的“ACK”包以及另一个“SYN”包,第三个包是客户端回给服务端的“ACK”包,显而易见,这是一个TCP三次握手的过程。
第二部分是MySQL服务端校验客户端密码的过程。其中第一个包是服务端发给客户端要求认证的保温,第二和第三个包是客户端将加密后的密码发送给服务端的包,最后两个包是服务端回给客户端认证OK的报文。从图中,可以看到整个连接过程大概消耗了4ms(969012-964904)。
![](https://img.haomeiwen.com/i9798631/1e327db84e772dfe.png)
相较于SQL的执行,MySQL建立连接的过程是比较耗时的,这在请求量小的时候影响不大,可是请求量很大时,如果按照原来的方式建立一次连接只执行一条SQL的话,1s只能执行200次数据库的查询,而数据库建立连接的时间占了其中绝大部分。
那么该如何解决呢?
需要使用连接池将数据库连接预先建立好,这样在使用的时候就不需要频繁的创建连接了。调整之后,1s就可以执行1000次的查询,性能大大提升。
用连接池预先建立数据库连接
其实在开发过程中会用到很多的连接池,比如数据库连接池,HTTP连接池,Redis连接池等。而连接池的管理是连接池设计的核心。
数据库连接池有两个最重要的配置:最小连接数和最大连接数,他们控制着从连接池获取连接的流程:
- 如果当前连接数小于最小连接数,则创建新的连接处理数据库的请求;
- 如果连接池中有空闲连接则复用空闲连接;
- 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
- 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间等待旧的连接可用;
- 如果等待超过了设定时间,向用户抛出错误。
对于数据库连接池,一般线上建议最小连接数控制在10左右,最大连接数控制在20-30即可。
在这里需要注意池子中连接的维护问题,一般情况下,故障原因可能有以下几种:
1.数据库的域名对应的IP发生了变更,池子的连接还是使用旧的IP,当旧的IP下的数据库服务关闭后,再使用这个连接查询就会发成错误;
2.MySQL有个参数是“wait_timeout”,控制着当数据库连接闲置多长时间后,数据库会主动的关闭这条连接。这个机制对于数据库使用方式无感知的,当我们使用这个被关闭的连接时就会发生错误。
那么如何保证连接一定是可用的呢?
1.启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除并且尝试关闭。目前C3P0连接池可用采用这种方式来检测连接是否可用。
2.在获取到连接之后,先校验连接是否可用,如果可用再执行SQL语句。比如DBCP连接池的testBorrow配置项,就是控制是否开启这个验证,这种方式在获取连接时会引入多余的开销,在线上系统中尽量不要开启,在测试服务器上可以使用。
至此连接池的工作原理已经清晰 。
假如在一个非常重要的接口中,需要访问3次数据库。根据经验判断,未来肯定会成为系统瓶颈。
进一步想,你觉得可以创建多个线程来并行处理与数据库之间的交互,这样速度就快了吗,但是频繁的创建线程的开销也会很大,于是顺着之前的思路继续想,猜测到了线程池。
用线程池预先创建线程
JDK 1.5中引入的ThreadPoolExecutor就是一种线程池的实现,它有2个重要的参数:coreThreadCount和maxThreadCount,这两个参数控制着线程池的执行过程。以下是原理过程:
![](https://img.haomeiwen.com/i9798631/bdbc76ab3ac9ccd8.png)
![](https://img.haomeiwen.com/i9798631/1cb02002edffd73b.png)
这个任务处理流程看似简单,实际上有很多坑,使用时一定要注意。
首先,JDK实现这个线程池优先吧任务放入队列暂存起来,而不是创建更多线程,它比较适用于执行CPU密集型的任务,因为执行CPU密集型任务时CPU比较繁忙,因此只需要创建和CPU核心数相当的线程就好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当当前线程数超过核心线程数时,线程池不会增加线程,而放在队列里等待线程空闲下来。
但是我们平时开发的Web系统通常有大量的IO操作,比方说查询数据库、查询缓存等。任务在执行IO操作时CPU就空闲了下来,这时如果增加执行任务的线程数而不是把任务暂存在队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐量。所以Tomcat使用的线程池就不是JDK原生的线程池,而是做了一些改造,当线程数超过coreThreadCount之后会优先创建线程,直到线程数达到max,这样就比较适合于Web大量IO操作的场景了。
其次,线程池中使用队列的堆积量也是需要监控的重要指标,对于实时性要求比较该的任务来说,这个指标尤为关键。
(在实际项目中遇到过任务被丢给线程池之后,长时间都没有被执行的诡异问题。经过排查发现,是因为线程池的coreThreadCount和max设置的比较小,导致任务在线程池里有大量堆积,调大了这两个参数后问题解决。任务堆积量是一个重要监控指标。)
最后,如果你使用线程池一定不要使用无界队列(没有设置固定大小的队列)。也许你觉得使用了无界队列后,任务就永远不会被丢弃,只要任务对实时性要求不高,总有消费完的一天,但是大量的任务堆积会占用大量的内存空间,一旦内存空间被沾满就会频繁的触发Full GC,造成服务不可用。
回顾一下这两种技术,会发现它们有一个共同点:它们所管理的对象没无论是连接还是线程,它们的创建过程都比较耗时,也比较消耗系统资源。所以我们把它们放在一个池子里统一管理起来,以达到提升性能和资源复用 的目的。
这是一种常见的软件设计思想,叫做池化技术。核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理。降低了兑现更多使用成本。
池化技术也存在一些缺陷,比如说存储池中的对象肯定需要消耗多余的内存,如果对象没有被频繁的使用,会造成内存上的浪费。再比如说,池子中的对象需要在系统启动的时候就预先创建完成。这在一定程度上增加了系统启动时间。
小结
- 池子的最大值和最小值的设置很重要,初期可以跟经验来设置,后面还需要根据实际运行做调整。
- 池中的对象需要在使用之前预先初始化完成,称之为预热,比如说,使用线程池时就需要预先初始化所有的核心线程。如果池子未经预热可能会导致系统重启后产生比较多的慢请求。
- 池化技术核心是一种空间换时间的实践,所以要关注空间占用情况,避免出现空间过度使用出现内存泄漏或者频繁的垃圾回收等问题。
node.js实现
mysql.js
//连接数据库
var mysql = require('mysql');
var pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database:'baseName'
});
module.exports = function(sql, callback) {
pool.getConnection(function(conn_err, conn) {
if(conn_err) {
callback(err,null,null);
} else {
conn.query(sql, function(query_err, rows, fields) {
conn.release();
callback(query_err, rows, fields);
});
}
});
};
调用
var query = require("./mysql.js");
query(sql, function(err, rows, fields) {
console.log(rows);
});
网友评论