Hot Key(热 Key)
什么是热key
在高并发系统中某个Key短时间内产生了大量的访问,那么这个就变成了热Key,比如秒杀场景下瞬时产生的对产品的访问、某个帖子突然成为了热点等、或者恶意爬虫进行高频率地破坏性访问。
热Key危害
这种瞬时的高频词访问会给数据库带来巨大的负载,甚至导致系统瘫痪。一般来说,为了减轻mysql数据库的压力,会采用Redis等框架进行缓存,在高并发场景下Redis大多采用分片集群,Key会按照Hash规则映射到某个Redis分片上,日常运行是可以的,但如果某个数据成为了热Key,大量的访问请求会打到所在的Redis分片上去(Redis单机QPS大概在10w+),造成该分片集群瘫痪,由于Redis是单线程访问的,这将不仅影响了这个热key数据的访问,还会导致存储在该分片上的其他数据也无法访问。
如何解决热key问题
热Key复制
将热Key复制存储到多个数据分片(Key_copy1,Key_copy2…..)上来降低单个分片的负载,这种方式的问题在于需要提前得知哪些是热Key,提前进行处理,但很多时候是不太好预判的,如果对所有的Key都进行复制会带来很大的资源浪费问题。

读写分离
如果热Key主要来源于读请求,可以将集群设置为读写分离架构,主节点只负责处理写请求,提供一个代理服务将读请求分散到每个从节点上去。

二级缓存
读取到Redis中的信息后,借助Caffeine、Guava等框架把信息缓存在本机中, 对于变更频率低、实时性要求高的数据放到本地缓存中可以大大提高访问速度,但是要考虑如何更新本地缓存来保证一致性问题,而且这种方案对于热Key场景来说,缓存的不都是热Key,会导致命中率低的问题。(所以关键问题是如何识别某个Key是否为热Key)

限流
限制单个用户的请求频率,识别异常用户拒绝其访问,如采用sentinel等,但其实从本质上来看并没有提高并发能力,而只是提前拦截了一些请求负载。
热Key识别
其实二级缓存是一个很好的解决方案,如果能精准识别热Key并缓存热Key,就能提高缓存命中率,提高访问速度。所以需要进行热key探测。这里我觉得可以给Redis集群做一个Proxy,进行负载均衡转发的同时进行一个Key访问频率的实时统计,如果是热Key就可以在用户请求这个Key的时候给他返回一个附带的信息,告知他这个是热Key,然后本地进行一个缓存处理。但是这样的话Proxy可能会产生新的性能瓶颈。后来了解到了京东零售团队开源的JD-Hot Key就可以实现高效的探测。下面将介绍以下这个热Key识别方案:
JD-Hot Key解决方案
京东hotkey框架(JD-hotkey)是京东app后台研发的一款高性能热数据探测中间件,用来实时探测出系统的热数据,并将热数据毫秒内推送至系统的业务集群服务器的JVM内存。该框架的优点如下:
- 轻量级对本地代码侵入小
- 实时性强,
500ms即可探测出热key,而且可根据业务场景灵活调整 - 性能优秀,按照官方说明,一台8核8G的机器,在承担该框架热key探测计算任务时,每秒可以处理来自于数千台服务器发来的高达16万个的待测key,8核单机吞吐量在16万,16核机器每秒可达30万以上探测量。
- 低成本,高性能代表了低成本,仅采用10台机器,即可完成每秒近300万次的key探测任务

框架构成
该框架由四部分构成:
Client,在服务端通过引入Jar包的形式使用,可完成key上报,rule变化监听,worker信息变化更新,热key变化更新,本地缓存等功能。etcd集群,作为配置中心,提高高效地监听订阅服务,主要用于存放规则配置,维护worker信息,存储探测出的热Key等worker集群,与etcd建立连接,为client提供热key统计功能,数量达到阈值后将热key推送到各个clientdashboard,可视化界面
运行流程
JD-HotKey的运行流程如下:
client通过etcd获取规则、以及专属的worker地址信息,借助netty建立与worker的长连接client启动一个定时任务,没500ms(可配置),向worker发送一批待测key,基于hash规则进行映射,每个key会发送到同一个workerworker探测到热key后,推送给client,client自动进行本地缓存
对于client来说该框架主要是提供了几个api供调用:
boolean JdHotKeyStore.isHotKey(String key)该方法会返回该key是否是热key,如果是返回true,如果不是返回false,并且会将key上报到探测集群进行数量计算。该方法通常用于判断只需要判断key是否热、不需要缓存value的场景,如刷子用户、接口访问频率等。Object JdHotKeyStore.get(String key)该方法返回该key本地缓存的value值,可用于判断是热key后,再去获取本地缓存的value值,通常用于redis热key缓存void JdHotKeyStore.smartSet(String key, Object value)方法给热key赋值value,如果是热key,该方法才会赋值,非热key,什么也不做Object JdHotKeyStore.getValue(String key)该方法是一个整合方法,相当于isHotKey和get两个方法的整合,该方法直接返回本地缓存的value。 如果是热key,则存在两种情况,1是返回value,2是返回null。返回null是因为尚未给它set真正的value,返回非null说明已经调用过set方法了,本地缓存value有值了。 如果不是热key,则返回null,并且将key上报到探测集群进行数量探测。
可以看到使用还是很简单的,借助这几个api可以按照业务场景自定义热Key解决方案。
性能表现
参考官方技术文档:
etcd负载较轻,数千级别的客户端连接,平时秒级百来个的热key诞生,cpu占用率不超过5%,大部分时间在1%左右。worker是压力最大的一环,对于8核8G的单机,平均160万次/10s的负载下,cpu占有率为70%,加上gc有时会造成更高的负载,差不多是单机的极限了。而对于16核16G的单机压测表明可以在30万qps情况下稳定工作半小时以上。
参考: