Appearance
Redis 热点数据
1. 什么是 Redis 热点?
我曾在我过往的博文中多次提到了局部性一词(关于局部性可以看下我之前的博文局部性原理),数据热点就是数据访问局部性的体现,具体表现就是 Redis 中某个 Key 的访问频次远大于其他剩余的 Key,我们也有一句俗语来形容这种现象旱的旱死,涝的涝死。

为什么 Redis 会有热点问题?这就得从 Redis 的原理说起了。众所周知,Redis 中存储的是 K-V 数据,在集群模式下,Redis 会将所有数据按 Key 的 CRC64 值分配到 16384 个数据槽(slot)中,并将这 16384 个数据槽分配到集群中各个机器上,尽可能实现数据在各个机器上的均匀存储。但均匀存储并不意味着均匀访问,有时候某个 Key 的请求会占到总请求的很大一部分,这就会导致请求集中在某个 Redis 实例上,将该 Redis 实例的承载能力耗尽,所有存储在这个实例上的其他数据也就无法正常访问了,这就意味着所有依赖于这些数据的服务都会出问题。
这里的出问题并不是说 Redis 会直接宕机,众所周知 Redis 的核心流程是单线程模式,这就意味着 Redis 是串行处理所有请求的,当请求过多时,请求就会拥堵起来,从应用层的视角来看,就是请求 Redis 的耗时会特别特别高。因为应用层使用 Redis 都是作为缓存,都是同步请求,所以会间接导致应用层的请求处理也会特别耗时,从而导致应用层请求也逐渐拥堵,最终整体不可用。
我们来举个简单的例子,相信大家都在微博上吃过瓜,当有大瓜出现时,微博上会迅速涌入一批用户检索相关信息,疯狂访问同一份微博(数据),这种情况下这份数据就是热点数据,如果数据太 “热” 最终会导致微博挂掉,实际上微博挂掉的情况已经出现很多次了,不是因为微博技术不行,而是因为热点问题太可怕。
2. Redis 热点是如何拖垮其他服务的?
Redis 的热点并不仅仅会导致单个服务异常,而是会导致所有依赖于此 Redis 集群的所有服务异常。上图中 Server2、Server、Server3 频繁访问 XXX_KEY,导致 RedisServer2 实例不可用,因为 Server4 依赖于 RedisServer2 上的 Key7,即便是 RedisServer 1 3 4 5 都正常服务,Server4 也无法对外提供正常的服务。

是不是有人会问 RedisServer2 挂了,不能把它下掉,换一个新的机器上去吗?其实在 Redis 集群模式下,某个实例宕机 Redis 集群会自动将其替换。但现在的情况是,即便是替换了新实例,大量的请求也会一下子涌进来将其压挂。所以面对 Redis 热点问题,重启之类的手段是无效的,只能从请求端解决问题。
其实这就是一个很明显的木桶模型,一个木桶所能装水的多少取决于木桶上最短的那块木板,当应用使用 Redis 集群时,Redis 集群的性能上限并不完全等于单实例上限乘以实例个数。但当 Redis 集群中任一实例有问题,上层感知到的就是整个 Redis 集群有问题。

3. Redis 热点如何避免?
上文也说到,热点问题其实是局部性问题,而局部性问题的避免其实非常难,任何分布式系统几乎都会受局部性的影响。面对这种问题,说实话没有绝对可以避免的方式,只能提前通过分析数据的特性,做好相应的措施。说白了就是靠经验,不知道大家有啥其他好的思路,可以在评论区探讨下。
4. 热点问题如何排查?
Redis 的热点的问题其实算是很好查的,就是靠监控数据,监控 Redis 各个实例的 CPU 使用率、QPS 数据,如果你看到 Redis 集群中某些实例负载和 QPS 特别高,但其他实例负载很低,不用问肯定是出现热点问题了,接下来你需要做的就是找出具体的热点 key,并且找出数据访问的来源。
找出热点 Key 其实也很简单,抓部分访问日志,然后统计下很容易就看出来了。但比较难的是找出数据访问的来源,像我之前所在的公司,同一个 Redis 集群是被很多业务所共享的,但 Redis 的访问并为被纳入到全链路监控的数据中,所以找出访问来源最直接的方式就是在群里面问,听起挺原始的,但也没有其他方法。
5. 热点问题如何解决?
虽然解决问题最好的方式是避免问题的发生,但我刚才也说了,Redis 热点其实很难避免,任何业务中热点一定有,但不一定会造成灾难而已。热点的发现不一定非得是事故引出的,我们也可以在日常工作中定期巡检,一有发现热点的苗头,直接将其扼杀。
对于发现热点后如何解决,我这里提供两个我的解决思路,大家可以探讨下:
5.1. 应用层 Cache

常用的实现方式就是在应用里实现本地缓存(LocalCache),就相当于是对 Redis 数据又加了一层 Cache,对于那些非常热的热点数据,应用层有极大的概率能在本地缓存中找到数据,只有极小部分 LocalCache 数据过期时的请求会漏到下面,这样热点数据的请求在应用层内部就能消化,从而极大减少对 Redis 的压力。
这种实现方法的话,仅需要数据读取端做改造,数据写入端完全不需要改造。然而缺点的话也很明显:
- 需要各端自行实现,会增加应用层开发和维护成本;
- 会额外浪费各端的存储空间;
- 需要针对性开发,不适合大范围推广;
5.2. 增加数据副本
既然热点问题是因为某个 Key 被大量访问导致的,那我们将这个 Key 的请求做下拆分不就行了。例如,原始的热点 Key 叫做 XXX_KEY,我们在数据写入的时候,可以用不同的 Key 重复写 10 份,比如 XXX_KEY_01, XXX_KEY_02 …… XXX_KEY_10,访问的时候在原始 Key 上随机凭接一个 1-10 之间的后缀即可,这样就能实现数据请求的分散,如果想让请求更分散,可以存储更多的副本。

这种方案的优点就是数据读取端实现成本较低(也不是完全没有),但对数据写入端的要求就高多了。不仅要写入多份,而且还得考虑数据写入后一致性的问题。这种方法要求两端都得改,貌似更麻烦了,你是不是觉得还不如第一种方案?实则不然,一般来说读取端会很多且很分散,改造的成本会非常高,频繁变动更是不太可能,所以有些工作不得不放置到比较集中的端上。
以上两种方案其实都是通过存储来换性能,主要差异点就在于由谁来做而已。前者是客户端来做,后者是服务端来做,各有优缺点。有没有可能对外提供纯粹的 Redis 协议,但可以解决数据热点的问题?鲁迅 …… David Wheeler 曾经说过,计算机科学的如何问题都可以通过增加一层来解决,热点的问题也不例外。我们可以在应用和 Redis 之间加一层中间层,这层中间层可以是真实的服务,比如数据统一访问层。也可以是特制的 Redis 客户端。
中间层可以对特定的 Key 加本地 Cache,就可以保证热点不会出现在 Redis 上。至于对哪些 Key 加本地 Cache,中间层可以实时去分析近期请求热点数据,自行决定。其实最简单的方式就是开个 LRU 或者 LFU 的 Cache。
另外,像第二种增加数据副本的方案,也完全可以由中间层去实现。当我们发现有数据热点时,让中间层主动将热点数据复制,拦截并改写所有对热点数据的请求,将其分散开来。当然,如果中间层更智能的化,这些完全都可以实现自动化,从热点的发现到解决,完全不需要人参与。
原文链接