在解析borrowObject方法时,方法内部最开始的有一小段逻辑如下
AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
- 首先获取当前对象池的内部属性abandonedConfig,他是一个AbandonedConfig类的实例,赋值给ac局部变量
- 接下来进行一个逻辑判断,一共4个条件
- ac不为空
- ac的removeAbandonedOnBorrow属性为true
- 当前空闲对象的数量 < 2
- 当前活跃对象的数量 >(对象池最大数量-3)
- 如果判断为真,把ac当做参数传入,则执行removeAbandoned方法。
后两个条件都为真的时候,空闲对象最多1个,可供新申请的对象资源最多2个:说明对象池中的资源使用趋于饱和了,资源快耗尽了。这个时候新的的请求过来,如果请求量较大,1、2个剩余资源被争抢后,剩下的请求就只能排队等待或者直接返回异常了。
那么此时如果配置了遗弃对象管理机制的话,就可以针对活跃对象,进行一些检测,来移除那些长时间运行未被回收的对象。
我们首先来看下遗弃对象的管理配置都有哪些。也就是AbandonedConfig类都定义了哪些属性。
AbandonedConfig类解析
AbandonedConfig就是一个简单的java bean,里面定义了几个private属性以及提供了对应的getter、setter方法。我们主要对属性来进行解析。
public class AbandonedConfig {
/**
* Whether or not borrowObject performs abandoned object removal.
* 如果设置为true,就意味着,在borrowObject方法中(从对象池中申请对象的时候)就可以进行遗弃对象的相关清理逻辑。
* (当然是否能触发清理,还受限于其他条件)
*/
private boolean removeAbandonedOnBorrow = false;
/**
* Whether or not pool maintenance (evictor) performs abandoned object removal.
* 对象池本省可以通过GenericObjectPoolConfig配置后台异步清理任务,但是后台清理任务的主要职责是关注空闲(状态为空闲)对象的检测和清理。
* 如果removeAbandonedOnMaintenance设置为true,就意味着,在对象池的异步清理任务中,也可以进行遗弃(状态为活跃)对象的相关清理。
*/
private boolean removeAbandonedOnMaintenance = false;
/**
* Timeout in seconds before an abandoned object can be removed.
* 这个时间,默认300s。如果对象池的一个对象被占用了超过300s还没有被释放,就认为是被调用方遗弃了。
*/
private int removeAbandonedTimeout = 300;
/**
* Determines whether or not to log stack traces for application code
* which abandoned an object.
* 如果处理发了遗弃对象的回收和清理,是否要打印该对象的调用堆栈。
* 生产环境十分不建议设为true,会很消耗性能。
*/
private boolean logAbandoned = false;
/**
* PrintWriter to use to log information on abandoned objects.
* Use of default system encoding is deliberate.
* 这是一个日志输出器,用来定义调用堆栈日志的输出行为(输出到控制台、文件等)。默认输出到控制台。
* 如果logAbandoned为false,就不会有输出行为。
*/
private PrintWriter logWriter = new PrintWriter(System.out);
/**
* If the pool implements {@link UsageTracking}, should the pool record a
* stack trace every time a method is called on a pooled object and retain
* the most recent stack trace to aid debugging of abandoned objects?
* 这个UsageTracking从名字上来直观翻译,叫做使用追踪,指的就是池中对象的使用追踪。UsageTracking也是一个接口,GenericObjectPool实现了该接口。关于UsageTracking我们后续单独详细解析,
* 什么叫使用?
* 就是只要是调用了池中对象的任何方法,就算使用了池中对象。
* 什么叫追踪?
* 就是在池中某个对象的任何一个方法被调用时,都会创建一个调用堆栈快照。
* logAbandoned为true时,useUsageTracking也为true时,那么回收被遗弃的对象时,就会打印该对象最后一次的调用堆栈信息了。
* 如果useUsageTracking为true,即便是logAbandoned为false,那么每次对象的方法调用,一样还是会创建调用堆栈对象。只不过最终被回收时不会打印输出。
* 生产环境该属性也不建议设置为true。
*/
private boolean useUsageTracking = false;
}
通过以上的属性解析,我们就了解完了AbandonedConfig类的作用,接下来就可以来分析到底是如何处理这些被遗弃对象的了。
removeAbandoned方法解析
/**
* Recover abandoned objects which have been checked out but
* not used since longer than the removeAbandonedTimeout.
* 回收那些被取走的,但是超过了很长时间没有被使用的(被遗弃的)对象。
* 很长时间是多长的,取决于removeAbandonedTimeout配置了多少,默认300秒。
* @param ac The configuration to use to identify abandoned objects
*/
private void removeAbandoned(AbandonedConfig ac) {
// Generate a list of abandoned objects to remove
final long now = System.currentTimeMillis(); // 获取当前的毫秒时间戳
/*
ac.getRemoveAbandonedTimeout()获取的是AbandonedConfig的removeAbandonedTimeout属性值,默认300秒
当前的毫秒时间戳 - (removeAbandonedTimeout->默认300秒 * 1000) = 300秒之前的毫秒时间戳
*/
final long timeout =
now - (ac.getRemoveAbandonedTimeout() * 1000L);
ArrayList<PooledObject<T>> remove = new ArrayList<PooledObject<T>>(); // 初始化一个list,用来存储需要删除的对象池中的废弃的对象
Iterator<PooledObject<T>> it = allObjects.values().iterator(); // 对象池中所有对象的集合转换为一个迭代器
while (it.hasNext()) { // 遍历所有对象
PooledObject<T> pooledObject = it.next(); // 取出一个对象
synchronized (pooledObject) { // 锁定该对象
/*
如果该对象的状态为ALLOCATED,也就是使用中状态(使用中不一定就是执行中,也不一定真的还在被使用)
pooledObject.getLastUsedTime()返回的就是对象的lastUseTime属性值,见名知意,就是该对象上一次被使用的时间
这个值我们在解析borrowObject时提到过一点,就是在调用对象的allocte方法时,会把当前的毫秒时间戳赋值给lastUseTime
所以如果:lastUseTime < timeout ,说明该对象已经在(removeAbandonedTimeout -> 默认300秒)的时间里没有被再次使用了。
*/
if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
pooledObject.getLastUsedTime() <= timeout) {
pooledObject.markAbandoned(); // 把该对象标记为ABANDONED(可舍弃)
remove.add(pooledObject); // 把该对象添加到待移除列表中
}
}
}
// Now remove the abandoned objects
Iterator<PooledObject<T>> itr = remove.iterator(); // 待移除列表转换为迭代器
while (itr.hasNext()) { // 遍历所有待移除对象
PooledObject<T> pooledObject = itr.next();
if (ac.getLogAbandoned()) { // 如果logAbandoned配置为true
pooledObject.printStackTrace(ac.getLogWriter()); // 打印调用栈日志
/* 一般情况下,这个日志我们不关心,而且打印调用栈日志会消耗jvm性能,所以生产环境一般建议:logAbandoned不要设置为true */
}
try {
invalidateObject(pooledObject.getObject()); // 调用invalidateObject方法,来作废(销毁、释放资源)该对象
} catch (Exception e) {
e.printStackTrace();
}
}
}
小结
为什么会触发被遗弃对象移除逻辑
- removeAbandonedOnBorrow配置为true。基于内部校验逻辑,检测到池资源即将耗尽。
- removeAbandonedOnMaintenance配置为true。如果开启了后台异步回收机制,那么在每一个异步检测周期,也会顺带进行遗弃对象的检测和移除。
如何定义被遗弃对象
使用时间超过300秒,未归还的。
300秒可通过removeAbandonedTimeout进行配置。
如何查找被遗弃对象
从所有活跃对象中根据对象最后一次借出时间与当前时间比对。超过removeAbandonedTimeout设置的时间长度的。
如何处理被遗弃对象
调用invalidateObject方法来进行销毁。
根据logAbandoned配置来决定是否打印调用堆栈日志。
生产环境建议配置
logAbandoned、useUsageTracking都维持默认false设置。设置为true会对性能损耗较大。
- 堆栈信息的创建是基于Exception对象的创建来实现的。Exception对象是一个很重的对象,会抓取线程栈快照信息。
- 因为调用栈信息一般都比较庞大(想想我们的排查问题时的异常堆栈),这么多的信息通过日志打印输出,本身对IO消耗就会比较大。
AbandonedConfig如何使用
GenericObjectPool类提供了一个包含AbandonedConfig入参的构造方法以及一个public的setAbandonedConfig方法。
public GenericObjectPool(PooledObjectFactory<T> factory,
GenericObjectPoolConfig config, AbandonedConfig abandonedConfig) {
this(factory, config);
setAbandonedConfig(abandonedConfig);
}
public void setAbandonedConfig(AbandonedConfig abandonedConfig) throws IllegalArgumentException {
if (abandonedConfig == null) {
this.abandonedConfig = null;
} else {
this.abandonedConfig = new AbandonedConfig();
this.abandonedConfig.setLogAbandoned(abandonedConfig.getLogAbandoned());
this.abandonedConfig.setLogWriter(abandonedConfig.getLogWriter());
this.abandonedConfig.setRemoveAbandonedOnBorrow(abandonedConfig.getRemoveAbandonedOnBorrow());
this.abandonedConfig.setRemoveAbandonedOnMaintenance(abandonedConfig.getRemoveAbandonedOnMaintenance());
this.abandonedConfig.setRemoveAbandonedTimeout(abandonedConfig.getRemoveAbandonedTimeout());
this.abandonedConfig.setUseUsageTracking(abandonedConfig.getUseUsageTracking());
}
}
对象池内部,并没有直接使用外部传进来的AbandonedConfig对象,而是自己内部重新new了一个新的,然后把所有入参对象的属性拷贝到新对象上。这样做是为了保证对象池的安全,如果直接使用传进来的配置对象引用,那么该配置对象在池外被更改属性的话,会直接影响到对象池的行为。
在基于spring框架进行业务开发层面,几乎不会自己去new一个GenericObjectPool,我们最常用的对象池是连接池,基于common-pool的比较常用的连接池
- 数据库连接池:dbcp
- redis连接池:jedis、lettuce
dbcp和AbandonedConfig有关的配置
dbcp是一个老牌的,曾经非常流行,一个很多项目中都被使用的数据库连接池。他的底层就依赖common-pool(dbcp2依赖common-pool2)。目前数据库连接池这一块有很多后起之秀,比较有名气的应该就属druid和hikari,在监控、扩展、性能层面相比于dbcp更有优势。当然这两个不是我们本文分析的重点。我们还是看下dbcp中如何使用AbandonedConfig,通过dbcp的官网可以很容易的查到dbcp的配置参数。
除了最后一个属性abandonedUsageTracking的命名和useUsageTracking有些差异之外,其他属性都是同名的。
参考dbcp2官网:http://commons.apache.org/proper/commons-dbcp/configuration.html
jedis和AbandonedConfig有关的配置
jedis是一个redis的java客户端框架,用来提供针对redis进行操作的java api。可能截止目前还是应用最广的redis java客户端。他同样也提供redis连接池的功能。官网上有如下一段简短的描述说明了,redis的连接池要怎么配置。
To use it, init a pool:
JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
You can store the pool somewhere statically, it is thread-safe.
JedisPoolConfig includes a number of helpful Redis-specific connection pooling defaults. JedisPool is based on Commons Pool 2, so you may want to have a look at Commons Pool’s configuration. Please see http://commons.apache.org/proper/commons-pool/apidocs/org/apache/commons/pool2/impl/GenericObjectPoolConfig.html for more details.
这段描述重点提到了,JedisPoolConfig独享是用来配置连接池属性的,他里面只是默认初始化了几个jedis连接池中比较常用的属性值。因为JedisPool也同样是基于Commons Pool 2,所以如果要配置连接池的属性,可以完全参考Commons Pool 2的GenericObjectPoolConfig的配置说明即可。也就说JedisPool没有自己去定义多余的配置,也没有针对Commons Pool 2的配置做一些命名上的更改。
我们再看一下JedisPoolConfig的源码,你就会更加明了上面这段话的意思。
public class JedisPoolConfig extends GenericObjectPoolConfig {
public JedisPoolConfig() {
// defaults to make your life with connection pool easier :)
setTestWhileIdle(true);
setMinEvictableIdleTimeMillis(60000);
setTimeBetweenEvictionRunsMillis(30000);
setNumTestsPerEvictionRun(-1);
}
}
JedisPoolConfig继承了GenericObjectPoolConfig。默认的构造函数,设置了4个属性的值。
我们本文的重点不在于讨论这4个属性的意义,而在于讨论AbandonedConfig怎么配置。
AbandonedConfig和GenericObjectPoolConfig是两个不同的配置类。JedisPool目前的这个构造函数,支持了GenericObjectPoolConfig设置。如果需要jedis连接池能够支持遗弃对象(redis连接)的清理逻辑,那么等同于需要JedisPool支持AbandonedConfig的设置。也就是说要么JedisPool中有一个重载的构造函数可以传入AbandonedConfig,要么JedisPool能够提供一个set方法,用来设置AbandonedConfig。但是很可惜JedisPool没有提供这样的支持。也就意味着JedisPool不支持AbandonedConfig,也就不支持遗弃对象(redis连接)的清理逻辑。
JedisPool为什么不支持AbandonedConfig
你应该提出这样的疑问,毕竟JedisPool自己都说了他是基于Commons Pool 2,也就是说他的底层对象池一样是基于GenericObjectPool,但是GenericObjectPool是可以针对AbandonedConfig进行配置呀。
由于篇幅原因,这里不粘贴JedisPool的源码。我们看一下他的继承结构
主要原因是JedisPool并没有继承GenericObjectPool,他继承自Pool,内部维护了一个GenericObjectPool类型的internalPool属性。很像是包装模式,但是严格的从设计模式类关系分析的话不属于。因为JedisPool和GenericObjectPool并没有共同的父类或接口(Object类、一些标识性接口除外)。JedisPool只是依赖GenericObjectPool实现了自己的连接池。他从中屏蔽了他不需要的特性(AbandonedConfig)
lettuce和AbandonedConfig有关的配置
从Lettuce官网摘下来这样一段连接池的代码样例
RedisClient client = RedisClient.create(RedisURI.create(host, port));
GenericObjectPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport
.createGenericObjectPool(() -> client.connect(), new GenericObjectPoolConfig());
// executing work
try (StatefulRedisConnection<String, String> connection = pool.borrowObject()) {
RedisCommands<String, String> commands = connection.sync();
commands.multi();
commands.set("key", "value");
commands.set("key2", "value2");
commands.exec();
}
// terminating
pool.close();
client.shutdown();
参考lettuce官网:https://github.com/lettuce-io/lettuce-core/wiki/Connection-Pooling-5.1
可以看到创建一个连接池是通过ConnectionPoolSupport.createGenericObjectPool方法,最终返回的是GenericObjectPool类型。既然最终返回的就是GenericObjectPool对象,我们就可以通过调用setAbandonedConfig方法进行设置AbandonedConfig了。
总结
我们前面已经分析过AbandonedConfig的源码,知道他的主要作用是用来清理遗弃对象的。
遗弃对象的产生主要有两种可能:
- 对象执行的操作确实比较耗时,没有其他超时机制进行干预。比如:在数据库连接池中,一个连接执行了一个比较特别慢的sql。
- 对象执行的操作已经完成,但是状态没有变更。这种情况大多应该产生自编程失误(借了没还),例如:我们在使用了数据库连接池中的获取到的连接,执行完业务逻辑后,没有close。
通过我们前面对于dbcp、jedis、lettuce这三种基于common pool实现的连接池的使用分析,发现并不是所有的common pool下游框架都支持AbandonedConfig。
- dbcp是一个数据库的连接池,数据库的遇到慢查询的几率还是相对较大的,所以很有可能出现一个连接长时间执行不释放的情况。
- dbcp他本身诞生比较早,早于spring,当没有其他上层框架来更友好的管理连接的借用,归还的时候,编程失误的情况会很多,所以第二种情况产生的概率也会很高。
redis的出现较晚,jedis和lettuce出现时,spring已经应用非常广泛了。
- redis是一个基于内存操作的数据库,性能非常高,qps可达5-10万。除非是技术设计上的失误导致执行了特别耗时的类keys操作等,或者请求并发量巨大,否则不会产生特别严重的阻塞。
- jedis也许是一种乐观态度设计,认为高延迟概率很低、编程失误在测试和review阶段会被发现。
- lettuce也许是一种自由态度设计,给了使用方更大的自由,因为直接返回了底层的GenericObjectPool,使用方可以针对其追加设置额外属性。
- 但spring在集成lettuce时,并没有提供对AbandonedConfig的配置支持,也许spring认为,自身已经提供了更上层的api,对外能够屏蔽连接的管理机制,业务逻辑无需关心连接的借用和归还,编程失误会更进一步降低吧。