缘起:
redis.clients.jedis.exceptions.JedisConnectionException:Could not get a resource from the pool
生产环境的业务服务器报了大量上面的错误。Jedis无法从连接池中获取一个可用的连接,所有客户端与Redis服务端保持通信的连接都在工作中,没有闲置的连接可以使用。
目前生产环境每天Redis的QPS在5000左右,连接池配置20个最大连接数貌似是真的很小,是不是增大连接池的配置就解决问题了?出现这个问题的根本原因是:连接池中的Jedis对象是有限的,如果Jedis一直被占用,没有归还,如果这时需要操作redis,就需要等待可用的Jedis,当等待时间超过maxWaitMillis,就会抛出could not get a resource from pool。以下几种场景会出现这个问题:
并发实在太高了,连接池中的连接数确实太小了,大量的请求等待空闲的连接。
由于Redis是单线程,某个查询太慢,阻塞了其他操作命令的执行。
Redis内部问题导致处理客户端的命令慢了,比如RDB持久化时,fork进程做内存快照;AOF持久化时,AOF文件重写时会占用大量的CPU资源;
大量key同时过期。
以下数据来自于CAT对缓存的监控数据:蓝线表示出现Could not get a resource from the pool的次数,绿线表示QPS,从图中可以看出随着QPS的升高,出现异常的次数也在增高,难道真的是因为QPS高,连接池数小的原因?
CAT上按照小时为维度获取缓存出现异常的数据如下:
从以下数据可以发现缓存出现异常的时间段都比较集中,而且间隔的时间段貌似存在着某种规律。出现问题的时间段也并不是每天QPS最高的时候,QPS最高的几个时间段反而没有出现任何异常。取了一个出现异常的时间段的缓存情况如下
发现这个时间段有几个比较耗时的操作命令,但是这几个命令在其他时间段最大耗时就10多毫秒。业务上也不存在不合理使用Redis数据结构的问题。是该看看缓存的监控情况了(这一部分图片没截)。
找运维看了Redis的情况,发现Redis的某个时间段CPU飙到100%了,这个时间段和出现异常的时间段吻合。问题基本已经确认,这个时间段Redis内部一定发生了点什么,导致处理客户端的请求变慢了,导致大量的请求被阻塞,超过maxWaitMillis时,集中出现了大量的Could not get a resource from the pool异常。
生产环境Redis的持久化策略是AOF,AOF会将所有的写命令按照一定频率写入到日志文件中,随着AOF文件越来越大,里面会有大部分是重复命令或者可以合并的命令(比如100次incr = set key 100),重写可以减少AOF日志尺寸,减少内存占用,加快数据库恢复时间。AOF重写的过程会fork一个子进程,导致CPU飙到100%了。在这种情况下即使增大接池连接数也没什么卵用。这个问题的解决思路是减少AOF重写的频率,两种方式:
让Redis决定是否做AOF重写操作,根据auto-aof-rewrite-percentage和auto-aof-rewrite-min-size两个参数,auto-aof-rewrite-percentage:当前写入日志文件的大小超过上一次rewrite之后的文件大小的百分之多少时重写;auto-aof-rewrite-min-size:当前aof文件大于多少字节后才触发
用crontab定时重写,命令是:BGREWRITEAOF
上面提到慢查询会阻塞Redis,那么业务开发同学在使用时如何避免呢?
避免让Redis执行耗时长的命令,绝大多数读写命令的时间复杂度都在O(1)到O(N)之间,O(1)的命令是安全的,O(N)命令在使用时需要注意,如果N的数量级不可预知,应避免使用,如对一个field数未知的Hash数据执行HGETALL/HKEYS/HVALS命令,通常来说这些命令执行的很快,但如果这个Hash中的field数量极多,耗时就会成倍增长
避免在使用这些O(N)命令时发生问题主要有几个办法:不要把List当做列表使用,仅当做队列来使用,严格控制Hash、Set、Sorted Set的大小,将排序、并集、交集等操作放在客户端执行,禁止使用KEYS命令
避免一次性遍历集合类型的所有成员,而应使用SCAN类的命令进行分批的,游标式的遍历SSCAN/HSCAN/ZSCAN等命令,分别用于对Set/Hash/Sorted Set中的元素进行游标式遍历
尽可能使用长连接或连接池,避免频繁创建销毁连接,使用pipelining将连续执行的命令组合执行