本文字数:4066字
预计阅读时间:10分钟
一、何谓热点数据?
在某段时间范围内,经常被访问的数据可以称为热点数据。
以视频网站为例,热点视频主要是来自于观众的观看,那么热点视频可以分为如下几类:
永久性热点数据
比如某部热门电视剧,在其播出期间,观看频次非常高,那么在此期间,该电视剧可以算作永久性热点数据。
阶段性热点数据
周期性热点
例如最近的欧洲杯,比赛视频观看频次都会随着比赛场次呈现出周期性的波动。
突发性热点
主要来源于突发事件:例如地震报道,明星直播等等。
不同类别的热点视频区分主要依据是在某段时间范围内访问量的变化,也就是访问频率。
将热点数据缓存起来,将会明显提升应用的性能。
二、权衡
针对上面提到的三种热点数据,可以制定不同的缓存策略:
永久性热点:其访问频率随时间变化波动基本不大,这种数据尽量全部缓存起来。
突发性热点:其访问频率由于突发性而无法预估,故只能制定有效的检测手段。
周期性热点:虽然其访问频率随时间呈现周期性的波动,但是在每个周期内,都可以看做突发性热点。
如果所有的数据都能存储到进程内存中,岂不是能大大提升应用性能?
但是,实际情况却不是这样的。
针对java应用来说GC,会暂停整个应用。内存中缓存的对象越多,GC也会越频繁,GC总体时间也会越长。
而且针对动辄数千万的视频数据,其大小可能占到数百G的级别,也不可能全部放到内存中。
缓存有项重要的指标:命中率,就是访问某个数据时,该数据正好在缓存中,即为命中。
内存越大,缓存的数据就越多,理论上命中率就会越高。
内存越小,缓存的数据就越少,理论上命中率就会越低。
所以命中率本质上跟内存大小有直接的联系。
在命中率和内存大小之间权衡,以便获得最好的性能,是项技术活,于是各种各样的淘汰算法各显神通。
三、最佳命中率之Caffeine
当数据访问的概率随时间的变化是恒定的时候,LFU(Least Frequently Used)算法:淘汰使用频率最低的,缓存命中率可以达到最高。
因为它统计了缓存数据的访问频率(甚至包括历史数据),所以可以判断出那些是热的数据。
LFU的特点也给它带来了两个致命的缺点:
在大多数的实际场景中,访问频率会随着时间发生变化,而LFU无法自适应。
维护大量统计数据,内存消耗极大。
而另一种流行的淘汰机制LRU, 淘汰最久未被使用的数据,其能自适应时间变化,但命中率无法达到最高。
Caffeine采用了一种改进算法TinyLFU,解决了LFU的这两个缺点。下面来看一下TinyLFU的作用:
上图流程说明:
当有新数据需要放到缓存时,Cache将需要淘汰的数据,通过TinyLFU来决定是否用新数据替代被淘汰的数据,来提升命中率。
TinyLFU本质是布隆过滤器的变种,其使用Count–Min Sketch算法,可以用极小的内存,来实现大量数据统计:
这里存在三个问题,顺便说一下:
如果存在大量只访问一次的数据(长尾流量),那么TinyLFU中的计数器将被无效占用,从而影响频率判断。
TinyLFU如何反映时间变化影响的频率变化?
突发的数据可能还没有累计够足够的频率就被淘汰了,导致命中率下降。
一、长尾流量问题
Caffeine在TinyLFU之前加一个Dookeeper(BloomFilter),过滤这种低频数据:
如果一个元素,在Doorkeeper中,则直接插入TinyLFU的主结构,否则先插入Doorkeeper。
对于数据查询,会将Doorkeeper中的那一个计数值也加到计数值上去。
这样DoorKeeper就可以将低频数据拦截住,降低了计数器数量。
二、时间变化影响的频率问题
如果整体计数超过某个阈值,会对TinyLFU所有的统计计数进行衰减。
保障剔除历史频率很高但之后不经常使用的数据,另外也降低了内存消耗。
三、突发性的稀疏访问问题
Caffeine采用了Window TinyLFU机制来解决这个问题:
即,增加一个只占总体缓存1%大小的Window缓存(内部采用LRU算法),所有的新数据都先进入Window缓存。
当Window缓存满了,淘汰的数据进入TinyLFU流程,这个被称为W-TinyLFU算法,它保证了突发的数据有机会被保留下来。
另外W-TinyLFU的Main Cache淘汰算法采用了分段LRU算法,进而保障突发热数据有机会被保留下来。
W-TinyLFU算法已经被证明可以适应时间变化进而提供近似最佳的命中率。
四、热点探测
Caffeine很优秀,可是这里还有几个问题:
在本地内存中做统计,而对于互联网应用来说,通常会部署多个实例,流量均分到每个实例上,
这样,单个实例的数据情况无法代表整个应用的情况。所以从应用的整体角度看,可能某个数据是热的,但是在单个实例上并不明显。
当发现某个数据变热时,Caffeine只能在本地内存中生效,无法通知到其他实例。
如果在Caffeine之上,支持集中式热点数据探测,并且各个实例能够自动感知,岂不是更好?
我们已经知道,命中率跟数据的访问频率有很大关系,那么基于数据的访问频率做下筛选,即高于某个频率的数据才放到缓存中,那么就能保证缓存中的数据尽可能都是高频的。
热点探测原型如下:
主要分为两部分:
在客户端埋点,采用环形计数器统计数据的访问量,并定时上报统计数据。
探测器用于汇总客户端的统计数据,采用滑动窗口来做集中式探测,发现高频数据及时通知客户端缓存起来。
流程简单释义:
① 客户端key计数统计。
② 客户端统计轮切换。
③ 客户端推送统计数据到探测器的缓冲队列。
④ 探测器并行消费统计数据。
⑤ 探测器采用滑动窗口统计key的访问频率。
⑥ 探测器通知客户端Caffeine缓存热点数据。
上面的图示为了展示热点探测系统简化版流程,实际客户端实例有多个,探测器也是集群结构。
来看下采用热点探测系统如何来解决一、何谓热点数据中的三种热点数据:
永久性热点
该类数据的特点就是访问频率较为稳定,可以针对此类数据设置固定的频率阈值。
当此类数据在客户端快过期时,立马统计并上报访问情况,就可以保证该类数据一直缓存在客户端中。
突发性热点
该类数据特点就是突发性的高频访问,随着时间进行衰减,探测系统采用滑动窗口计数,
既能够保证达到阈值时及时探测到,又不会因为时间衰减而处于阈值边缘漏掉的情况。
周期性热点
周期性热点在每个周期内都可以看做是突发性热点,故不再赘述。
五、热咖啡
我们基于京东的hotkey项目,做了大量优化和改进,衍生出高性能,高可靠的热点数据探测系统 - HotCaffeine。
架构如下:
组件介绍:
etcd:配置中心,用于服务发现和配置等。
client:客户端SDK,用于数据收集上报和接收热key。
worker:用于接收客户端数据上报和热key计算。
dashboard:管理后台和数据展示。
etcd安全性介绍:
etcd的安全模型如下:
HotCaffeine采用如下鉴权保障安全性:
针对客户端,需要单独的用户名和密码,但是他们共同拥有同样的角色,就是client,而client对资源/hotcaffeine/只有只读权限
针对worker,由于是内部应用,单独建立一个worker的角色,对资源/hotcaffeine/具有读写权限。
针对dashboard,由于是内部应用,需要给各个用户赋权,故其拥有root用户和权限。
HotCaffeine功能介绍:
多缓存配置:
HotCaffeine客户端支持配置多个Caffeine缓存,并且支持动态调整缓存大小和过期时间。
灵活的热点规则配置:
所谓热点规则就是针对热点数据的滑窗统计数据进行配置,例如在3秒内达到5次访问即认为变为热点数据。
不同的热点规则支持对应到不同的缓存中,即热点规则和缓存缓存是多对多的。
而且支持前缀规则匹配和动态修改,业务端可以实时修改规则实时生效。
热点数据实时查看:
点击热key名可以查看热key在各个客户的实例对应的值:
调用量分布:
业务端很少知道自己缓存的数据分布情况,使用调用量分布功能业务端就能大概知道key的分布情况,用于设置缓存大小和过期时间等有很大帮助。
下面举个具体的例子来说明一下,假设业务系统在某段时间内共有如下6个key访问,访问总量为100:
那么,如果这段时间内,key6在缓存中的话,命中率就能达到50%。而key1和key2在缓存中的话,命中率仅为10%。
根据这个简单的例子,来看下调用量分布:
调用量=2的key有944个,那么调用量为2的key的总调用量为1888,它占本次统计的总调用量的比例为17.95%。
也就是说如果把调用量>=2的key都缓存下来,命中率为(1-43.86%)=56.14%。
左边第一列是调用量,也就是上面那个例子中说的访问量。
第二列是key的数量,根据这列可以大概知道key的多少(非重复),进而可以设置缓存大小。
第三列是调用量占比,比如 以第二行的数据为例来说明一下:
第四列是key的生存时间数据,可以参照个这个数据设置缓存的过期时间。
TopK热键:
Topk作为阈值规则的补充,从访问量的维度来选择热键,它的度量指标是,即访问量最高的k个键作为热键。
此曲线图纵坐标轴含义是topk的访问量与总量的比例,即可以认为,此段时间内,如果将这些topk的键缓存下来,命中率可以达到此比例。
鼠标放到曲线点上展示的是具体的数据情况。当点击该曲线点时,可以看到具体的topk数据,如下:
这里的调用量是指从key上报开始,如果后续持续的有数据上报会持续统计。
存活时间是指从key上报开始,至统计时的时间
点击key的名字即可看到该key实时的滑动窗口数据,如下:
吞吐优化:
起初,在16核32G内存的docker配置下,HotCaffeine最高只能支持35万key/秒的探测:
瓶颈主要体现在GC层面:
由于大量对象在内存中无法及时回收导致频繁GC,而且GC时间较长等问题。针对这个问题,采取降低探测热点数据缓存时间的办法,即仅满足客户端时间范围要求即可,使吞吐量提升至稳定支撑40万key/秒:
此时的瓶颈已经由GC转换到了CPU上,毕竟HotCaffeine是个CPU密集型应用,故尝试对锁竞争进行优化。
优化主要包括如下两个方面:
经过这些优化,HotCaffeine吞吐最终提升至59万key/秒:
将数据缓冲队列拆分为多个小队列,数据均匀hash到不同的队列,保障同样hash的数据肯定只在一个队列中,将并发变为并行处理。
针对某个待检测数据来说,后续的滑窗计数等不存在并发问题,故消除全部锁竞争代码。
接入效果:
下图为某个核心业务接入HotCaffeine后,客户端的响应情况:
响应由42ms降低至36ms,降低14%。
回馈社区:
目前HotCaffeine已经在github开源,地址:https://github.com/sohutv/hotcaffeine。
六、参考文献
Caffeine
万字详解本地缓存之王-Caffeine
W-TinyLFU论文
Segment LUR
对象存储系统中热点数据的研究
hotkey
也许你还想看
(▼点击文章标题或封面查看)
iOS:制作简易的 AAC 播放器 —— 了解音频的播放流程
【文末有惊喜!】详解:mach-o文件如何分析多余的类和方法