07 池化技术:如何减少频繁创建数据库连接的性能损耗?

在人手紧张、时间不足的情况下,为了能够完成任务,一般会采用最简单的架构:前段一台web服务器运行业务代码,后端一台数据库服务器存储业务数据。

但是当用户出现大幅度增长时,系统的访问速度开始变慢。这时候慢的原因大概率出现在和数据库的交互上。因为数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。在这种调用方式下,每次执行SQL都需要重新建立连接。

为什么频繁的创建连接会遭横响应时间慢呢?看一个实际测试:

用"tcpdump -i bond0 -mm -tttt port 4490"命令抓取线上MySQL建立连接的网络包来做分析,从抓包结果来看,整个MySQL的连接过程可以分为两部分:
第一部分是前三个数据包。第一个数据包是客户端向服务端发送的一个“SYN”包,第二个包是服务端返回客户端的“ACK”包以及另一个“SYN”包,第三个包是客户端回给服务端的“ACK”包,显而易见,这是一个TCP三次握手的过程。
第二部分是MySQL服务端校验客户端密码的过程。其中第一个包是服务端发给客户端要求认证的保温,第二和第三个包是客户端将加密后的密码发送给服务端的包,最后两个包是服务端回给客户端认证OK的报文。从图中,可以看到整个连接过程大概消耗了4ms(969012-964904)。

image.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,这两个参数控制着线程池的执行过程。以下是原理过程:

image.png

image.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);
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • 最原始的数据库连接就是我们打开一个连接,使用过后再关闭该链接来释放资源。频繁的新建打开再关闭连接对jvm和数据库都...
    野柳阅读 6,229评论 1 11
  • 什么是数据库连接池呢? 数据库连接池(Connection Pooling)是程序启动时建立足够的数据库连接,并将...
    JunChow520阅读 4,364评论 0 21
  • 最近在看数据库相关的三方库的时候,我发现在Android应用开发的时候是可以并行操作数据库的读写,但Android...
    静默加载阅读 2,712评论 0 3
  • //联系人:石虎QQ: 1224614774昵称:嗡嘛呢叭咪哄 1.HTML5为什么只需要写? 答案解析:html...
    石虎132阅读 370评论 2 14
  • 2020年的一月是灰色的。新型冠状病毒肺炎的疫情爆发,NBA巨星科比和“老娘舅”李九松的相继离世....... 明...
    晓薇有话说阅读 223评论 0 1