redis问题
最近优惠服务的redis常常会间歇性的抖动,具体表现为在短时间内redis rt上涨显著,RedisCommandTimeoutException异样陡增,如下图:
监控面板是依照分钟级别进行统计,所以rt上涨看起来不是很显著。
这种状况必定不太失常,并且在近期呈现的频率有回升趋势。
定位起因
遇到这种问题,首先会想到是不是redis自身抖动造成的,看表象其实很像,无规律,间歇性,影响工夫很短,所以第一工夫找了DBA确认过后是不是redis实例产生了问题,或者网络呈现了抖动,同时也去dms redis的监控面板上看下运行指标是否失常。很遗憾,失去的复原是服务抖动这段时间内,redis运行状况失常,网络情况也无任何异样,而且从监控面板上看,redis 运行状况十分好,cpu负载不高,io负载也不高,内核运行rt也都失常,无显著稳定。(下图抉择了redis集群中的一个节点实例,16个节点的情况基本一致)
redis cpu:
redis io:
redis maxRT
到此,中间件自身的起因基本上是能够排除的了。那么,只能是应用姿态的问题了。应用姿态这块可能造成的影响,首先要定位是不是有hot key 还有big key,如果一个big key 又同时是hot key,那么极有可能在流量尖刺的同时造成这种景象。
先去阿里云redis监控面板上看hot key统计
发现一周内并无热点key,也没有大key,显然,缓存内容自身还是比拟正当的。这就有点头疼了,redis自身,以及缓存内容都没什么问题,那只能把眼光放到代码中了,由代码异样来逆推起因。
天眼监控上,发现很多RedisCommandTimeoutException异样,那么先采样看下产生异样的申请上下文
异样接口是:会场商品流批量算价服务
这个申请中用到了redis mget 同时获取多个keys,大略有几十个key,居然超时了,500ms的工夫都不够。
换个存在异样接口
能够这两个接口都用到了mget批量拉取keys ,从key的命名看来,还是依赖同样的数据,当然这不影响。下面咱们看到了redis 缓存的数据是没问题的,无大key 热点key,redis自身运行状态也衰弱,网络也失常,那么,只有一种可能,是不是这个mget有问题,mget是如何一次获取多个key的,带着疑难,咱们追一下mget的源码(零碎用的是Lettuce pool)
public RedisFuture<List<KeyValue<K, V>>> mget(Iterable<K> keys) { //获取分区slot和key的映射关系 Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys); //如果分区数小于2也就是只有一个分区即所有key都落在一个分区就间接获取 if (partitioned.size() < 2) { return super.mget(keys); } //每个key与slot映射关系 Map<K, Integer> slots = SlotHash.getSlots(partitioned); Map<Integer, RedisFuture<List<KeyValue<K, V>>>> executions = new HashMap<>(); //遍历分片信息,一一发送 for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { RedisFuture<List<KeyValue<K, V>>> mget = super.mget(entry.getValue()); executions.put(entry.getKey(), mget); } // restore order of key 复原key的程序 return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> { List<KeyValue<K, V>> result = new ArrayList<>(); for (K opKey : keys) { int slot = slots.get(opKey); int position = partitioned.get(slot).indexOf(opKey); RedisFuture<List<KeyValue<K, V>>> listRedisFuture = executions.get(slot); result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position))); } return result; });}
整个mget操作其实分为了以下几步:
- 获取分区slot和key的映射关系,遍历出所需key对应的每个分区slot。
- 断定,slot个数是不是小于2,也就是是否所有的key都在同一分区,如果是,发动一次mget命令,间接获取。
- 如果分区数量大于2,keys对应多个分区,那么遍历所有分区,别离向redis发动mget申请获取数据。
- 期待所有申请执行完结,从新组装后果数据,放弃跟入参key的程序统一,而后返回后果。
能够看到,当应用mget办法获取多个key,并且这些key还存在于不同的slot分区中,那么一次mget操作其实会对redis发动屡次mget命令的申请,有多少个slot,就发动多少次,而后在所有申请执行结束之后,整个mget办法才会可能继续执行上来。看似一次mget办法调用,其实底层对应的是屡次redis调用和屡次io交互。
(图a)
这张图就能很直观的看出redis 在集群模式下,mget的弊病。
问题优化:
计划1 - hashtag
hashtag 强制将key放在一个redis node上。这个计划,相当于将redis集群进化成了单机redis,零碎的高可用,容灾能力就大打折扣了,只能尝试应用主从,哨兵等其余分布式架构来缓减,然而,既然抉择了集群,必定集群模式是相比于其余模式是最合乎以后零碎架构现状的,应用这种计划,可能会引发更大的问题。不举荐。
计划2 -并发调用
咱们从图a,以及下面的代码中能够看到,for循环内屡次串行的redis调用,是导致执行rt上涨的起因,那么,自然而然能够想到,是否能够用并行代替底层串行的逻辑。也就是将mget中的keys,依据slot分片规定,先groupBy一下,而后用多线程的形式并行执行。
那么rt最现实的状况其实就是一次单机mget的rt耗时,也就是一次网络io耗时,一次redis mget命令耗时。
看似比拟完满的解决方案,其实不尽然,咱们考虑一下理论场景:首先,这个计划中,用于并发调用提交redis mget工作的线程池的设计十分重要,各种参数的调校,势必须要十分充沛的压测,这自身难度就比拟大。其次,咱们在日常应用中,一次mget的key基本上在几十到100,相比于redis 16384的固定槽位数量,是数量级上的差距,所以,咱们一次申请的这些key,基本上是散布在不同的slot中的,换句话讲,如果依照这么拆分keys,大概率是相当于拆出了等于key数量的get申请。。也就丢失了mget的意义。
两种计划各有利弊吧,计划一简略,然而架构层面的隐患比拟大,计划二实现简单,然而可靠性绝对比拟好一点。mget 始终是让人又爱又恨,要害还是看应用场景,key扩散到的redis集群节点越多,性能就越差,然而对于小数量级别,比方5~20个这种,其实问题都不大。
文/Hulk
关注得物技术,携手走向技术的云端