张伟帆(网易严选技术团队)node
RedisTemplate是Spring封装的一个最经常使用的redis数据访问工具类,功能强大,使用简洁。但在RedisTemplate看似简单的API下,也是暗流涌动,背后别有一番洞天。本文从一个线上性能问题入手,抽丝剥茧一步步探索RedisTemplate,挖出了RedisTemplate操做psetex命令的深坑。git
在以前的压测过程当中,商详页面RT居高不下,发现yx_malltech_user这个redis集群的异常报警,起初是在业务没有开启分流读的状况下,一个Redis集群slave节点存在大量链接,占用大量CPU资源,且致使redis主从断掉等异常行为。github
dba分析后发现,从数据库实时监控平台,能够看到有大量的cluster命令产生,且这个请求一直存在,只是压测流量过大,致使问题放大,单个cluster命令请求都是在30毫秒以上。都是打到随机一个NODE节点上,致使这个节点cpu飙升。redis
而后DBA开始相应的处理,包括但不限于单独隔离异常节点到独立机器上,配置参数优化等,问题没有根本解决。能够肯定不是Redis服务端问题,应该是业务代码端问题,须要从业务用法开始排查使用方式,看是如何触发了大量的cluster命令。spring
6.9单压商详发现问题仍是存在。开始咱们的排查之路。数据库
既然是cluster命令异常过多,咱们观察下监控数据。 6.9号下午对于商详的单压缓存
能够看到cluster命令确实过多,且耗时较长。从而影响其余正常get,set操做。markdown
咱们仔细观察,发现异常的请求不只仅是cluster,仍是psetex。正常来说,咱们操做缓存并不会直接使用psetex命令。单独观察下二者。app
下面两张图分别是 psetex命令与cluster命令的走势图,发现是一致的。基本能够肯定psetex和cluster是有强关联关系的。框架
pset以下 cluster以下
且cluster只打到 10.130.68.239:16379 这一台机器上。
从上面观察获得,psetex命令和cluster命令基本同比例,且psetex命令请求量很多。而咱们开发中,实际上并不会使用pset,咱们都是redisTemplate.opsForValue().set()
这一点很奇怪,因此咱们开始撸下代码。
这里面封装了缓存逻辑,底层就是redisTemplate.opsForValue().set()方法,那咱们进去redisTemplate源码看下,它究竟偷偷作了啥。 在这个代码里面,咱们就明白了psetex哪里来的。若是你timeout使用的时间单元是ms,就会使用psetex,而其余就使用setex命令。感受咱们往前走了一步。可是psetex和cluster命令同比例为啥呢?继续看下。
这里面会根据key去计算对应的槽所在节点。咱们再进去topology方法里瞧瞧。
看到这段代码,真相就在眼前了。原来获取集群拓扑信息有一个缓存,失效时间100ms(这里吐槽下,spring竟然把缓存过时时间写死了)。缓存失效,而后从cluster的clusternodes循环取节点去发送cluster nodes命令来更新拓扑信息。这里就证实了cluste命令与psetex命令之间存在的关联性了。缓存过时时间是100ms,必然引发频繁的发起cluster nodes命令。经过和dba大大峰哥确认,确实在压测过程当中存在的大量cluster命令,就是cluster nodes命令。验证了猜测。
解决方法很简单:将设置缓存中使用毫秒做为过时时间的代码全都改为按秒级别来设置缓存。
只要改为秒级别,也就是使用setex,setex不会触发cluster命令。避免使用psetex命令,带来的频繁cluster nodes命令。
改完上线后,观察监控。发如今上线过程当中,明显有降低的趋势。 在平峰期表现,已经降到很是低了
最终的压测验证:
从压测结果来看,表现也是很是良好。问题得以解决。
核心链路压测中,总体商详的MRT也从以前100ms+降到66ms。
这个问题比较有意思,罪魁祸首竟然是一个时间单元。在于使用了 1000ms 和 1s 理论上应该是同样的,绝大部分人并不会去关心。然而spring底层留下了这么一个用法上的坑,致使了问题在大流量压测下的出现。一个小小的区别带来了巨大的影响。
而咱们对于缓存的过时时间其实不必精确到毫秒级别的,因此直接改为秒级别便可。
PSETEX和SETEX除了过时时间的精度问题,并无本质的区别。
因此spring提供了根据时间单元来区别使用setex和psetex,而接口没有显示体现。我我的以为这是spring接口设计一个不合理的地方。虽然是为了方便开发者要屏蔽细节,但是底层的逻辑并非彻底同样(不是只是发送不一样命令而已)。而是有获取cluster nodes等命令逻辑,是两套逻辑。对于咱们开发设计暴露出去的接口和方法也是一种警醒。一个接口,方法应该保证用户便于理解,不能有二义性。不然就得拆分多个接口,让用户选择,不给用户留坑。
后续优化措施:
前面咱们说到,还有一个问题点,cluster命令老是打到某一台节点上,致使这个节点cpu使用率飙升。
缘由仍是出在下面Spring-data-redis的这段代码上:
for循环遍历全部节点,从第一个开始发送nodes命令,若是获得返回,就直接返回了。而咱们节点信息通常状况下都是不变的。所以entrySet的顺序也不会改变,Cluster Nodes命令就会一直发送到同一个节点上。
在网上资料查到,这个bug已经官方修复了。jira.spring.io/browse/DATA…
嗯,这个就是直接摘抄网上资料了。
答案在下图,CLUSTER_SLOTS常量等于16384,所以redis每次都要循环不少次去组装每一个节点的slot信息。CPU至少须要循环16384乘以N次,N为redis集群master的个数。所以,随着redis集群规模的扩大、以及客户端节点数的增长,NODES命令打满CPU的问题会愈来愈严重。
redis系统命令NODES的性能问题,在2018年已经有反馈给redis官方:github.com/antirez/red…
PSETEX works exactly like SETEX with the sole difference that the expire time is specified in milliseconds instead of seconds.
PSETEX和SETEX除了过时时间的精度问题,并无本质的区别。 因此我我的理解,psetex应该是用在对于过时时间的精度要求较高的场景吧。
这次问题的定位排查,感谢dba大大在服务端作了大量的优化,且提供了很是好的分析方向,才能很快且明确的从业务代码上进行排查。在业务代码的排查过程当中,离不开文渊。十分感谢!!!
网易技术热爱者队伍持续招募队友中!网易严选,由于热爱因此选择, 期待志同道合的你加入咱们, Java开发简历可发送至邮箱:lizhuo01@corp.netease.com