惹不起的 Redis 热点 key(一):寻找热点 key

如今,明星们结婚离婚都流行官宣、电商网站也有各种秒杀活动、直播类也有很多大v,对于网站来说是好事儿,可以吸引大量用户关注,但对于做后端技术的同学可不是什么“好事”,因为这意味着同时在某一瞬间或阶段,流量会暴增,而且还会集中在某一个id、表、key等情况,当然换个思路想,也是好事,可以锻炼一下技术。

本文主要讨论Redis的热点问题,这是第一篇Redis热点key的发现。

还是先从一次故障(苦逼..)开始,今年元旦晚上,好像正在看某古老电视剧,突然一票报警和微信通知过来,线上某集群短时间不可用,影响了某重要核心业务(现在就怕听到核心这两个字),快速止损后,经过一晚奋战终于找到原因,可以看如下视频:

可以从视频里看到,某bigkey + hotkey把Redis打爆(CPU),proxy都hang在该节点,造成整个集群不可用。但事后,业务同学问我,热点key是哪个,能帮忙查下吗?我也是一脸懵逼,说实话我之前查热点key的通常都是在没有故障的情况下用faina查一下。

面对此等情况,岂能坐视不理,于是决定彻底调研并开发出一个“实用”的hot key检测系统。

一、分析路径

既然要找到它,首先要看它会落在哪,其实抽象的话就是四个部分:客户端、机器、proxy、redis server,如下图所示:

二、寻找热点key

1. 客户端

客户端其实是距离key"最近"的地方,因为Redis命令就是从客户端发出的,以Jedis为例,可以在核心命令入口,使用这个字典进行记录,如下所示。

public static final AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create();
public Connection sendCommand(final ProtocolCommand cmd, final byte[]... args) {
    //从参数中获取key
    String key = analysis(args);
    //计数
    ATOMIC_LONG_MAP.incrementAndGet(key);
    //ignore
}

使用客户端进行热点key的统计非常容易实现,但是同时问题也非常多:

(1) 无法预知key的个数,存在内存泄露的危险。

(2) 对于客户端代码有侵入,各个语言的客户端都需要维护此逻辑,维护成本较高。

(3) 规模化汇总实现比较复杂。

此种方法基本不靠谱,除非键值个数较少,否则进程有个大对象。

2. 代理

客户端对于proxy的访问通常是均匀的,所以理论上只要在一个代理完成热点key的统计即可。但是这样做的不多。一方面并不是所有Redis架构都是proxy模式,另一方面在proxy层做二次开发也需要投入相应的人力和时间。

此种方法不具有普遍性,而且改proxy源码虽然不难,但维护成本和稳定性需要考量。

3. redis server

(1) monitor

monitor命令可以监控到Redis执行的所有命令,下面就是它的模型:

下面为一次monitor命令执行后部分结果。

1477638175.920489 [0 10.16.xx.183:54465] "GET" "tab:relate:kp:162818"
1477638175.925794 [0 10.10.xx.14:35334] "HGETALL" "rf:v1:84083217_83727736"
1477638175.938106 [0 10.16.xx.180:60413] "GET" "tab:relate:kp:900"
1477638175.939651 [0 10.16.xx.183:54320] "GET" "tab:relate:kp:15907"
...
1477638175.962519 [0 10.10.xx.14:35334] "GET" "tab:relate:kp:3079"
1477638175.963216 [0 10.10.xx.14:35334] "GET" "tab:relate:kp:3079"
1477638175.964395 [0 10.10.xx.204:57395] "HGETALL" "rf:v1:80547158_83076533"

利用monitor的结果就可以统计出一段时间内的热点key排行榜,命令排行榜,客户端分布等数据,例如下面的伪代码统计了最近10万条命令中的热点key。

//获取10万条命令
List<String> keyList = redis.monitor(100000);
//存入到字典中,分别是key和对应的次数
AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create(); 
//统计
for (String command : commandList) {
    ATOMIC_LONG_MAP.incrementAndGet(key);
}
//后续统计和分析热点key
statHotKey(ATOMIC_LONG_MAP);

Facebook开源的redis-faina正是利用上述原理使用Python语言实现的,例如下面获取最近10万条命令的热点key、热点命令、耗时分布等数据。为了减少网络开销以及加快输出缓冲区的消费速度,monitor尽可能在本机执行。

redis-cli -p 6380 monitor | head -n 100000 | ./redis-faina.py

Overall Stats
========================================
Lines Processed     50000
Commands/Sec        900.48
Top Prefixes
========================================
tab         27565   (55.13%)
rf          15111   (30.22%)
ugc         2051    (4.10%)
...
Top Keys
========================================
tab:relate:kp:9350              2110    (4.22%)
tab:relate:kp:15907             1594    (3.19%)
...
Top Commands
========================================
GET         25700   (51.40%)
HGETALL     15111   (30.22%)
...
Command Time (microsecs)
========================================
Median      622.75
75%         1504.0
90%         2820.0
99%         6798.0

此种方法会有两个问题:

  • monitor命令在高并发条件下,会存在内存暴增和影响Redis性能的隐患,所以此种方法适合在短时间内使用。

  • 只能统计一个Redis节点的热点key,对于Redis集群需要进行汇总统计。

(2) --hotkeys

Redis在4.0.3中为redis-cli提供了--hotkeys,用于找到热点key,它的执行结果是这样的:

-------- summary -------
Sampled 382562 keys in the keyspace!
hot key found with counter: 231 keyname: user:125
.......

可以看到Redis当前的热点key是user:125。那它是怎么实现的呢?其实是Redis 4.0提供了maxmemory policy LFU功能,可以为键值对象提供了LFU(Least Frequently Used)的属性,如下图所示

可以看到Redis使用8位(0~255)来记录LFU,很显然是不够的,这里他是做了个取巧,使用概率算法来实现的(由于篇幅原因,这里不介绍了,可以参考阿里云-仲肥写的:https://yq.aliyun.com/articles/278922)

--hotkeys正是利用该属性完成对热点key的统计的,需要注意的是该选项必须配合maxmemory-policy的属性为LFU相关,例如:

config set maxmemory-policy allkeys-lfu
config set maxmemory-policy volatile-lfu

当然你可以自己扫描所有的key,使用object freq xx功能进行查看,--hotkeys在使用上有一些问题:

  • 如果键值较多,执行较慢,和热点的概念的有点背道而驰

  • 热度定义的不够准确。

所以,个人认为这个功能虽然不错,但是不够好(快)

4. 机器TCP包

Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。如果站在机器的角度,可以通过对机器上所有Redis端口的TCP数据包进行抓取完成热点key的统计,如下图所示。

此种方法对于Redis客户端和服务端来说毫无侵入,是比较完美的方案,但是依然存在3个问题:

(1) 需要一定的开发成本,但是一些开源方案实现了该功能,例如ELK(ElasticSearch Logstash Kibana)体系下的packetbeat[2] 插件,可以实现对Redis、MySQL等众多主流服务的数据包抓取、分析、报表展示

(2) 对于高流量的机器抓包,对机器网络可能会有干扰,同时抓包时候会有丢包的可能性。

(3) 维护成本过高。

不过确实有公司利用上述思路,开发了对应的系统,不太方便贴图,有需要的私聊。

5. 已知事件

其实还是有第五点的,因为事件是可以预知的,例如某个大V在某些平台直播、秒杀、红包等情况。即使不做业务的同学,对业务的适度了解对于问题的解决有时候也有奇效。

最后通过下给出上述四种方案的特点。

方案 优点 缺点
客户端 实现简单 内存泄露隐患
维护成本高
只能统计单个客户端
代理 代理是客户端和服务端的桥梁,实现最方便最系统 增加代理端的开发部署成本
服务端 实现简单 monitor本身的使用成本和危害,只能短时间使用
只能统计单个Redis节点
机器TCP流量 对于客户端和服务端无侵入和影响 需要专业的运维团队开发,并且增加了机器的部署成本

三、我是怎么开(折)发(腾)的?

其实上面的内容差不多是我书里写的,但我上面说了,其实我也就是用了一些faina。但是当问题真正出现时候faina基本是不行的:后知后觉、大集群怎么定位哪个节点、不够自动化等等问题。

但是客户端不靠谱、proxy没有通用性、机器有丢包或者维护成本的问题,该怎么办呢?技术不咋地但习惯投机取巧的我,找到了一个从技术看比较low,但很实用的方法,这个方法在日常的优化中解决了很多坑。

做法其实很简单,就是我手里有很多数据,比如每分钟每个实例的QPS、网络流量、CPU、内存等数据。如果某个集群的某些节点明显不正常,例如QPS明显高于其他节点,例如:

那么他就会进入黑名单,进入黑名单的节点,要做的事情就简单多了,执行monitor抓10000条数据,迅速关闭monitor的客户端,然后分析这10000条数据,基本上就能找到热点key,例如:

上述方法也有一个问题,就是如果热点真的就是一瞬间,此方法是抓不到的。还有上述方法都是放在CacheCloud(https://github.com/sohutv/cachecloud)每分钟自动抓取的,只需要修改一个CacheCloud配置即可,新版本的CacheCloud带有此功能,敬请期待。

四、总结

本文介绍了使用Redis时如何发现热点key,分析了各自方式的优劣势,最后给出我的一种投机取巧的实现方法,由于篇幅有限,没有介绍如何优化和预防热点key的发生,下篇文章我们继续。

附图一张,热点了(哈哈):

欢迎订阅我的公众号(Redis开发运维实战):关注Redis开发运维实战相关问题,干掉所有的坑。

由于本公众号是新号,没有留言功能,欢迎加我然后进群讨论:

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章