缓存设计中七大经典问题总结
形而上 Lv4

在缓存系统的设计架构中,还有很多坑,很多的明枪暗箭,如果设计不当会导致很多严重的后果。设计不当,轻则请求变慢、性能降低,重则会数据不一致、系统可用性降低,甚至会导致缓存雪崩,整个系统无法对外提供服务

接下来将对缓存设计中的7大经典问题,如下图,进行问题描述、原因分析,并给出日常研发中,可能会出现该问题的业务场景,最后给出这些经典问题的解决方案。本文先学习缓存失效、缓存穿透与缓存雪崩。

缓存失效

问题描述

缓存第一个经典问题是缓存失效。上一课时讲到,服务系统查数据,首先会查缓存,如果缓存数据不存在,就进一步查DB,最后查到数据后回种到缓存并返回。缓存的性能比DB高50~100倍以上,所以我们希望数据查询尽可能命中缓存,这样系统负荷最小,性能最佳。缓存里的数据存储基本上都是以key为索引进行存储和获取的。业务访问时,如果大量的key同时过期,很多缓存数据访问都会miss,进而穿透到DB,DB的压力就会明显上升,由于DB的性能较差,只在缓存的1%~2%以下,这样请求的慢查率会明显上升。这就是缓存失效的问题。

原因分析

导致缓存失效,特别是很多key一起失效的原因,跟我们日常写缓存的过期时间息息相关。

在写缓存时,我们一般会根据业务的访问特点,给每种业务数据预置一个过期时间,在写缓存时把这个过期时间带上,让缓存数据在这个固定的过期时间后被淘汰。一般情况下,因为缓存数据是逐步写入的,所以也是逐步过期被淘汰的。但在某些场景,一大批数据会被系统主动或被动从DB批量加载,然后写入缓存。这些数据写入缓存时,由于使用相同的过期时间,在经历这个过期时间之后,这批数据就会一起到期,从而被缓存淘汰。此时,对这批数据的所有请求,都会出现缓存失效,从而都穿透到DB,DB由于查询量太大,就很容易压力大增,请求变慢

业务场景

很多业务场景,稍不注意,就出现大量的缓存失效,进而导致系统DB压力大、请求变慢的情况。比如同一批火车票、飞机票,当可以售卖时,系统会一次性加载到缓存,如果缓存写入时,过期时间按照预先设置的过期值,那过期时间到期后,系统就会因缓存失效出现变慢的问题。类似的业务场景还有很多,比如微博业务,会有后台离线系统,持续计算热门微博,每当计算结束,会将这批热门微博批量写入对应的缓存。还比如,很多业务,在部署新IDC或新业务上线时,会进行缓存预热,也会一次性加载大批热数据。

解决方案

对于批量key缓存失效的问题,原因既然是预置的固定过期时间,那解决方案也从这里入手。设计缓存的过期时间时,使用公式:过期时间=baes时间+随机时间。即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对DB造成过大压力,如下图所示。

缓存穿透

问题描述

第二个经典问题是缓存穿透。缓存穿透是一个很有意思的问题。因为缓存穿透发生的概率很低,所以一般很难被发现。但是,一旦你发现了,而且量还不小,你可能立即就会经历一个忙碌的夜晚。因为对于正常访问,访问的数据即便不在缓存,也可以通过DB加载回种到缓存。而缓存穿透,则意味着有特殊访客在查询一个不存在的key,导致每次查询都会穿透到DB,如果这个特殊访客再控制一批肉鸡机器,持续访问你系统里不存在的key,就会对DB产生很大的压力,从而影响正常服务。

原因分析

缓存穿透存在的原因,就是因为我们在系统设计时,更多考虑的是正常访问路径,对特殊访问路径、异常访问路径考虑相对欠缺

缓存访问设计的正常路径,是先访问cache,cache miss后查DB,DB查询到结果后,回种缓存返回。这对于正常的key访问是没有问题的,但是如果用户访问的是一个不存在的key,查DB返回空(即一个NULL),那就不会把这个空写回cache。那以后不管查询多少次这个不存在的key,都会cache miss,都会查询DB。整个系统就会退化成一个“前端+DB“的系统,由于DB的吞吐只在cache的1%~2%以下,如果有特殊访客,大量访问这些不存在的key,就会导致系统的性能严重退化,影响正常用户的访问。

业务场景

缓存穿透的业务场景很多,比如通过不存在的UID访问用户,通过不存在的车次ID查看购票信息。用户输入错误,偶尔几个这种请求问题不大,但如果是大量这种请求,就会对系统影响非常大。

解决方案

那么如何解决这种问题呢?如下图所示。

  • 第一种方案就是,查询这些不存在的数据时,第一次查DB,虽然没查到结果返回NULL,仍然记录这个key到缓存,只是这个key对应的value是一个特殊设置的值。
  • 第二种方案是,构建一个BloomFilter缓存过滤器,记录全量数据,这样访问数据时,可以直接通过BloomFilter判断这个key是否存在,如果不存在直接返回即可,根本无需查缓存和DB。

不过这两种方案在设计时仍然有一些要注意的坑。

  • 对于方案一,如果特殊访客持续访问大量的不存在的key,这些key即便只存一个简单的默认值,也会占用大量的缓存空间,导致正常key的命中率下降。所以进一步的改进措施是,对这些不存在的key只存较短的时间,让它们尽快过期;或者将这些不存在的key存在一个独立的公共缓存,从缓存查找时,先查正常的缓存组件,如果miss,则查一下公共的非法key的缓存,如果后者命中,直接返回,否则穿透DB,如果查出来是空,则回种到非法key缓存,否则回种到正常缓存。
  • 对于方案二,BloomFilter要缓存全量的key,这就要求全量的key数量不大,10亿条数据以内最佳,因为10亿条数据大概要占用1.2GB的内存。也可以用BloomFilter缓存非法key,每次发现一个key是不存在的非法key,就记录到BloomFilter中,这种记录方案,会导致BloomFilter存储的key持续高速增长,为了避免记录key太多而导致误判率增大,需要定期清零处理。

BloomFilter

BloomFilter是一个非常有意思的数据结构,不仅仅可以挡住非法key攻击,还可以低成本、高性能地对海量数据进行判断,比如一个系统有数亿用户和百亿级新闻feed,就可以用BloomFilter来判断某个用户是否阅读某条新闻feed。下面来对BloomFilter数据结构做一个分析,如下图所示。

BloomFilter的目的是检测一个元素是否存在于一个集合内。它的原理,是用bit数据组来表示一个集合,对一个key进行多次不同的Hash检测,如果所有Hash对应的bit位都是1,则表明key非常大概率存在,平均单记录占用1.2字节即可达到99%,只要有一次Hash对应的bit位是0,就说明这个key肯定不存在于这个集合内。

BloomFilter的算法是,首先分配一块内存空间做bit数组,数组的bit位初始值全部设为0,加入元素时,采用k个相互独立的Hash函数计算,然后将元素Hash映射的K个位置全部设置为1。检测key时,仍然用这k个Hash函数计算出k个位置,如果位置全部为1,则表明key存在,否则不存在。

BloomFilter的优势是,全内存操作,性能很高。另外空间效率非常高,要达到1%的误判率,平均单条记录占用1.2字节即可。而且,平均单条记录每增加0.6字节,还可让误判率继续变为之前的1/10,即平均单条记录占用1.8字节,误判率可以达到1/1000;平均单条记录占用2.4字节,误判率可以到1/10000,以此类推。这里的误判率是指,BloomFilter判断某个key存在,但它实际不存在的概率,因为它存的是key的Hash值,而非key的值,所以有概率存在这样的key,它们内容不同,但多次Hash后的Hash值都相同。对于BloomFilter判断不存在的key ,则是100%不存在的,反证法,如果这个key存在,那它每次Hash后对应的Hash值位置肯定是1,而不会是0。

缓存雪崩

问题描述

第三个经典问题是缓存雪崩。系统运行过程中,缓存雪崩是一个非常严重的问题。缓存雪崩是指部分缓存节点不可用,导致整个缓存体系甚至甚至服务系统不可用的情况。缓存雪崩按照缓存是否rehash(即是否漂移)分两种情况:

  • 缓存不支持rehash导致的系统雪崩不可
  • 缓存支持rehash导致的缓存雪崩不可用

原因分析

在上述两种情况中,缓存不进行rehash时产生的雪崩,一般是由于较多缓存节点不可用,请求穿透导致DB也过载不可用,最终整个系统雪崩不可用的。而缓存支持rehash时产生的雪崩,则大多跟流量洪峰有关,流量洪峰到达,引发部分缓存节点过载Crash,然后因rehash扩散到其他缓存节点,最终整个缓存体系异常。

第一种情况比较容易理解,缓存节点不支持rehash,较多缓存节点不可用时,大量Cache访问会失败,根据缓存读写模型,这些请求会进一步访问DB,而且DB可承载的访问量要远比缓存小的多,请求量过大,就很容易造成DB过载,大量慢查询,最终阻塞甚至Crash,从而导致服务异常。

第二种情况是怎么回事呢?这是因为缓存分布设计时,很多同学会选择一致性Hash分布方式,同时在部分节点异常时,采用rehash策略,即把异常节点请求平均分散到其他缓存节点。在一般情况下,一致性Hash分布+rehash策略可以很好得运行,但在较大的流量洪峰到临之时,如果大流量key比较集中,正好在某1~2个缓存节点,很容易将这些缓存节点的内存、网卡过载,缓存节点异常Crash,然后这些异常节点下线,这些大流量key请求又被rehash到其他缓存节点,进而导致其他缓存节点也被过载Crash,缓存异常持续扩散,最终导致整个缓存体系异常,无法对外提供服务。

业务场景

缓存雪崩的业务场景并不少见,微博、Twitter等系统在运行的最初若干年都遇到过很多次。比如,微博最初很多业务缓存采用一致性Hash+rehash策略,在突发洪水流量来临时,部分缓存节点过载Crash甚至宕机,然后这些异常节点的请求转到其他缓存节点,又导致其他缓存节点过载异常,最终整个缓存池过载。另外,机架断电,导致业务缓存多个节点宕机,大量请求直接打到DB,也导致DB过载而阻塞,整个系统异常。最后缓存机器复电后,DB重启,数据逐步加热后,系统才逐步恢复正常。

解决方案

预防缓存雪崩,这里给出3个解决方案。

  • 方案一,对业务DB的访问增加读写开关,当发现DB请求变慢、阻塞,慢请求超过阀值时,就会关闭读开关,部分或所有读DB的请求进行failfast立即返回,待DB恢复后再打开读开关,如下图。

  • 方案二,对缓存增加多个副本,缓存异常或请求miss后,再读取其他缓存副本,而且多个缓存副本尽量部署在不同机架,从而确保在任何情况下,缓存系统都会正常对外提供服务。

  • 方案三,对缓存体系进行实时监控,当请求访问的慢速比超过阀值时,及时报警,通过机器替换、服务替换进行及时恢复;也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。

实际上,微博平台系统,这三种方案都采用了,通过三管齐下,规避缓存雪崩的发生。

数据不一致

问题描述

七大缓存经典问题的第四个问题是数据不一致。同一份数据,可能会同时存在DB和缓存之中。那就有可能发生,DB和缓存的数据不一致。如果缓存有多个副本,多个缓存副本里的数据也可能会发生不一致现象

原因分析

不一致的问题大多跟缓存更新异常有关。比如更新DB后,写缓存失败,从而导致缓存中存的是老数据。另外,如果系统采用一致性Hash分布,同时采用rehash自动漂移策略,在节点多次上下线之后,也会产生脏数据。缓存有多个副本时,更新某个副本失败,也会导致这个副本的数据是老数据。

业务场景

导致数据不一致的场景也不少。如下图所示,在缓存机器的带宽被打满,或者机房网络出现波动时,缓存更新失败,新数据没有写入缓存,就会导致缓存和DB的数据不一致。缓存rehash时,某个缓存机器反复异常,多次上下线,更新请求多次rehash。这样,一份数据存在多个节点,且每次rehash只更新某个节点,导致一些缓存节点产生脏数据。

解决方案

要尽量保证数据的一致性。这里也给出了3个方案,可以根据实际情况进行选择。

  • 第一个方案,cache更新失败后,可以进行重试,如果重试失败,则将失败的key写入队列机服务,待缓存访问恢复后,将这些key从缓存删除。这些key在再次被查询时,重新从DB加载,从而保证数据的一致性。
  • 第二个方案,缓存时间适当调短,让缓存数据及早过期后,然后从DB重新加载,确保数据的最终一致性。
  • 第三个方案,不采用rehash漂移策略,而采用缓存分层策略,尽量避免脏数据产生。

数据并发竞争

问题描述

第五个经典问题是数据并发竞争。互联网系统,线上流量较大,缓存访问中很容易出现数据并发竞争的现象。数据并发竞争,是指在高并发访问场景,一旦缓存访问没有找到数据,大量请求就会并发查询DB,导致DB压力大增的现象。

数据并发竞争,主要是由于多个进程/线程中,有大量并发请求获取相同的数据,而这个数据key因为正好过期、被剔除等各种原因在缓存中不存在,这些进程/线程之间没有任何协调,然后一起并发查询DB,请求那个相同的key,最终导致DB压力大增,如下图。

业务场景

数据并发竞争在大流量系统也比较常见,比如车票系统,如果某个火车车次缓存信息过期,但仍然有大量用户在查询该车次信息。又比如微博系统中,如果某条微博正好被缓存淘汰,但这条微博仍然有大量的转发、评论、赞。上述情况都会造成该车次信息、该条微博存在并发竞争读取的问题

解决方案

要解决并发竞争,有2种方案。

  • 方案一是使用全局锁。如下图所示,即当缓存请求miss后,先尝试加全局锁,只有加全局锁成功的线程,才可以到DB去加载数据。其他进程/线程在读取缓存数据miss时,如果发现这个key有全局锁,就进行等待,待之前的线程将数据从DB回种到缓存后,再从缓存获取。

  • 方案二是,对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况,如下图。

Hot key

问题描述

第六个经典问题是Hot key。对于大多数互联网系统,数据是分冷热的。比如最近的新闻、新发表的微博被访问的频率最高,而比较久远的之前的新闻、微博被访问的频率就会小很多。而在突发事件发生时,大量用户同时去访问这个突发热点信息,访问这个Hot key,这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象,甚至会被Crash。

原因分析

Hot key引发缓存系统异常,主要是因为突发热门事件发生时,超大量的请求访问热点事件对应的key,比如微博中数十万、数百万的用户同时去吃一个新瓜。数十万的访问请求同一个key,流量集中打在一个缓存节点机器,这个缓存机器很容易被打到物理网卡、带宽、CPU的极限,从而导致缓存访问变慢、卡顿。

业务场景

引发Hot key的业务场景很多,比如明星结婚、离婚、出轨这种特殊突发事件,比如奥运、春节这些重大活动或节日,还比如秒杀、双12、618等线上促销活动,都很容易出现Hot key的情况。

解决方案

要解决这种极热key的问题,首先要找出这些Hot key来。对于重要节假日、线上促销活动、集中推送这些提前已知的事情,可以提前评估出可能的热key来。而对于突发事件,无法提前评估,可以通过Spark,对应流任务进行实时分析,及时发现新发布的热点key。而对于之前已发出的事情,逐步发酵成为热key的,则可以通过Hadoop对批处理任务离线计算,找出最近历史数据中的高频热key。

找到热key后,就有很多解决办法了。首先可以将这些热key进行分散处理,比如一个热key名字叫hotkey,可以被分散为hotkey#1、hotkey#2、hotkey#3,……hotkey#n,这n个key分散存在多个缓存节点,然后client端请求时,随机访问其中某个后缀的hotkey,这样就可以把热key的请求打散,避免一个缓存节点过载,如下图所示。

其次,也可以key的名字不变,对缓存提前进行多副本+多级结合的缓存架构设计。

再次,如果热key较多,还可以通过监控体系对缓存的SLA实时监控,通过快速扩容来减少热key的冲击。

最后,业务端还可以使用本地缓存,将这些热key记录在本地缓存,来减少对远程缓存的冲击。

Big key

问题描述

最后一个经典问题是Big key,也就是大Key的问题。大key,是指在缓存访问时,部分Key的Value过大,读写、加载易超时的现象。

原因分析

造成这些大key慢查询的原因很多。如果这些大key占总体数据的比例很小,存Mc,对应的slab较少,导致很容易被频繁剔除,DB反复加载,从而导致查询较慢。如果业务中这种大key很多,而这种key被大量访问,缓存组件的网卡、带宽很容易被打满,也会导致较多的大key慢查询。另外,如果大key缓存的字段较多,每个字段的变更都会引发对这个缓存数据的变更,同时这些key也会被频繁地读取,读写相互影响,也会导致慢查现象。最后,大key一旦被缓存淘汰,DB加载可能需要花费很多时间,这也会导致大key查询慢的问题。

业务场景

大key的业务场景也比较常见。比如互联网系统中需要保存用户最新1万个粉丝的业务,比如一个用户个人信息缓存,包括基本资料、关系图谱计数、发feed统计等。微博的feed内容缓存也很容易出现,一般用户微博在140字以内,但很多用户也会发表1千字甚至更长的微博内容,这些长微博也就成了大key,如下图。

解决方案

对于大key,给出3种解决方案。

  • 第一种方案,如果数据存在Mc中,可以设计一个缓存阀值,当value的长度超过阀值,则对内容启用压缩,让KV尽量保持小的size,其次评估大key所占的比例,在Mc启动之初,就立即预写足够数据的大key,让Mc预先分配足够多的trunk size较大的slab。确保后面系统运行时,大key有足够的空间来进行缓存。

  • 第二种方案,如果数据存在Redis中,比如业务数据存set格式,大key对应的set结构有几千几万个元素,这种写入Redis时会消耗很长的时间,导致Redis卡顿。此时,可以扩展新的数据结构,同时让client在这些大key写缓存之前,进行序列化构建,然后通过restore一次性写入,如下图所示。

  • 第三种方案时,如下图所示,将大key分拆为多个key,尽量减少大key的存在。同时由于大key一旦穿透到DB,加载耗时很大,所以可以对这些大key进行特殊照顾,比如设置较长的过期时间,比如缓存内部在淘汰key时,同等条件下,尽量不淘汰这些大key。

至此,本文关于缓存的7大经典问题全部介绍完了。

我们要认识到,对于互联网系统,由于实际业务场景复杂,数据量、访问量巨大,需要提前规避缓存使用中的各种坑。你可以通过提前熟悉Cache的经典问题,提前构建防御措施, 避免大量key同时失效,避免不存在key访问的穿透,减少大key、热key的缓存失效,对热key进行分流。你可以采取一系列措施,让访问尽量命中缓存,同时保持数据的一致性。另外,你还可以结合业务模型,提前规划cache系统的SLA,如QPS、响应分布、平均耗时等,实施监控,以方便运维及时应对。在遇到部分节点异常,或者遇到突发流量、极端事件时,也能通过分池分层策略、key分拆等策略,避免故障发生。

最终,你能在各种复杂场景下,面对高并发、海量访问,面对突发事件和洪峰流量,面对各种网络或机器硬件故障,都能保持服务的高性能和高可用。

  • 本文标题:缓存设计中七大经典问题总结
  • 本文作者:形而上
  • 创建时间:2021-11-06 14:30:45
  • 本文链接:https://deepter.gitee.io/2021_11_06_cache_7_problems/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!