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
关注得物技术,携手走向技术的云端