亿级系统的Redis缓存如何设计???

Posted 微观技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了亿级系统的Redis缓存如何设计???相关的知识,希望对你有一定的参考价值。

知识分享,以技会友。大家好,我是Tom哥。阅读本文大约需要 15 分钟。

缓存设计可谓老生常谈了,早些时候都是采用memcache,现在大家更多倾向使用redis,除了知晓常用的数据存储类型,结合业务场景有针对性选择,好像其他也没有什么大的难点。

工程中引入Redis Client二方包,初始化一个Bean实例RedisTemplate ,一切搞定,so easy。

如果是几十、几百并发的业务场景,缓存设计可能并不需要考虑那么多,但如果是亿级的系统呢?



首先,先了解缓存知识图谱


早期的缓存用于加速CPU数据交换的RAM。随着互联网的快速发展,缓存的应用更加宽泛,用于数据高速交换的存储介质都称之为缓存。

使用缓存时,我们要关注哪些指标?缓存有哪些应用模式?以及缓存设计时有哪些Tip技巧?一图胜千言,如下:


七大经典问题缓存在使用过程不可避免会遇到一些问题,对于高频的问题我们大概归为了7类。具体内容下面我们一一道来1、缓存集中失效

当业务系统查询数据时,首先会查询缓存,如果缓存中数据不存在,然后查询DB再将数据预热到Cache中,并返回。缓存的性能比 DB 高 50~100 倍以上。

很多业务场景,如:秒杀商品、微博热搜排行、或者一些活动数据,都是通过跑任务方式,将DB数据批量、集中预热到缓存中,缓存数据有着近乎相同的过期时间

当过这批数据过期时,会一起过期,此时,对这批数据的所有请求,都会出现缓存失效,从而将压力转嫁到DB,DB的请求量激增,压力变大,响应开始变慢。

那么有没有解呢?

当然有了。

我们可以从缓存的过期时间入口,将原来的固定过期时间,调整为过期时间=基础时间+随机时间,让缓存慢慢过期,避免瞬间全部过期,对DB产生过大压力。

2、缓存穿透

不是所有的请求都能查到数据,不论是从缓存中还是DB中。

假如黑客攻击了一个论坛,用了一堆肉鸡访问一个不存的帖子id。按照常规思路,每次都会先查缓存,缓存中没有,接着又查DB,同样也没有,此时不会预热到Cache中,导致每次查询,都会cache miss

由于DB的吞吐性能较差,会严重影响系统的性能,甚至影响正常用户的访问。

解决方案:

  • 方案一:查存DB 时,如果数据不存在,预热一个特殊空值到缓存中。这样,后续查询都会命中缓存,但是要对特殊值,解析处理。
  • 方案二:构造一个BloomFilter过滤器,初始化全量数据,当接到请求时,在BloomFilter中判断这个key是否存在,如果不存在,直接返回即可,无需再查询缓存和DB
  • 3、缓存雪崩

    缓存雪崩是指部分缓存节点不可用,进而导致整个缓存体系甚至服务系统不可用的情况。

    分布式缓存设计一般选择一致性Hash,当有部分节点异常时,采用 rehash 策略,即把异常节点请求平均分散到其他缓存节点。但是,当较大的流量洪峰到来时,如果大流量 key 比较集中,正好在某 1~2 个缓存节点,很容易将这些缓存节点的内存、网卡过载,缓存节点异常 Crash,然后这些异常节点下线,这些大流量 key 请求又被 rehash 到其他缓存节点,进而导致其他缓存节点也被过载 Crash,缓存异常持续扩散,最终导致整个缓存体系异常,无法对外提供服务。

    解决方案:

  • 方案一:增加实时监控,及时预警。通过机器替换、各种故障自动转移策略,快速恢复缓存对外的服务能力
  • 方案二:缓存增加多个副本,当缓存异常时,再读取其他缓存副本。为了保证副本的可用性,尽量将多个缓存副本部署在不同机架上,降低风险。
  • 4、缓存热点

    对于突发事件,大量用户同时去访问热点信息,这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象,甚至 Crash,我们称之为缓存热点。



    这个在新浪微博经常遇到,某大V明星出轨、结婚、离婚,瞬间引发数百千万的吃瓜群众围观,访问同一个key,流量集中打在一个缓存节点机器,很容易打爆网卡、带宽、CPU的上限,最终导致缓存不可用。

    解决方案:

  • 首先能先找到这个热key来,比如通过Spark实时流分析,及时发现新的热点key。
  • 将集中化流量打散,避免一个缓存节点过载。由于只有一个key,我们可以在key的后面拼上有序编号,比如key#01key#02。。。key#10多个副本,这些加工后的key位于多个缓存节点上。
  • 每次请求时,客户端随机访问一个即可
  • 可以设计一个缓存服务治理管理后台,实时监控缓存的SLA,并打通分布式配置中心,对于一些hot key可以快速、动态扩容。

    5、缓存大Key

    当访问缓存时,如果key对应的value过大,读写、加载很容易超时,容易引发网络拥堵。另外缓存的字段较多时,每个字段的变更都会引发缓存数据的变更,频繁的读写,导致慢查询。如果大key过期被缓存淘汰失效,预热数据要花费较多的时间,也会导致慢查询。

    所以我们在设计缓存的时候,要注意缓存的粒度,既不能过大,如果过大很容易导致网络拥堵;也不能过小,如果太小,查询频率会很高,每次请求都要查询多次。

    解决方案:

  • 方案一:设置一个阈值,当value的长度超过阈值时,对内容启动压缩,降低kv的大小
  • 方案二:评估大key所占的比例,由于很多框架采用池化技术,如:Memcache,可以预先分配大对象空间。真正业务请求时,直接拿来即用。
  • 方案三:颗粒划分,将大key拆分为多个小key,独立维护,成本会降低不少
  • 方案四:大key要设置合理的过期时间,尽量不淘汰那些大key
  • 6、缓存数据一致性

    缓存是用来加速的,一般不会持久化储存。所以,一份数据通常会存在DB缓存中,由此会带来一个问题,如何保证这两者的数据一致性。另外,缓存热点问题会引入多个副本备份,也可能会发生不一致现象。


    解决方案:

  • 方案一:当缓存更新失败后,进行重试,如果重试失败,将失败的key写入MQ消息队列,通过异步任务补偿缓存,保证数据的一致性。
  • 方案二:设置一个较短的过期时间,通过自修复的方式,在缓存过期后,缓存重新加载最新的数据
  • 7、数据并发竞争预热

    互联网系统典型的特点就是流量大,一旦缓存中的数据过期、或因某些原因被删除等,导致缓存中的数据为空,大量的并发线程请求(查询同一个key)就会一起并发查询数据库,数据库的压力陡然增加。


    如果请求量非常大,全部压在数据库,可能把数据库压垮,进而导致整个系统的服务不可用。

    解决方案:

  • 方案一:引入一把全局锁,当缓存未命中时,先尝试获取全局锁,如果拿到锁,才有资格去查询DB,并将数据预热到缓存中。虽然,client端发起的请求非常多,但是由于拿不到锁,只能处于等待状态,当缓存中的数据预热成功后,再从缓存中获取
  • 为了便于理解,简单画了个流程图。这里面特别注意一个点,由于有一个并发时间差,所以会有一个二次check缓存是否有值的校验,防止缓存预热重复覆盖。

  • 方案二:缓存数据创建多个备份,当一个过期失效后,可以访问其他备份。
  • 写在最后

    缓存设计时,有很多技巧,优化手段也是千变万化,但是我们要抓住核心要素。那就是,让访问尽量命中缓存,同时保持数据的一致性。




    关于我:前阿里架构师,出过专利,竞赛拿过奖,CSDN博客专家,负责过电商交易、社区、营销、金融等业务,多年团队管理经验,爱思考,喜欢结交朋友

    「长按」↓↓↓ 二维码,拉你进群,一线大厂技术交流

    推荐阅读

    淘宝双11千亿交易额的系统架构演变

    淘宝订单自动确认收货的N种实现,秒杀面试官

    如何设计一个高性能的秒杀系统


    百亿级日访问量的应用如何做缓存架构设计?

    微博日活跃用户 1.6 亿+,每日访问量达百亿级,面对庞大用户群的海量访问,良好的架构且不断改进的缓存体系具有非常重要的支撑作用。

     

    技术分享图片

    本文由新浪微博技术专家陈波老师,分为如下四个部分跟大家详细讲解那些庞大的数据都是如何呈现的:

    • 微博在运行过程中的数据挑战

    • Feed 平台系统架构

    • Cache 架构及演进

    • 总结与展望

    微博在运行过程中的数据挑战

     

    技术分享图片

     

     

    Feed 平台系统架构

     

    技术分享图片

    Feed 平台系统架构总共分为五层:

    • 最上面是端层,比如 Web 端、客户端、大家用的 iOS 或安卓的一些客户端,还有一些开放平台、第三方接入的一些接口。

    • 下一层是平台接入层,不同的池子,主要是为了把好的资源集中调配给重要的核心接口,这样遇到突发流量的时候,就有更好的弹性来服务,提高服务稳定性。

    • 再下面是平台服务层,主要是 Feed 算法、关系等等。

    • 接下来是中间层,通过各种中间介质提供一些服务。

    • 最下面一层就是存储层。

     

     

    Feed Timeline

     

    技术分享图片

    大家日常刷微博的时候,比如在主站或客户端点一下刷新,最新获得了十到十五条微博,这是怎么构建出来的呢?

     

    刷新之后,首先会获得用户的关注关系。比如他有一千个关注,会把这一千个 ID 拿到,再根据这一千个 UID,拿到每个用户发表的一些微博。

     

    同时会获取这个用户的 Inbox,就是他收到的特殊的一些消息,比如分组的一些微博、群的微博、下面的关注关系、关注人的微博列表。

     

    拿到这一系列微博列表之后进行集合、排序,拿到所需要的那些 ID,再对这些 ID 去取每一条微博 ID 对应的微博内容。

     

    如果这些微博是转发过来的,它还有一个原微博,会进一步取原微博内容。通过原微博取用户信息,进一步根据用户的过滤词对这些微博进行过滤,过滤掉用户不想看到的微博。

     

    根据以上步骤留下的微博,会再进一步来看,用户对这些微博有没有收藏、点赞,做一些 Flag 设置,还会对这些微博各种计数,转发、评论、赞数进行组装,最后才把这十几条微博返回给用户的各种端。

     

    这样看来,用户一次请求得到的十几条记录,后端服务器大概要对几百甚至几千条数据进行实时组装,再返回给用户。

     

    整个过程对 Cache 体系强度依赖,所以 Cache 架构设计优劣会直接影响到微博体系表现的好坏。

     

     

    Feed Cache 架构

     

    技术分享图片

    接下来我们看一下 Cache 架构,它主要分为六层:

    • 第一层是 Inbox,主要是分组的一些微博,然后直接对群主的一些微博。Inbox 比较少,主要是推的方式。

    • 第二层是 Outbox,每个用户都会发常规的微博,都会到它的 Outbox 里面去。根据存的 ID 数量,实际上分成多个 Cache,普通的大概是 200 多条,如果长的大概是 2000 条。

    • 第三层是一些关系,它的关注、粉丝、用户。

    • 第四层是内容,每一条微博一些内容存在这里。

    • 第五层就是一些存在性判断,比如某条微博我有没有赞过。之前有一些明星就说我没有点赞这条微博怎么显示我点赞了,引发了一些新闻。而这种就是记录,实际上她有在某个时候点赞过但可能忘记了。

    • 最下面还有比较大的一层——计数,每条微博的评论、转发等计数,还有用户的关注数、粉丝数这些数据。

     

     

    Cache 架构及演进

     

     

    简单 KV 数据类型

     

    技术分享图片

    接下来我们着重讲一下微博的 Cache 架构演进过程。最开始微博上线时,我们是把它作为一个简单的 KV 数据类型来存储。

     

    我们主要采取哈希分片存储在 MC 池子里,上线几个月之后发现一些问题:有一些节点机器宕机或是其他原因,大量的请求会穿透 Cache 层达到 DB 上去,导致整个请求变慢,甚至 DB 僵死。

     

    于是我们很快进行了改造,增加了一个 HA 层,这样即便 Main 层出现某些节点宕机情况或者挂掉之后,这些请求会进一步穿透到 HA 层,不会穿透到 DB 层。

     

    这样可以保证在任何情况下,整个系统命中率不会降低,系统服务稳定性有了比较大的提升。

     

    对于这种做法,现在业界用得比较多,然后很多人说我直接用哈希,但这里面也有一些坑。

     

    比如我有一个节点,节点 3 宕机了,Main 把它给摘掉,节点 3 的一些 QA 分给其他几个节点,这个业务量还不是很大,穿透 DB,DB 还可以抗住。

     

    但如果这个节点 3 恢复了,它又加进来之后,节点 3 的访问就会回来,稍后节点 3 因为网络原因或者机器本身的原因,它又宕机了,一些节点 3 的请求又会分给其他节点。

     

    这个时候就会出现问题,之前分散给其他节点写回来的数据已经没有人更新了,如果它没有被剔除掉就会出现混插数据。

    技术分享图片

    实际上微博是一个广场型的业务,比如突发事件,某明星找个女朋友,瞬间流量就 30% 了。

     

    突发事件后,大量的请求会出现在某一些节点,会导致这些节点非常热,即便是 MC 也没办法满足这么大的请求量。这时 MC 就会变成瓶颈,导致整个系统变慢。

     

    基于这个原因,我们引入了 L1 层,还是一个 Main 关系池,每一个 L1 大概是 Main 层的 N 分之一,六分之一、八分之一、十分之一这样一个内存量,根据请求量我会增加 4 到 8 个 L1,这样所有请求来了之后首先会访问 L1。

     

    L1 命中的话就会直接访问,如果没有命中再来访问 Main-HA 层,这样在一些突发流量的时候,可以由 L1 来抗住大部分热的请求。

     

    对微博本身来说,新的数据就会越热,只要增加很少一部分内存就会抗住更大的量。

    技术分享图片

    简单总结一下:通过简单 KV 数据类型的存储,我们实际上是以 MC 为主的,层内 Hash 节点不漂移,Miss 穿透到下一层去读取。

     

    通过多组 L1 读取性能提升,能够抗住峰值、突发流量,而且成本会大大降低。

     

    对读写策略,采取多写,读的话采用逐层穿透,如果 Miss 的话就进行回写。对存在里面的数据,我们最初采用 Json/xml,2012 年之后就直接采用 Protocol Buffer 格式,对一些比较大的用 QuickL 进行压缩。

     

     

    集合类数据

     

    技术分享图片

    刚才讲到简单的 QA 数据,那对于复杂的集合类数据怎么来处理?

     

    比如我关注了 2000 人,新增 1 个人,就涉及到部分修改。有一种方式是把 2000 个 ID 全部拿下来进行修改,但这种对带宽、机器压力会很大。

     

    还有一些分页获取,我存了 2000 个,只需要取其中的第几页,比如第二页,也就是第十到第二十个,能不能不要全量把所有数据取回去。

     

    还有一些资源的联动计算,会计算到我关注的某些人里面 ABC 也关注了用户 D。这种涉及到部分数据的修改、获取,包括计算,对 MC 来说实际上是不太擅长的。

     

    各种关注关系都存在 Redis 里面取,通过 Hash 分布、储存,一组多存的方式来进行读写分离。现在 Redis 的内存大概有 30 个 T,每天都有 2-3 万亿的请求。

    技术分享图片

    在使用 Redis 的过程中,实际上还是遇到其他一些问题。比如从关注关系,我关注了 2000 个 UID,有一种方式是全量存储。

     

    但微博有大量的用户,有些用户登录得比较少,有些用户特别活跃,这样全部放在内存里成本开销是比较大的。

     

    所以我们就把 Redis 使用改成 Cache,比如只存活跃的用户,如果你最近一段时间没有活跃,会把你从 Redis 里踢掉,再次有访问的时候再把你加进来。

     

    这时存在一个问题,因为 Redis 工作机制是单线程模式,如果它加某一个 UV,关注 2000 个用户,可能扩展到两万个 UID,两万个 UID 塞回去基本上 Redis 就卡住了,没办法提供其他服务。

     

    所以我们扩展一种新的数据结构,两万个 UID 直接开了端,写的时候直接依次把它写到 Redis 里面去,读写的整个效率就会非常高。

     

    它的实现是一个 long 型的开放数组,通过 Double Hash 进行寻址。

    技术分享图片

    我们对 Redis 进行了一些其他的扩展,大家可能也在网上看到过我们之前的一些分享,把数据放到公共变量里面。

     

    整个升级过程,我们测试 1G 的话加载要 10 分钟,10G 大概要 10 分钟以上,现在是毫秒级升级。

     

    对于 AOF,我们采用滚动的 AOF,每个 AOF 是带一个 ID 的,达到一定的量再滚动到下一个 AOF 里去。

     

    对 RDB 落地的时候,我们会记录构建这个 RDB 时,AOF 文件以及它所在的位置,通过新的 RDB、AOF 扩展模式,实现全增量复制。

     

     

    其他数据类型:计数

     

    技术分享图片

    接下来还有一些其他的数据类型,比如一个计数,实际上计数在每个互联网公司都可能会遇到,对一些中小型的业务来说,实际上 MC 和 Redis 足够用的。

     

    但在微博里计数出现了一些特点:单条 Key 有多条计数,比如一条微博,有转发数、评论数,还有点赞;一个用户有粉丝数、关注数等各种各样的数字。

     

    因为是计数,它的 Value size 是比较小的,根据它的各种业务场景,大概就是 2-8 个字节,一般 4 个字节为多。

     

    然后每日新增的微博大概十亿条记录,总记录就更可观了,然后一次请求,可能几百条计数要返回去。

    技术分享图片

     

    计数器 Counter Service

     

    最初是可以采取 Memcached,但它有个问题,如果计数超过它内容容量时,会导致一些计数的剔除,宕机或重启后计数就没有了。

     

    另外可能有很多计数它为零,那这个时候怎么存,要不要存,存的话就占很多内存。

     

    微博每天上十亿的计数,光存 0 都要占大量的内存,如果不存又会导致穿透到 DB 里去,对服务的可溶性会存在影响。

     

    2010 年之后我们又采用 Redis 访问,随着数据量越来越大之后,发现 Redis 内存有效负荷还是比较低的,它一条 KV 大概需要至少 65 个字节。

     

    但实际上我们一个计数需要 8 个字节,然后 Value 大概 4 个字节,所以有效只有 12 个字节,还有四十多个字节都是被浪费掉的。

     

    这还只是单个 KV,如果在一条 Key 有多个计数的情况下,它就浪费得更多了。

     

    比如说四个计数,一个 Key 8 个字节,四个计数每个计数是 4 个字节,16 个字节大概需要 26 个字节就行了,但是用 Redis 存大概需要 200 多个字节。

     

    后来我们通过自己研发的 Counter Service,内存降至 Redis 的五分之一到十五分之一以下,而且进行冷热分离,热数据存在内存里,冷数据如果重新变热,就把它放到 LRU 里去。

     

    落地 RDB、AOF,实现全增量复制,通过这种方式,热数据单机可以存百亿级,冷数据可以存千亿级。

    技术分享图片

    整个存储架构大概是上图这样,上面是内存,下面是 SSD,在内存里是预先把它分成 N 个 Table,每个 Table 根据 ID 的指针序列,划出一定范围。

     

    任何一个 ID 过来先找到它所在的 Table,如果有直接对它增增减减,有新的计数过来,发现内存不够的时候,就会把一个小的 Table Dump 到 SSD 里去,留着新的位置放在最上面供新的 ID 来使用。

     

    有些人疑问说,如果在某个范围内,我的 ID 本来设的计数是 4 个字节,但是微博特别热,超过了 4 个字节,变成很大的一个计数怎么处理?

     

    对于超过限制的,我们把它放在 Aux dict 进行存放,对于落在 SSD 里的 Table,我们有专门的 IndAux 进行访问,通过 RDB 方式进行复制。

     

     

    其他数据类型:存在性判断

     

    技术分享图片

    除了计数,微博还有一些业务,一些存在性判断。比如一条微博展现的,有没有点赞、阅读、推荐,如果这个用户已经读过这个微博了,就不要再显示给他。

     

    这种有一个很大的特点,它检查是否存在,每条记录非常小,比如 Value 1 个 bit 就可以了,但总数据量巨大。

     

    比如微博每天新发表微博 1 亿左右,读的可能有上百亿、上千亿这种总的数据需要判断。

     

    怎么来存储是个很大的问题,而且这里面很多存在性就是 0。还是前面说的,0 要不要存?

     

    如果存了,每天就存上千亿的记录;如果不存,那大量的请求最终会穿透 Cache 层到 DB 层,任何 DB 都没办法抗住那么大的流量。

    技术分享图片

    我们也进行了一些选型:首先直接考虑能不能用 Redis。单条 KV 65 个字节,一个 KV 可以 8 个字节的话,Value 只有 1 个 bit,这样算下来每日新增内存有效率是非常低的。

     

    第二种我们新开发的 Counter Service,单条 KV Value 1 个 bit,我就存 1 个 byt,总共 9 个 byt 就可以了。

     

    这样每日新增内存 900G,存的话可能就只能存最新若干天的,存个三天差不多快 3 个 T 了,压力也挺大,但比 Redis 已经好很多。

    技术分享图片

    我们最终方案是自己开发 Phantom,先采用把共享内存分段分配,最终使用的内存只用 120G 就可以。

     

    算法很简单,对每个 Key 可以进行 N 次哈希,如果哈希的某一个位它是 1,那么进行 3 次哈希,三个数字把它设为 1。

     

    把 X2 也进行三次哈希,后面来判断 X1 是否存在的时候,从进行三次哈希来看,如果都为 1 就认为它是存在的;如果某一个哈希 X3,它的位算出来是 0,那就百分百肯定是不存在的。

    技术分享图片

    它的实现架构比较简单,把共享内存预先拆分到不同 Table 里,在里面进行开方式计算,然后读写,落地的话采用 AOF+RDB 的方式进行处理。

     

    整个过程因为放在共享内存里面,进程要升级重启数据也不会丢失。对外访问的时候,建 Redis 协议,它直接扩展新的协议就可以访问我们这个服务了。

    技术分享图片

    小结一下:到目前为止,我们关注了 Cache 集群内的高可用、扩展性、组件高性能,还有一个特别重要就是存储成本,还有一些我们没有关注到的,比如运维性如何,微博现在已经有几千差不多上万台服务器等。

     

     

    进一步优化

     

    技术分享图片

     

    服务化

     

    技术分享图片

    采取的方案首先就是对整个 Cache 进行服务化管理,对配置进行服务化管理,避免频繁重启,另外如果配置发生变更,直接用一个脚本修改一下。

    技术分享图片

    技术分享图片

    服务化还引入 Cluster Manager,实现对外部的管理,通过一个界面来进行管理,可以进行服务校验。

     

    服务治理方面,可以做到扩容、缩容,SLA 也可以得到很好的保障。另外,对于开发来说,现在就可以屏蔽 Cache 资源。

     

     

    总结与展望

     

    技术分享图片

    最后简单总结一下,对于微博 Cache 架构来说,我们从它的数据架构、性能、储存成本、服务化等不同方面进行了优化增强。欢迎对此有研究或有疑问的同行们留言,跟我们一起探讨。

     

    作者:陈波

    简介:新浪微博技术专家,《深入分布式缓存》作者。

    编辑:陶家龙、孙淑娟

    出处:本文转自中生代技术订阅号(ID:freshmanTechnology)。

     

    以上是关于亿级系统的Redis缓存如何设计???的主要内容,如果未能解决你的问题,请参考以下文章

    百亿级日访问量的应用如何做缓存架构设计?

    日访问量百亿级的应用如何做缓存架构设计

    百亿级日访问量的应用如何做缓存架构设计?

    Redis百亿级Key存储设计方案

    如何基于微服务设计亿级用户的秒杀系统?

    如何设计一个亿级API网关?