1. 异常现象
20190429 16:27:58,200 | ERROR | (RedisClient.java:262)RedisClient:262 - jedisInfo ... NumActive=8, NumIdle=0, NumWaiters=0, isClosed=false
20190429 16:27:59,462 | ERROR | (RedisClient.java:264)RedisClient:264 - GetJedis error,
redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:51)
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
...
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
at redis.clients.util.Pool.getResource(Pool.java:49)
2. 排查分析
2.1. Jedis 依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
确认没有问题。
2.2. Jedis 配置
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="testOnBorrow" value="true"/>
<property name="maxIdle" value="50" />
<property name="maxWaitMillis" value="3000" />
</bean>
疑点1:没有设置 maxTotal,使用默认值。MaxTotal 默认配置是 8。然而异常提示 8个资源正在被使用,连接池里没有了。
疑点2:Redis 连接得不到及时释放,其他线程无法申请到资源
导致 Redis 资源不足,无法从资源池获取到资源。与异常信息匹配:JedisException: Could not get a resource from the pool
2.3. Jedis 资源获取与释放
/**
* 获取Jedis实例
*
* @return
*/
private synchronized Jedis getRedisClient() {
int timeoutCount = 0;
try {
while (timeoutCount < MAX_TIMEOUT_COUNT) {
try {
return jedisPool.getResource();
} catch (JedisConnectionException e) {
timeoutCount++;
logger.error("getJedis timeoutCount={}", timeoutCount);
}
}
} catch (Exception e) {
if (jedisPool != null) {
logger.error("jedisInfo ... NumActive=" + jedisPool.getNumActive() + ", NumIdle=" + jedisPool.getNumIdle()
+ ", NumWaiters=" + jedisPool.getNumWaiters() + ", isClosed=" + jedisPool.isClosed());
}
logger.error("GetJedis error,", e);
}
return null;
}
/**
* 释放Jedis资源
*
* @param jedis
*/
public void closeResource(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
确认没有什么问题。
2.4. 默认值 – 源码
源码 GenericObjectPoolConfig.class
public class GenericObjectPoolConfig extends BaseObjectPoolConfig {
public static final int DEFAULT_MAX_TOTAL = 8;
public static final int DEFAULT_MAX_IDLE = 8;
public static final int DEFAULT_MIN_IDLE = 0;
private int maxTotal = 8;
private int maxIdle = 8;
private int minIdle = 0;
public GenericObjectPoolConfig() {
}
......
}
从源码可以看到,maxTotal 默认配置是 8,maxIdle 默认配置是8,minIdle 默认配置是0。确认使用的是默认配置,与异常信息一致。
NumActive=8, NumIdle=0, NumWaiters=0, isClosed=false
源码 JedisPool.class
/** @deprecated */
@Deprecated
public void returnBrokenResource(Jedis resource) {
if(resource != null) {
this.returnBrokenResourceObject(resource);
}
}
/** @deprecated */
@Deprecated
public void returnResource(Jedis resource) {
if(resource != null) {
try {
resource.resetState();
this.returnResourceObject(resource);
} catch (Exception var3) {
this.returnBrokenResource(resource);
throw new JedisException("Could not return the resource to the pool", var3);
}
}
}
Jedis 2.9.0 版本及以上的 JedisPool 的 returnBrokenResource() 和 returnResource() 方法被标注废弃了,取而代之的是 Jedis 的 close() 。
源码 Jedis.class
public void close() {
if(this.dataSource != null) {
if(this.client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
} else {
this.client.close();
}
}
所以,这里使用 jedis.close() 释放 Jedis 资源处理没有问题。
3. 解决方案
JedisPool 资源池优化,spring-jedis.xml 优化后配置:
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!--向资源池借用连接时是否做连接有效性检测(ping)。检测到的无效连接将会被移除。如果为true,则得到的jedis实例均是可用的-->
<property name="testOnBorrow" value="true"/>
<!--资源池中的最大连接数,默认8个-->
<property name="maxTotal" value="50" />
<!--资源池允许的最大空闲连接数,默认8个-->
<property name="maxIdle" value="50" />
<!--当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)。-->
<property name="maxWaitMillis" value="30000" />
</bean>
4. 参数说明
DruidDataSource配置兼容DBCP,但个别配置的语意有所区别。
Jedis 就是集成了 Redis 的一些命令操作,封装了 Redis 的 Java 客户端,提供了连接池管理。一般不直接使用 Jedis,而是在其上再封装一层,作为业务的使用。
Jedis 连接就是连接池中 JedisPool 管理的资源, JedisPool 保证资源在一个可控范围内,并且保障线程安全。使用合理的 GenericObjectPoolConfig 配置能够提升 Redis 的服务性能,降低资源开销。下面两表对一些重要参数进行说明,并提供设置建议。
参数 | 说明 | 默认值 | 建议 |
---|---|---|---|
maxTotal | 资源池中的最大连接数 | 8 | 参考关键参数设置建议 |
maxIdle | 资源池允许的最大空闲连接数 | 8 | 参考关键参数设置建议 |
minIdle | 资源池确保的最少空闲连接数 | 0 | 参考关键参数设置建议 |
blockWhenExhausted | 当资源池用尽后,调用者是否要等待。只有当值为 true 时,下面的 maxWaitMillis 才会生效。 | true | 建议使用默认值。 |
maxWaitMillis | 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)。 | -1(表示永不超时) | 不建议使用默认值。 |
testOnBorrow | 向资源池借用连接时是否做连接有效性检测(ping)。检测到的无效连接将会被移除。 | false | 业务量很大时候建议设置为 false,减少一次 ping 的开销。 |
testOnReturn | 向资源池归还连接时是否做连接有效性检测(ping)。检测到无效连接将会被移除。 | false | 业务量很大时候建议设置为 false,减少一次 ping 的开销。 |
jmxEnabled | 是否开启 JMX 监控 | true | 建议开启,请注意应用本身也需要开启。 |
空闲 Jedis 对象检测由下列四个参数组合完成,testWhileIdle 是该功能的开关。
名称 | 说明 | 默认值 | 建议 |
---|---|---|---|
testWhileIdle | 是否开启空闲资源检测。 | false | true |
timeBetweenEvictionRunsMillis | 空闲资源的检测周期(单位为毫秒) | -1(不检测) | 建议设置,周期自行选择,也可以默认也可以使用下方 JedisPoolConfig 中的配置。 |
minEvictableIdleTimeMillis | 资源池中资源的最小空闲时间(单位为毫秒),达到此值后空闲资源将被移除。 | 180000(即30分钟) | 可根据自身业务决定,一般默认值即可,也可以考虑使用下方 JeidsPoolConfig中的配置。 |
numTestsPerEvictionRun | 做空闲资源检测时,每次检测资源的个数。 | 3 | 可根据自身应用连接数进行微调,如果设置为 -1,就是对所有连接做空闲监测。 |
5. 关键参数设置建议
maxTotal(最大连接数)
想合理设置maxTotal(最大连接数)需要考虑的因素较多,如:
- 业务希望的 Redis 并发量;
- 客户端执行命令时间;
- Redis资源,例如 nodes (如应用个数等) * maxTotal 不能超过 Redis 的最大连接数;
- 资源开销,例如虽然希望控制空闲连接,但又不希望因为连接池中频繁地释放和创建连接造成不必要的开销。
假设一次命令时间,即 borrow|return resource 加上 Jedis 执行命令 ( 含网络耗时)的平均耗时约为 1ms,一个连接的 QPS 大约是1000,业务期望的 QPS 是 50000,那么理论上需要的资源池大小是 50000 / 1000 = 50。
但事实上这只是个理论值,除此之外还要预留一些资源,所以 maxTotal 可以比理论值大一些。这个值不是越大越好,一方面连接太多会占用客户端和服务端资源,另一方面对于 Redis 这种高 QPS 的服务器,如果出现大命令的阻塞,即使设置再大的资源池也无济于事。
maxIdle 与 minIdle
maxIdle 实际上才是业务需要的最大连接数,maxTotal 是为了给出余量,所以 maxIdle 不要设置得过小,否则会有 nwe Jedis (新连接)开销,而 minIdle 是为了控制空闲资源检测。
连接池的最佳性能是 maxTotal = maxIdle ,这样就避免了连接池伸缩带来的性能干扰。但如果并发量不大或者 maxTotal 设置过高,则会导致不必要的连接资源浪费。
您可以根据实际总 QPS 和调用 Redis 的客户端规模整体评估每个节点所使用的连接池大小。
使用监控获取合理值
在实际环境中,比较可靠的方法是通过监控来尝试获取参数的最佳值。可以考虑通过 JMX 等方式实现监控,从而找到合理值。
此类异常的原因不一定是资源池不够大,建议从网络、资源池参数设置、资源池监控(如果对 JMX 监控)、代码(例如没执行jedis.close())、慢查询、DNS等方面进行排查。
参考技术文档:
https://help.aliyun.com/document_detail/98726.html#section-m2c-5kr-zfb
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8