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

640?wx_fmt=gif

相关阅读:

为什么分布式一定要有redis?

中国IT工作者35岁后的发展出路调查报告

互联网技术(java框架、分布式、集群)干货视频大全,不看后悔!(免费下载)

作者:陈波

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

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

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


640?wx_fmt=png

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

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

  • Feed 平台系统架构

  • Cache 架构及演进

  • 总结与展望


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


640?wx_fmt=png


Feed 平台系统架构


640?wx_fmt=png

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

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

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

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

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

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


Feed Timeline


640?wx_fmt=png

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

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


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


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


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


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


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


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


Feed Cache 架构


640?wx_fmt=png

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

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

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

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

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

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

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


Cache 架构及演进


简单 KV 数据类型


640?wx_fmt=png

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


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


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


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


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


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


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


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

640?wx_fmt=png

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


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


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


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


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

640?wx_fmt=png

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


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


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


集合类数据


640?wx_fmt=png

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


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


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


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


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

640?wx_fmt=png

 妈妈常教导我,让我养成良好习惯。这样长大才能成为一个有用的人。良好的习惯是尊敬师长这样长大才能成为一个有用的人。良好的习惯是尊敬师长,爱护同学,对人有礼貌;是不粗心,做事情不拖拉;还是爱护公物,不浪费粮食。为什么呢?因为拥有良好习惯,做一个品德高尚的人,懂得尊重别人,才会得到别人的尊重。我要努力地做到这些。我有一些坏习惯,有时候学习很粗心,把一些会做的题做错。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃饭很慢,有的时候还剩饭。我还起床磨蹭,本来应该迅速地穿好衣服,但是,我总是磨磨蹭蹭地,速度很慢。我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!  在上幼儿园以前,我什么也不会干,就连穿衣服也是妈妈给我穿好,就要上幼儿园了,这样可不行,妈妈锻炼我要学会自己穿衣服。   有一天,妈妈把衣服摆在我面前,开始让我自己穿。一开始。我又哭又叫就是不穿,还把衣服扔的满地都是,然后坐在地上开始大哭,等了好长时间,妈妈还是不理我,我只好自己乖乖的把衣服穿好, 一出了房间门,妈妈就笑了起来,再看看我的衣服,毛衣和裤子都穿反了,我赶紧回房间又重新穿了一遍,这次穿好了,拿起外套,可是外套的扣子又扣不上了,扣子可调皮了,好像故意和我作对,我把扣子往扣眼——人类邪恶的根源;爱情——幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话:幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话亲爱的!擦干你的眼泪,至高无上的爱情已经打开了我们的眼界,使我们成了它的崇拜者。是它,





 妈妈常教导我,让我养成良好习惯。这样长大才能成为一个有用的人。良好的习惯是尊敬师长这样长大才能成为一个有用的人。良好的习惯是尊敬师长,爱护同学,对人有礼貌;是不粗心,做事情不拖拉;还是爱护公物,不浪费粮食。为什么呢?因为拥有良好习惯,做一个品德高尚的人,懂得尊重别人,才会得到别人的尊重。我要努力地做到这些。我有一些坏习惯,有时候学习很粗心,把一些会做的题做错。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃饭很慢,有的时候还剩饭。我还起床磨蹭,本来应该迅速地穿好衣服,但是,我总是磨磨蹭蹭地,速度很慢。我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!  在上幼儿园以前,我什么也不会干,就连穿衣服也是妈妈给我穿好,就要上幼儿园了,这样可不行,妈妈锻炼我要学会自己穿衣服。   有一天,妈妈把衣服摆在我面前,开始让我自己穿。一开始。我又哭又叫就是不穿,还把衣服扔的满地都是,然后坐在地上开始大哭,等了好长时间,妈妈还是不理我,我只好自己乖乖的把衣服穿好, 一出了房间门,妈妈就笑了起来,再看看我的衣服,毛衣和裤子都穿反了,我赶紧回房间又重新穿了一遍,这次穿好了,拿起外套,可是外套的扣子又扣不上了,扣子可调皮了,好像故意和我作对,我把扣子往扣眼——人类邪恶的根源;爱情——幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话:亲爱的!擦干你的眼泪,至高无上的爱情已经打开了我们的眼界,使我们成了它的崇拜者。是它,


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


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


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


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


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


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

640?wx_fmt=png

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


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


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


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


其他数据类型:计数


640?wx_fmt=png

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


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


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


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

640?wx_fmt=png

计数器 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,实现全增量复制,通过这种方式,热数据单机可以存百亿级,冷数据可以存千亿级。

640?wx_fmt=png

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


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


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


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


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


640?wx_fmt=png

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


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


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


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


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

640?wx_fmt=png

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


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


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

640?wx_fmt=png

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


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


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

640?wx_fmt=png

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


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

640?wx_fmt=png

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


进一步优化


640?wx_fmt=png

服务化


640?wx_fmt=png

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

640?wx_fmt=png

640?wx_fmt=png

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


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


总结与展望


640?wx_fmt=png

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

看完本文有收获?请转发分享给更多人


欢迎关注“互联网架构师”,我们分享最有价值的互联网技术干货文章,助力您成为有思想的全栈架构师,我们只聊互联网、只聊架构,不聊其他!打造最有价值的架构师圈子和社区。

本公众号覆盖中国主要首席架构师、高级架构师、CTO、技术总监、技术负责人等人 群。分享最有价值的架构思想和内容。打造中国互联网圈最有价值的架构师圈子。

  • 长按下方的二维码可以快速关注我们

  • 640?wx_fmt=jpeg

    如想加群讨论学习,请点击右下角的“加群学习”菜单入群

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值