微博日活跃用户超过1.6亿,日访问量达百亿。面对庞大用户群的海量访问,良好的架构和不断完善的缓存系统起到了非常重要的支持作用。本文由新浪微博技术专家陈博先生分为以下四个部分,详细阐述海量数据是如何呈现的:微博Feed平台系统架构运行过程中的数据挑战缓存架构及演进总结及期待微博Feed平台系统架构在运行过程中的数据挑战Feed平台系统架构分为五层:最上层是端层,比如web端,client端,一些iOS或者Android客户端,大家使用,以及一些开放平台和一些第三方接入的接口。下一层是平台接入层。不同的pool主要是将好的资源集中分配给重要的核心接口,这样在遇到突发流量的时候,会有更好的弹性去服务,提高服务的稳定性。.接下来是平台服务层,主要是feed算法,关系等等。接下来是中间层,通过各种中介提供一些服务。底层是存储层。FeedTimeline当你每天浏览微博时,比如在主站或客户端点击刷新,最后会得到十到十五条微博。这是如何构建的?刷新后会优先获取用户的关注关系。比如他有一千个粉丝,他就得到这千个ID,然后根据这千个UID得到每个用户发布的一些微博。同时会获取到用户的收件箱,收件箱是他收到的一些特殊的消息,比如一些群里的微博,群里的微博,关注关系,关注者的微博列表。拿到这一系列的微博列表后,收集整理,得到自己需要的id,然后为这些id取每个微博id对应的微博内容。如果转发了这些微博,它还有一条原微博,还要进一步抓取原微博的内容。通过原始微博获取用户信息,根据用户的过滤词进一步过滤这些微博,过滤掉用户不想看的微博。根据以上步骤留下的微博,我们会进一步查看用户是否对这些微博进行了收藏和点赞,设置了一些flag,同时也会对这些微博的各种统计、转发、评论、点赞等进行汇总,才做***将十几条微博返回给用户的各个终端。由此看来,后台服务器很可能需要针对用户一次请求的十几条记录,实时汇集成百上千条记录,然后返回给用户。整个过程取决于Cache系统的强弱,因此Cache架构设计的好坏将直接影响到微博系统的性能。FeedCache架构接下来我们看一下Cache的架构,主要分为六层:第一层是Inbox,主要是对一些微博进行分组,然后直接feed一些群主的微博。收件箱比较小,主要是推送的形式。第二层是发件箱。每个用户都会发布常规微博并转到其发件箱。根据存储的ID个数,其实分为多个缓存。一般有200多个缓存,长的话2000个缓存左右。第三层是一些关系,它的关注度、粉丝、用户。第四层是内容,每条微博的一些内容都存放在这里。第五层是一些存在性的判断,比如我有没有点赞过某个微博。之前有明星说我不喜欢这条微博,怎么就说明我喜欢了,引起了一些新闻。这是唱片,她在某个时候确实喜欢它,但可能忘记了。最下面是比较大的一层——计数,每条微博的评论数和转发数,还有用户关注数和粉丝数。缓存架构及演进SimpleKVdatatype下面重点介绍微博缓存架构的演进过程。微博刚上线的时候,我们把它存储成一个简单的KV数据类型。我们主要使用哈希分片存储在MC池中。上线几个月后,我们发现了一些问题:部分节点机器宕机或者其他原因,大量的请求会穿透Cache层到达DB,导致整个请求变慢,甚至DB冻结。所以我们很快做了改造,加了一个HA层,这样即使Main层的一些节点宕机或者挂了,这些请求也会进一步渗透到HA层,而不是DB层。这样可以保证在任何情况下,整个系统的攻击率都不会降低,大大提高了系统服务的稳定性。对于这种方式,现在业界用的比较多,很多人说我直接用hash,其实也有一些坑。比如我有一个节点,节点3宕机了,Main去掉,节点3的一些QA分发给其他几个节点。业务量不是很大,穿透DB的时候DB还是可以承受的。但是如果这个节点3恢复了,再添加之后,节点3的访问又回来了。后面由于网络原因或者机器原因,节点3又会宕机,节点3的一些请求会分发到其他节点。这时候,就会出现问题。写回其他节点的数据还没有更新。如果不删除,就会有混杂的数据。其实微博是一个方型的生意。比如遇到紧急情况,明星找女朋友,瞬间流量会是30%。紧急情况之后,一些节点上会出现大量的请求,这会导致这些节点非常热。如此大量的请求,即便是MC也无法满足。这时候MC就会成为瓶颈,导致整个系统变慢。为此,我们引入了L1层,它也是一个主要的关系池。每个L1大约是主层的N分之一,内存的六分之一、八分之一、十分之一。根据请求我会在数量上加上4到8个L1,这样所有的请求来的时候都会先访问L1。如果L1中有***,则直接访问。如果没有***,就会访问Main-HA层,这样在一些突发流量中,L1可以抵挡大部分的热点请求。对于微博本身来说,新的数据越热,只要加少量的内存,就能抵御更大的量。简单总结一下:通过简单的KV数据类型的存储,我们其实是基于MC的,层中的Hash节点不漂移,Miss穿透到下一层读取。通过提高多组L1的读取性能,可以承受峰值和突发流量,成本会大大降低。读写策略采用多写,读采用逐层穿透,使用Miss则回写。其中存储的数据,我们最初使用的是Json/xml,2012年后直接采用ProtocolBuffer格式,使用QuickL压缩一些比较大的数据。采集数据刚才提到了简单的QA数据,复杂的采集数据如何处理?比如我关注了2000人,加了1个人,就涉及到一些修改。一种方法是把2000个ID全部拿下来修改,但是这样会给带宽和机器带来很大的压力。还有一些逐页收购。我存了2000,只需要取前几页,比如第二页,也就是第十到二十页。您不能完整检索所有数据吗?还有一些资源的联动计算,会计算到我关注的一些人里面。ABC也跟着用户D,这种涉及到一些数据,包括计算的修改和获取,其实对MC来说不是很好。各种注意力关系存储在Redis中,通过Hash分布、存储、一组多种存储方式实现读写分离。现在Redis有30T左右的内存,每天有2-3万亿次请求。在使用Redis的过程中,其实还遇到了一些其他的问题。比如从关注关系来看,我关注了2000个UID,一种方式是全量存储。但是微博用户量大,有的用户登录少,有的用户活跃度很高,全部存入内存的成本比较高。所以我们把Redis的使用改成了Cache。例如,只存储活跃用户。如果你有一段时间没有活跃,你会被踢出Redis,等有下次访问的时候再加入。这时候就有问题了,因为Redis的工作机制是单线程模式。如果加上某个UV,聚焦2000个用户,可能会扩展到20000个UID。20000个UID插回去之后,基本上Redis就卡住了。提供其他服务的方式。所以我们扩展一个新的数据结构,20000个UID直接起步,写的时候直接一个一个写到Redis里面,整体读写效率会很高。它的实现是一个long类型的开放数组,由DoubleHash寻址。我们对Redis做了一些其他的扩展。大家可能在网上看到过我们之前的一些分享,把数据放到公共变量中。整个升级过程中,我们测试1G加载需要10分钟,10G需要10多分钟。现在是毫秒级的升级。对于AOF,我们采用滚动AOF,每个AOF都有一个ID,达到一定数量后,滚动到下一个AOF。在实现RDB的时候,我们会在构建这个RDB的时候记录下AOF文件以及它所在的位置,通过新的RDB和AOF的扩展方式实现全增量复制。其他数据类型:Count接下来还有一些其他的数据类型,比如count,其实每个互联网公司都会遇到。对于一些中小企业来说,MC和Redis其实已经足够了。.但是微博中的计数有一些特点:一个Key有多个计数,比如一条微博,有转发数、评论数、点赞数;用户有各种数字,例如粉丝和关注者的数量。因为是计数,所以它的Value大小比较小。根据其各种业务场景,大概2-8个字节,一般都在4个字节以上。那么每天新增的微博记录大约有十亿条,总记录量会更加可观。那么,一个请求可能有数百条记录需要返回。counterCounterService最初可以使用Memcached,但是它有一个问题。如果计数超出其内容容量,会导致部分计数被清除,关机或重启后计数消失。另外,可能有很多计数是0,那么这个时候怎么存,存不存,会占用很多内存。微博每天都有几十亿条计数,0的内存会占用大量内存。如果不存储,会渗透到DB中,影响服务的可溶性。2010年后,我们再次使用Redis进行访问。随着数据量的增加,我们发现Redis的内存负载还是比较低的,一个KV大概需要至少65字节。但实际上我们一个count需要8个字节,Value需要4个字节左右,所以有效的只有12个字节,浪费了40多个字节。这只是一个单独的KV,如果一个Key有多个count,那浪费就更多了。比如4个count,一个key是8个字节,4个count每个4个字节,16个字节大概需要26个字节,但是用Redis存储需要200个字节左右。后来通过自己开发的CounterService,将内存降低到不到Redis的五分之一到十五分之一,冷热数据分离。热数据存储在内存中。如果冷数据再次变热,则将其存储在LRU中的Go中。实现RDB和AOF实现全增量复制。这样单机就可以存储百亿级的热数据,可以存储千亿级的冷数据。整个存储架构大致如上图所示,上面是内存,下面是SSD。在内存中,预先分成N个Table,每个Table根据ID的指针顺序划出一定的范围。当任何一个ID过来的时候,先找到它所在的Table,如果里面有直接增加或者减少,就来一个新的count,当发现内存不够的时候,会dump一个小的TableDump进去SSD和一个新的将被保留。该位置放在最上面,供新ID使用。有人质疑,如果在一定范围内,我的ID原本设置的计数是4字节,微博火爆超过了4字节,变成很大的计数怎么办?对于那些超过限制的,我们将它们存储在Auxdict中。对于落在SSD中的Tables,我们有专门的IndAux进行访问,通过RDB进行copy。其他数据类型:存在性判断微博除了统计之外,还有一些业务和一些存在性判断。比如一条微博显示有没有点赞、阅读、推荐,如果用户已经看过这条微博,就不要再给他看。这种有一个很大的特点,就是检查是否存在,每条记录很小,比如1bit的Value就够了,但是数据总量很大。比如微博,每天大约发布1亿条新微博,需要判断的总数据可能有数百亿、上千亿。怎么存是个大问题,里面很多存在都是0,前面说了,要存0吗?如果存储,每天将存储数千亿条记录;如果不存储,大量的请求最终会穿透Cache层到达DB层,没有哪个DB能抗得住这么大的流量。我们也做了一些选择:第一,直接考虑能不能用Redis。单个KV为65字节。如果一个KV可以是8个字节,那么Value就只有1位。这样,每天添加新内存的效率就很低了。我们新开发的第二种CounterService,单个KVValue1位,我只存1个字节,一共9个字节就够了。这样每天新增900G内存。如果存放的话,可能只能存放几天。三天下来,差不多3T了,压力挺大的,不过比Redis好太多了。我们最终的方案是自己开发Phantom,先分段分配共享内存,最后只使用120G内存。算法很简单,每个Key可以哈希N次,如果哈希的某一位为1,那么就哈希3次,三个数都置1。哈希X2三次,后面判断X1是否存在,从三个哈希中,如果为1,则认为存在;如果某个hashX3,其位计算为0,则***肯定不存在。它的实现架构比较简单。sharedmemory预先拆分成不同的Tables,open计算在里面进行,然后读写。如果实现的话,就是AOF+RDB处理的。因为整个进程都放在共享内存中,即使进程升级重启,数据也不会丢失。对外访问时,构建Redis协议,可以直接扩展新的协议来访问我们的服务。总结:至此,我们关注的是Cache集群中组件的高可用、可扩展、高性能。另一个重要的事情是存储成本。现在有几千几乎几万台服务器等等。进一步优化服务的解决方案是首先实现对整个缓存和配置的服务化管理,避免频繁重启。另外如果配置有变化,直接用脚本修改即可。Servitization也引入了ClusterManager实现对外管理,通过接口进行管理,进行服务校验。在服务治理方面,可以实现扩容和缩容,也可以很好的保证SLA。另外,为了开发,现在可以屏蔽Cache资源。总结与展望***简单总结一下,对于微博Cache架构,我们从数据架构、性能、存储成本、服务等不同方面进行了优化和提升。欢迎对此有研究或疑问的同仁留言讨论。
