最近在做条线的系统重构需要用Java重新实现,除了业务之外我负责进行长连接支持的改造。在具体探讨通用对象池化技术之前简单介绍下我们的系统交互,我们的系统主要服务于保险企业,全国基本大型的保险企业都接入了我们的服务,保险企业通过我们提供的前置程序(类似于SDK)与我们的核心系统交互。我们本次改造的项目便是这个前置程序,我们的客户数量不多,只有一两百家,现在的前置程序通过短链接于核心服务交互,如果在业务量较大的时候会频繁的新建连接,出于性能考虑,我们需要将原来的短连接交互模式调整为长连接交互。 因为连接不释放,因此需要采用一些方法对长连接进行维护,这种方法也就是我们常说的池化技术。本文将对池化技术进行简单的介绍以及探讨通用池化技术应该有哪些API接口。

池化技术

概念

池化也就是将特定的资源进行统一的创建、存储和使用管理。池化技术本质上是对重量级资源的资源复用,可池化的资源常常具有以下特征。

  • 创建复杂,往往需要经过较多步骤才能创建成功且创建时间较长
  • 创建后可重复使用

典型的便是各种连接资源,例如数据库连接,常常会通过各种数据库连接池来对数据库连接进行池化,例如druid,Hikari等。

资源池化后有以下好处:

  • **资源重复利用,减少了资源分配和释放过程中的系统消耗。**例如在IO密集型应用中,常常需要通过线程提高系统的并发量,但是大量的线程的创建销毁会导致内核进行频繁的上下文切换带来不必要的开销。再例如IO密集型应用中进行网络通信时频繁的建立和释放连接也会加大系统的资源消耗。通过提前创建好资源将有效减少系统资源消耗。
  • 对系统资源进行整体限制。 池化后创建的资源是有限的,通常会有一个上界,避免资源的过度消耗。
  • 对资源的集中分配,在某些情况下可降低内存的碎片化问题

应用

池化技术有着广泛的应用,平常的开发中比较容易见到的是这些场景。

一是线程池,线程的创建和销毁会比较耗费系统资源,也较为耗时,通过提前创建好一组线程,等到请求来时借出时使用将会减少原有的消耗。

二是数据库连接池,这个也是比较常用的,例如我们常用的druid,Hikari等连接池。基本所有的数据库都采用TCP通信,一次完整的通信上最起码要进行三次握手和四次挥手,如果每一次通信都需要与数据库建立一次连接,整体的延迟将会提高,而通过提前建立好连接资源将会降低系统的句柄消耗,同时也免去了建立连接的开销。

三是内存池,例如在Java中的Integer的内存池,Netty中的内存池等,都是对内存的提前分配。在Java中我们通过new关键词进行对象内存的分配,但是很容易出现大量内存碎片。而内存池通过提前分配一大块内存区分,后续的分配都有池本身管理,可对该内存区域进行重复使用,并减少内存的碎片化

通用池化技术

上述的各类池都是应用于特定的场景,连接池管理连接资源,内存池管理内存的分配,线程池管理线程资源。上述池都是否存在什么共性,在我们实现自己的池的时候给我们提供些思路,这也是本文着重讨论的点—通用池化技术

核心API接口

我们见到的各种池,例如连接池、对象池,在本文中都统称为池。池本质上是一个容器,其最核心的操作便是从从池中获取资源和使用完毕后归还资源,因此通用池化技术API会有以下两个核心API接口。

  • borrow: 从池中获取指定的资源,并将池中维护的资源标记为已分配使用。
  • return(resource): 将从池中的获取的资源归还池,并将池中的资源标记为闲置。
  • close: 将池关闭,并将所有已分配的资源进行释放,然后拒绝提供服务

资源的状态管理

池除了要实现资源的借出和返还,也需要对池中的资源做维护,至少需要能够标记好资源的状态。最简化的资源状态应该有三种种对应三个核心API接口borrow、return和close。

对应borrow的状态可以被描述为已分配-ALLOCATED,对应return后的状态是闲置状态-IDLE 。池在初始化时将状态标记为IDLE ,所有资源均可借出使用;当调用borrow以后将资源标记为ALLOCATED 对应资源不允许被借出使用,是独占的;当对象返还时将资源标记为IDLE 可被后续其他请求借出使用;当池调用close接口后应该将所有的资源进行销毁,销毁时资源的状态应该标记为DESTORY

Untitled1.png

资源的创建和销毁

在前面我们提到池是通过提前创建和重复利用来降低系统资源消耗的,那么池是如何完成资源的创建的呢?常规来说池只负责资源的管理,其实是不需要管资源如何创建和销毁,我们应该是可以提供一个资源分配的处理逻辑给池,让池本身来调用,这个处理逻辑我们可以称之为资源工厂-ResouceFactory ,主要有两个核心的方法:

  • create: 根据实际的需要创建出指定资源,有池的使用者决定
  • destory(resource): 将对应的资源进行销毁和释放

池可以在初始化的时候就利用ResouceFactorycreate分配好对应的资源并存储下来,等到用时取用,不用时闲置,等到池关闭时统一释放并调用ResouceFactorydestory 进行资源销毁。

扩展1:资源失效驱逐和有效补充

对于有些有自身状态的资源,我们需要考虑资源失效后如何处理。举个例子,如果一个池中维护的资源是TCP连接,连接中断后,不能提供正常服务了。如果所有连接资源都中断了,池肯定提供出的连接都是不可用的,最后对导致系统的不可用,正常情况我们肯定是要避免这种情况的发生,因此我们需要能够判断什么是无效的资源,并对无效的资源进行驱逐(销毁)。对于资源的状态池都不应该直接操作,这些应该都是交由池的使用者来完成,因此,我们可以对池进行扩展。

首先对ResouceFactory 进行扩展,ResouceFactory 需要添加一个 check 方法进行资源检测:

  • check: 检查资源是否有效,如果资源有效应该返回true,如果资源已经失效或者无法判断应该返回false

池的扩展上,我们有多种方式来实现对失效资源的驱逐,各有优劣但相互互补。

第一种方式我们可以考虑在使用者调用池的borrow 方法时先取出一个资源,对资源进行校验,如果校验失败则将该资源进行释放,再新建一个资源填充。

第二种资源我们可以定期对资源进行检查,可以创建一个调度器,定期的对所有IDLE 资源进行校验,如果校验失败则销毁对象并新建填充。为了避免进行校验的时候资源被借出使用,资源应该需要在进行检查的时候切换的一个不同于IDLEALLOCATED 的状态-EVICT ,检查完成后回到IDLE 状态。

第一种方式能够在借出对象之前保证资源是可用的,第二种方式不能保证资源借出是可用的,但是能够在一段时间不用的时候维护好资源的可用性。

在第二种方式下,除了能够对资源进行定期的检查,还可以用于进行池中资源的资源的保活,例如连接保活,定期的向对端发送心跳数据探测两端的活动情况。

扩展2: 池的弹性扩展

有时候我们对池的要求能够做到弹性扩展,弹性的意思是当系统压力大时我们可以提供更多的资源,当系统压力小时我们可以仅提供部分资源,其余资源销毁及时的释放系统资源。在弹性扩展上常常会有三个核心参数来描述池的弹性能力:

  • MAX_IDLE: 池中允许的最大闲置资源个数,超过该值后,池需要将多余的资源进行清理。
  • MIN_IDLE :池中允许的最小闲置资源个数,为了保证有请求来时有资源直接可用,所以系统需要提前创建部分资源预热。
  • MAX_TOTAL: 池允许同时存在的最大资源个数,不能超过该值,该值大于等于MAX_IDLE,主要是在当系统压力变大时相应提供资源来提升系统性能,当系统压力变小后能够恢复到MAX_IDLE及以下的资源数量。

接下来我们描述池是如何进行扩展。我们假定池是惰性池,即一开始不分配资源,只有使用时才分配资源。当调用borrow 时如果可用资源小于MAX_TOTAL 则创建新的资源添加到池中。当池进行失效驱逐维护时可以资源的维护,如果资源个数大于MAX_IDLE,则进行多余资源的销毁,如果资源个数小于MIN_IDLE 时则补充新的资源,将资源始终维护在MIN_IDLEMAX_IDLE 之间。

我们还需要考虑一种额外情况,如果资源始终在MIN_IDLEMAX_IDLE 之间, 但是系统压力始终很小的情况。比如现在空闲资源就是MAX_IDLE ,但是系统现在只有MIN_IDLE 的需求,我们其实需要考虑将多余的资源进行销毁。对此池可以添加一个新的阈值(闲置时长)IDLE_TIME,池中维护好资源的上次归还时间,在进行驱逐的时候检查资源的闲置时间是否大于阈值,将最久未使用的资源进行销毁。

扩展3:异常处理

我们写的程序不可能没有bug,如果因为意外情况或异常没有正确的归还资源,那么池将会很快耗尽,池应该能够正常处理这种情况提升池的有效性。

我们可以设置类似扩展2中IDLE_TIME 在进行资源驱逐的时候将资源收回。我们可以将该值称为ABANDON_TIME ,当资源处于ALLOCATED 状态且超过ABANDON_TIME 后,就将资源进行销毁。

正确的使用池

在上一个章节中提到了对池的不合法使用将会造成不好的结果。本节将提供一个伪码描述如何正确使用一个池

p is a pool
--------------------
let r is null
try{
	r=p.borrow()
  // use r
  // .....
}catch(Exception) {
	if r != null {
		p.destory(r)
	}
	r == null
}finaly {
	if r != null { 
		p.return(r)
	}
}

当从池中借出对象后,如果发生异常应该直接将资源进行释放。如果没有发生异常则应该在资源使用完毕后归还池

总结

在本文中我们讨论了池化技术的概念和实际的一些场景。池化技术是为了将资源进行重复利用降低系统资源消耗的一种有效方式,但是池化技术会增加维护的成本,从价值层面上来说维护成本远远小于池化技术带来的性能提升的价值。

基于现在池各类应用场景继续讨论一个通用的资源池化技术,我们总结了池应该具有的核心API接口,描述了资源如何维护管理以及如何实现池的弹性化和异常处理。

以上内容来源于作者对Java线程池,Netty内存池和通用对象池化开源软件apache-common-pool2的研究总结,对于作者本人而言,对资源复用有了更高的理解。

参考资源

  1. https://commons.apache.org/pool/ Apache Common pool2
  2. Java 线程池源码
  3. Netty内存池源码
  4. https://blog.csdn.net/qq_41854911/article/details/121895526 池化基础和原理