最近,项目收到中间件团队的报告,我们的应用连接他们的中间件(项目内部的MQ)连接数太大了,要求我们做一些调整。然后看了下我们的代码,发现我们接收和发送MQ消息的方式是每次新建一个连接然后关闭连接(询问了之前的同事,目前因为某些原因现在只能采取这种方式发送消息),但是每连接一次都会new一个对象出来,感觉挺占用资源的,而且万一没有及时释放掉资源,一直占用连接资源就对服务端压力也很大,讨论了一番决定采用连接池方式,这样我们可以自己控制连接数量。
我们基于【2W1h】方式来讨论连接池:什么是连接池(what)?我们内部项目的MQ为什么需要连接池(why)?怎么样做一个基于我们内的的MQ做连接池 (how)?
what: 什么是连接池?
深入思考连接池的本质,但不要思考的过于复杂~~
连接: 是网络中用于传输数据的通道; ”连接“才是我们要真正去使用的对象,“池”是用来管理多个连接的一种方式。
池: 是一种容器的概念,做存储的。在编程中我们往往使用数组,链表,队列,Map来表示。
所以“连接池”中的“连接”肯定是已经建立的好的长连接,比如tcp连接,websocket连接等,即取即用,用完放回。
跟据下游类型,我们常见的有数据库连接池,缓存连接池,服务连接池。在编程中,我们还会经常碰到进程池,线程池,协程池,内存池,对象池等。
why: 我们内部项目的MQ为什么需要连接池?
其实开头的第一句话已经回答了这个问题。
连接池除了能非常方便的对连接进行管理外,而且在高吞吐的连接池大大提高数据的传输的效率。提高效率主要在于下面两个方面:
1.避免反复的三次握手和四次握手
长连接的建立需要进行三次握手,而连接的释放需要进行四次握手,这是发生在系统层面的两个动作,对于单条连接来说耗时微乎其微,
但来高吞吐场景时,耗时则不能忽略。所以连接池的及取即用和用完放回的特性,避免了大量三次握手和四次握手的无效耗时,
从而节省系统资源。
2. 增加并行车道,实现全双工并行
数据通信包括单工,半双工和全双工。单工通信如下图,数据只能从A到B,不符合访问下游服务的场景。
半双工通信如下图,数据可以从A到B,也可以从B到A,但是同一时刻只能一个方向上的数据传输,通道利用率是50%。
全双工通信如下图,可同时存在从A到B和从B到A的数据传输,通道利用率是100%。长连接就是全双工通信。
在IO密集型的互联网应用中,一条全双工通信通道仍然无法满足数据吞吐的需求时,该如何解决?在互联网性能测试指标中有个这样一个公式:QPS(吞吐量)= 并发数/平均响应时间,在平均相响应时间不变的情况下,适度增加并发数可以提升吞吐量;所以采用多条双全工通信的方式可以在一定程度上提高吞吐量,而连接池就是最好的实现方式。
总结一下:为什么需要连接池?
1.方便管理连接 2.避免反复的三次握手和四次握手 3.更好的实现双全工并行
how: 怎么样做一个基于我们内的的MQ做连接池?
实现一个连接池,最关键的是均衡和保活,如下图:
我们项目组实现方式的思路如下:
1.MqClient类,初始化连接池,设置初始连接数量,最大连接数量,获取资源,释放资源,伪代码如下:
MqClient { ConcurrentHashMap<String queueKey,ArrayBlockingQueue<Mqconnection> queue> queueMap; long initConnect = 5; long maxConnect = 10; AtomicLong capacity; init(){ for(){ // 初始化5个连接 mqConnecions.offer(mqConnecion); capacity.getAndIncrement(); } } get(String queueKey){ // 根据queKey获取queue // 获取mqCoonnection if(mqConnection.peek()){ return mqConnection.poll(); } // 队列没有到最大值继续创建连接 if(capacity.get() < maxConnect){ // 继续创建新的连接,并塞入队列 } // 递归重试get() } free(String queueKey, Mqconnection connection){ // 如果可用并且队列大小没有超过最大连接值塞入队列,否则主动断开连接并丢弃 } }
2.MqClientHelper(辅助类,负责监听和保活)
MqClientHelper { MqClient mqclient; listenMqAcLog(){ // 监听mq相关的日志 } // 发送心跳 sendHeartbeat(){ } }
当然上面我们项目的第一版的方案的还是比较的粗糙的,还有很多地方不完善,比如有些方法需要加锁操作等,而且通常我们的连接池会连接下游多个节点。如下图所示:
一般来说,相对比较完善的连接池,有如下几个特性:
1.高可用:下游任意一个server宕机时,连接池关闭相关无效连接,防止被client访问; 2.可扩展:下游增加一个server节点时,连接池会发现并建立到新server节点的连接,供client访问; 3.负载均衡:连接池会根据下游server的服务能力的高低分配数据请求; 4.中间件:当下游server是类似MYSQL数据库并分片时,连接池会将请求打在相应的数据节点上,并对数据进行聚合;