Redis 服务器 CPU 100% 问题调查

旁边的一个项目组在做压测时发现 TPS 一直上不去,经过几次测试发现有如下几个现象:

  • Redis 的 CPU 升到了 100% ,提了工单发现是短时间内创建了大量连接导致的。
  • 即使没有请求,仍然会不停的创建新连接,只是数量没有压测时大。
  • 总的客户端数( connected_clients )没有变化。

项目使用的是 SpringBoot 1.5.9.RELEASE ,连接池使用的是 JedisPool

刚开始以为是连接池没有起作用,但经过测试配置项是有效的(如 minIdlemaxTotal 等)。

仔细查看配置发现有几个配置项设置的好像有问题:

  • minEvictableIdleTimeMillis:500
    连接的最小空闲时间,达到此值后空闲连接将被移除。
  • softMinEvictableIdleTimeMillis:1000
    连接的最小空闲时间,达到此值后空闲连接将被移除,但是只有在空闲连接超过 timeBetweenEvictionRunsMillis 指定的时间后才会移除。
  • timeBetweenEvictionRunsMillis:1000
    空闲连接检查线程的运行时间间隔。

这几个配置项的单位都是毫秒,也就是说每一秒钟就会执行一次驱逐空闲超过半秒钟的连接。而由于 JedisPool 默认是 LIFO 的,导致连接很容易被驱逐。

但是将这几个配置项设置为 1800000 (即 30 分钟)后,压测时出现了一个更奇怪的现象:所有的请求都超时了(50s+),感觉像是在哪里卡住了。压测前单个请求时接口是这个正常访问的,压测后单个请求也无法响应。

仔细看了下代码,发现代码里获取缓存的方式有些混乱,有的地方直接使用了 Jedis 来操作缓存,有的地方使用了封装的工具类。这个本身并不会导致缺陷,但是我发现有个别地方出现了嵌套的缓存操作,导致获取的 Jedis 还没有关闭的时候再次尝试获取新的连接。压测时会导致所有的连接很快就被 borrow 了且创建的连接数也达到了 maxTotal 的上限,从而导致所有的请求都被卡住了。这个现象有些类似于多线程时资源争用产生的死锁,大量线程 hold 了一个 Redis 连接,再次尝试 borrow 一个新连接时已经没有资源可以获取了,只有等到 borrow 超时处理才会继续。而 borrow 的超时时间是通过 maxWaitMillis 配置的,默认值为 -1 ,即永不超时。

将上述问题修改后大部分处理都正常了,但是偶尔还是会出现 Could not return the resource to the pool 的异常,而且仍然会创建不少新的连接。

java 复制代码
redis.clients.jedis.exceptions.JedisException: Could not return the resource to the pool
    at redis.clients.util.Pool.returnBrokenResourceObject(Pool.java:103)
    at redis.clients.jedis.JedisPool.returnBrokenResource(JedisPool.java:239)
    at redis.clients.jedis.JedisPool.returnResource(JedisPool.java:255)
    at redis.clients.jedis.JedisPool.returnResource(JedisPool.java:16)
    at redis.clients.jedis.Jedis.close(Jedis.java:3409)

经调查发现这个还是由于代码的写法问题导致的。有一个方法在 try 中调用了一次 Jedisclose 方法,在 finally 中又调用了一次,导致在第二次 close 时发生了这个异常。在多线程的情况下,第二次 close 时这个连接可能已经被别的线程 borrow 出去了,这样就有可能引发一些意想不到的情况,甚至是业务错误。

这个缺陷修复后再次压测,从结果看基本上算是正常了,Redis 的 CPU 和连接数也比较正常,没有出现很大的波动。

本次调查过程中,发现了一些个人对一些配置项的错误理解:

  • minIdlemaxIdle 分别表示最小空闲连接数和最大空闲连接数,maxTotal 则表示允许的最大连接数。本以为是在应用启动后就会创建 minIdle 数量的连接,但其实并没有,而是在之后使用时才会创建需要的连接,才会逐渐达到 minIdle 的数量。
  • 在连接数超过 maxIdle 时本以为连接会在空闲时间超过 minEvictableIdleTimeMillis 后才会销毁,但查看过源码才发现每次在 return 回连接池时都有可能被销毁。每次 return 回连接池时都会判断 idleObjects 中的元素数量,超过 maxIdle 就会执行 destroy 处理。 也就是说如果要避免这种现象,需要将 maxIdlemaxTotal 设置成相同的值。或者将两个值设置的不要差别太大,应该也可以减少注销的次数。

在这个项目中还发现了另外几个比较常见的 Redis 错误用法:

  • 把功能开关的配置项保存在 Redis 中,并且每次都从 Redis 获取实时的值。
    这个会导致本来纳秒级(如使用获取全局配置变量)的操作变成了毫秒级,会极大的影响效率,而且也会导致这个 key 变成热点 Key ,从而引发其它问题。 配置的开关还是建议通过配置中心来实现。如果没有注册中心,也可以临时通过缓存来配置,但是一定要添加对应的本地缓存,并且设置一个可以接受的本地缓存的过期时间。
  • 使用了多个 db 但使用的是相同的连接池,导致每次都要执行一次 select db 操作。
    select 操作本身就是一个命令,未使用 multi 模式时会单独发送一次请求到 Redis 。如果能省掉这个操作,可以提升约一倍的性能。 这个可以考虑使用多个连接池,分别维护每个 db 的连接。另外也可以考虑使用同一个 db ,毕竟我看这个项目中使用的 key 并不做,总共也就百十个,完全没必要分成多个 db 。
  • testOnBorrowtestOnReturn 都设置为了 true
    这会导致每个 Redis 操作都会额外发送两个 PING-PONG 请求。 发送网络请求之类的 IO 操作耗时肯定是相对较长的,虽然可以避免一些由于服务器关闭了连接导致的错误问题,但是对性能的影响有些得不偿失,毕竟服务器出问题的几率很小。 个人感觉即使要开,也只需要把 testOnBorrow 的开关打开就足够了。一般情况下这两个都不需要开,只需要把 testWhileIdle 设置为 true 就可以了。

另外顺便提醒一下,使用 JedisPoolConfig 时需要注意:它在初始化时修改了 BaseObjectPoolConfig 的几个默认配置。

java 复制代码
public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    // defaults to make your life with connection pool easier :)
    setTestWhileIdle(true);
    setMinEvictableIdleTimeMillis(60000);
    setTimeBetweenEvictionRunsMillis(30000);
    setNumTestsPerEvictionRun(-1);
  }
}

从上面的代码可以很直观的看到修改的配置项,大多数情况下使用这个配置基本上没啥问题,如果需要修改再次设置一下就可以了,剩下的就是根据业务需求设置合适的连接数量就行了。

最后建议不要直接操作 JedisPool 来获取连接并操作 Redis ,还是推荐使用 RedisTemplate 之类的模板类。使用起来比较方便,还可以极大的减少因为使用不当而导致的奇怪问题。

相关推荐
水深火乐11 分钟前
安全地生成验证码、密码和 Token
后端
小强198823 分钟前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐25 分钟前
interface和any
后端
步行cgn26 分钟前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆29 分钟前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端
H_Peak33 分钟前
Python运算符与流程控制:if条件判断
后端
H_Peak35 分钟前
Python字符串操作:从基础到正则入门
后端
自进化Agent智能体36 分钟前
Hermes 子代理委派 —— 并行工作的艺术
后端
步行cgn39 分钟前
MyBatis 动态 SQL 中的 <foreach> 标签完全解析
后端
SomeB1oody2 小时前
【RustyML入门】5.0. 模型评估
开发语言·后端·机器学习·rust·教程