线上Redis高并发性能调优实践

项目背景

  最近,作一个按优先级和时间前后排队的需求。用 Redis 的 sorted set 作排队队列。java

  主要使用的 Redis 命令有, zadd, zcount, zscore, zrange 等。redis

  测试完毕后,发到线上,发现有大量接口请求返回超时熔断(超时时间为3s)。数据库

  Error日志打印的异常堆栈为:缓存

    redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool服务器

    Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection timed out (Connection timed out)网络

    Caused by: java.net.ConnectException: Connection timed out (Connection timed out)并发

  且有一个怪异的现象,只有写库的逻辑报错,即 zadd 操做。像 zadd, zcount, zscore 这些操做所有能正常执行。socket

  还有就是报错和正常执行交错持续。即假设每分钟有1000个 Redis 操做,其中900个正常,100个报错。而不是报错后,Redis 就不能正常使用了。分布式

问题排查

1.链接池泄露?

  从上面的现象基本能够排除链接池泄露的可能,若是链接未被释放,那么一旦开始报错,后面的 Redis 请求基本上都会失败。而不是有90%均可正常执行。高并发

  但 Jedis 客户端听说有高并发下链接池泄露的问题,因此为了排除一切可能,仍是升级了 Jedis 版本,发布上线,发现没什么用。

2.硬件缘由?

  排查 Redis 客户端服务器性能指标,CPU利用率10%,内存利用率75%,磁盘利用率10%,网络I/O上行 1.12M/s,下行 2.07M/s。接口单实例QPS均值300左右,峰值600左右。

  Redis 服务端链接总数徘徊在2000+,CPU利用率5.8%,内存使用率49%,QPS1500-2500。

  硬件指标彷佛也没什么问题。

3.Redis参数配置问题?

 1 JedisPoolConfig config = new JedisPoolConfig();
 2 config.setMaxTotal (200);        // 最大链接数
 3 config.setMinIdle (5);           // 最小空闲链接数
 4 config.setMaxIdle (50);          // 最大空闲链接数
 5 config.setMaxWaitMillis (1000 * 1);    // 最长等待时间
 6 config.setTestOnReturn (false);
 7 config.setTestOnBorrow (false);
 8 config.setTestWhileIdle (true);
 9 config.setTimeBetweenEvictionRunsMillis (30 * 1000);
10 config.setNumTestsPerEvictionRun (50);

  基本上大部分公司的配置包括网上博客提供的配置其实都和上面差很少,看不出有什么问题。

  这里我尝试把最大链接数调整到500,发布到线上,并没什么卵用,报错数反而变多了。

4.链接数统计

  在 Redis Master 库上执行命令:client list。打印出当前全部链接到服务器的客户端IP,并过滤出当前服务的IP地址的链接。

  发现均未达到最大链接数,确实排除了链接泄露的可能。

 

5.最大链接数调优和压测

  既然链接远未打满,说明不须要设置那么大的链接数。而 Redis 服务端又是单线程读写。客户端建立过多链接,只会耗费资源,反而拖累性能。

     使用以上代码,在本机使用 JMeter 压测300个线程,连续请求30秒。

  首先把最大链接数设为500成功率:99.61%

  请求成功:82004次,TP90耗时目测在50-80ms左右。

  请求失败322次,所有为请求服务器超时:socket read timeout,耗时2s后,由 Jedis 自行熔断。

  (这种状况形成数据不一致,实际上服务端已执行了命令,只是客户端读取返回结果超时)。

  再把最大链接数设为20,成功率:98.62%(有必定概率100%成功)

  请求成功:85788次,TP90耗时在10ms左右。

    请求失败:1200次,所有为等待客户端链接超时:Caused by: java.util.NoSuchElementException: Timeout waiting for idle object,熔断时间为1秒。

   再将最大链接数调整为50,成功率:100%

   请求成功:85788次, TP90耗时10ms。

   请求失败:0次。

  综上,Redis 服务端单线程读写,链接数太多并没卵用,反而会消耗更多资源。最大链接数配置过小,不能知足并发需求,线程会由于拿不到空闲链接而超时退出。

  在知足并发的前提下,maxTotal链接数越小越好。在300线程并发下,最大链接数设为50,能够稳定运行。

  

  基于以上结论,尝试调整 Redis 参数配置并发布上线,但以上实验只执行了 zadd 命令,仍未解决一个问题:为何只有写库报错?

  果真,发布上线后,接口超时次数有所减小,响应时间有所提高,但仍有报错,没能解决此问题。

6.插曲 - Redis锁

  在优化此服务的同时,把同事使用的另外一个 Redis 客户端一块儿优化了,结果同事的接口过了一天开始大面积报错,接口响应时间达到8个小时。

  排查发现,同事的接口仅使用 Redis 做为分布式锁。而这个 RedisLock 类是从其余服务拿过来直接用的,自旋时间设置过长,这个接口又是超高并发。

  最大链接数设为50后,锁资源竞争激烈,直接致使大部分线程自旋把链接池耗尽了。因而又紧急把最大链接池恢复到200,问题得以解决。

  因而可知,在分布式锁的场景下,配置不能彻底参考读写 Redis 操做的配置。

7.排查服务端持久化

  在把客户端研究了好几遍以后,发现并无什么能够优化的了,因而开始怀疑是服务端的问题。

  持久化是一直没研究过的问题。在查阅了网上的一些博客,发现持久化确实有可能阻塞读写IO的。

 

  “1) 对于没有持久化的方式,读写都在数据量达到800万的时候,性能降低几倍,此时正好是达到内存10G,Redis开始换出到磁盘的时候。而且从那之后再也没办法从新振做起来,性能比Mongodb还要差不少。

  2) 对于AOF持久化的方式,整体性能并不会比不带持久化方式差太多,都是在到了千万数据量,内存占满以后读的性能只有几百。

  3) 对于Dump持久化方式,读写性能波动都比较大,可能在那段时候正在Dump也有关系,而且在达到了1400万数据量以后,读写性能贴底了。在Dump的时候,不会进行换出,并且全部修改的数据仍是建立的新页,内存占用比平时高很多,超过了15GB。并且Dump还会压缩,占用了大量的CPU。也就是说,在那个时候内存、磁盘和CPU的压力都接近极限,性能不差才怪。”  ---- 引用自lovecindywang 的博客园博客

 

  内存越大,触发持久化的操做阻塞主线程的时间越长

  Redis是单线程的内存数据库,在redis须要执行耗时的操做时,会fork一个新进程来作,好比bgsave,bgrewriteaof。 Fork新进程时,虽然可共享的数据内容不须要复制,但会复制以前进程空间的内存页表,这个复制是主线程来作的,会阻塞全部的读写操做,而且随着内存使用量越大耗时越长。例如:内存20G的redis,bgsave复制内存页表耗时约为750ms,redis主线程也会由于它阻塞750ms。”       ---- 引用自CSDN博客

 

  而咱们的Redis实例总内存20G,内存使用了50%,keys数量达4000w。

  主从集群,从库不作持久化,主库使用RDB持久化。rdb的save参数是默认值。(这也刚好能解释通为何写库报错,读库正常)

  且此 Redis 已使用了几年,里面可能存在大量的key已经不使用了,但未设置过时时间。

  

  然而,像 Redis、MySQL 这种都是由数据中台负责,咱们并没有权查看服务端日志,这个事情也很差推进,中台会说客户端使用的有问题,建议调整参数。

  因此最佳解决方案多是,从新申请 Redis 实例,逐步把项目中使用的 Redis 迁移到新实例,并注意设置过时时间。迁移完成后,把老的 Redis 实例废弃回收。

小结

  1)若是简单的在网上搜索,Could not get a resource from the pool , 基本都是些链接未释放的问题。

  然而不少缘由可能致使 Jedis 报这个错,这条信息并非异常堆栈的最顶层。

       2)Redis其实只适合做为缓存,而不是数据库或是存储。它的持久化方式适用于救救急啥的,不太适合看成一个普通功能来用。

  3)仍是建议任何数据都设置过时时间,哪怕设1年呢。否则老的项目可能已经都废弃了,残留在 Redis 里的 key,其余人也不敢删。

       4)不要存放垃圾数据到 Redis 中,及时清理无用数据。业务下线了,就把相关数据清理掉。