Java异常 | JedisException: Could not get a resource from the pool

  • Post author:
  • Post category:java


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 的服务性能,降低资源开销。下面两表对一些重要参数进行说明,并提供设置建议。

表 1. 资源设置与使用相关参数
参数 说明 默认值 建议
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 是该功能的开关。

表 2. 空闲资源检测相关参数
名称 说明 默认值 建议
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



版权声明:本文为itanping原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。