微博架构01

微博feed系统的推(push)模式和拉(pull)模式和时间分区拉模式架构探讨

 推模式需要把一篇微博推送给所有关注他的人(推给所有的粉丝),比如姚晨,我们就需要推送给2594751个用户的feeds表中。当然,feeds表可以很好的进行sharding,存储也都是一些数字型的字段,存储空间可能不是很大,用户在查询自己关注的所有人的feed时,速度快,性能非常高,但是推送量会非常大,姚晨发表一篇,就会产生200多万条数据。试想,一个大量用户的微薄系统通过使用推模式,是不是会产生非常惊人的数据呢?

 拉模式只需要用户发表微博时,存储一条微博数据到feeds表中(feeds表可以是一个临时表,只保存近期可接受范围的数据).用户每次查询feed时都会去查询feeds表。比如姚晨打开自己的微博首页,就产生:SELECT id FROM feeds where uid in(following uid list) ORDER BY id DESC LIMIT n(查询最新的n条),缓存到memcached

uidlist=>{data:id list,timeline:上次查询出来的最新的一条数据的时间}

再次刷新:SELECT id FROM feeds where uid in(following uid list) AND timeline>(memcached存储的上次的timeline) ORDER BY id DESC LIMIT n

 

    这种模式实现起来也是比较简单和容易的,只是在查询的时候需要多考虑下缓存的结构。但是feeds表会产生很大的压力,怎么说feeds表也要保存最近十天半个月的数据吧,对于一个大点的系统,这会产生比较大的数据,如果following的人数比较多,数据库的压力就会非常大。而且一般在线的用户,客户端都会定期扫描,又会增加很大的压力,这在查询性能上没有推模式的效率高。

  这种模式实现起来也是比较简单和容易的,只是在查询的时候需要多考虑下缓存的结构。但是feeds表会产生很大的压力,怎么说feeds表也要保存最近十天半个月的数据吧,对于一个大点的系统,这会产生比较大的数据,如果following的人数比较多,数据库的压力就会非常大。而且一般在线的用户,客户端都会定期扫描,又会增加很大的压力,这在查询性能上没有推模式的效率高。

     下面我们在对拉模式做一下改进优化

   图五:拉模式(pull)-改进(时间分区拉模式)

           拉模式的改进主要是在feeds的存储上,使用按照时间进行分区存储。分为最近时间段(比如最近一个小时),近期的,比较长时期等等。我们再来看下查询的流程,比如姚晨登陆微博首页,假设缓存中没有任何数据,那么我们可以查询比较长时期的feeds表,然后进入缓存。下一次查询,通过查询缓存中的数据的timeline,如果timeline还在最近一个小时内,那么只需要查询最近一个小时的数据的feed表,最近一个小时的feeds表比图四的feeds表可要小很多,查询起来速度肯定快几个数量级了。

          改进模式的重点在于feeds的时间分区存储,根据上次查询的timeline来决定查询应该落在那个表。一般情况下,经常在线的用户,频繁使用的客户端扫描操作,经常登录的用户,都会落在最近的feeds表区间,查询都是比较高效的。只有那些十天,半个月才登录一次的用户需要去查询比较长时间的feeds大表,一旦查询过了,就又会落在最近时间区域,所以效率也是非常高的。

         关于时间的分区,需要根据数据量,用户访问特点进行一个合理的切分。如果数据发表量非常大,可以进行更多的分区。

        上面介绍的推模式和拉模式都有各自的特点,个人觉得时间分区拉模式弥补了图四的拉模式的很大的不足,是一个成本比较低廉的解决方案。当然,时间分区拉模式也可以结合推模式,根据某些特点来增加系统的性能。

第三代技术体系

微博平台的第三代技术体系,使用正交分解法建立模型,在水平方向,采用典型的三级分层模型,即接口层、服务层与资源层,在垂直方向,进一步细分为业务架构、技术架构、监控平台与服务治理平台,接着看一下平台的整体架构图

 

服务层框架
服务层主要涉及RPC远程调用框架以及消息队列框架,这是微博平台在服务层使用最为广泛的两个框架。

MCQ消息队列 

消息队列提供一种先入先出的通讯机制,在平台内部,最常见的场景是将数据的落地操作异步写入队列,队列处理程序批量读取并写入DB,消息队列提供的异步机制加快了前端机的响应时间,其次,批量的DB操作也间接的提高了DB操作性能,另外一个应用场景,平台通过消息队列,向搜索、大数据、商业运营部门提供实时数据。

微博平台内部大量使用的MCQ(SimpleQueue Service Over Memcache)消息队列服务,基于MemCache协议,消息数据持久化写入BerkeleyDB,只有get/set两个命令,同时也非常容易做监控(stats queue),丰富的client library,线上运行多年,性能比通用的MQ高很多倍。

Motan RPC框架

微博的Motan RPC服务,底层通讯引擎采用了Netty网络框架,序列化协议支持Hessian和Java序列化,通讯协议支持Motan、http、tcp、mc等,Motan框架在内部大量使用,在系统的健壮性和服务治理方面,有较为成熟的技术解决方案,健壮性上,基于Config配置管理服务实现了High Availability与Load Balance策略(支持灵活的FailOver和FailFast HA策略,以及Round Robin、LRU、Consistent Hash等Load Balance策略),服务治理方面,生成完整的服务调用链数据,服务请求性能数据,响应应时间(Response Time)、QPS以及标准化Error、Exception日志信息
 

资源层框架
资源层的框架非常多,有封装MySQL与HBase的Key-List DAL中间件、有定制化的计数组件,有支持分布式MC与Redis的Proxy,在这些方面业界有较多的经验分享,我在这里分享一下平台架构的对象库与SSD Cache组件。

对象库

对象库支持便捷的序列化与反序列化微博中的对象数据,序列化时,将JVM内存中的对象序列化写入在HBase中并生成唯一的ObjectID,当需要访问该对象时,通过ObjectID读取,对象库支持任意类型的对象,支持PB、JSON、二进制序列化协议,微博中最大的应用场景将微博中引用的视频、图片、文章统一定义为对象,一共定义了几十种对象类型,并抽象出标准的对象元数据Schema,对象的内容上传到对象存储系统(Sina S3)中,对象元数据中保存Sina S3的下载地址。

SSDCache

随着SSD硬盘的普及,其优越的IO性能被越来越多的替换传统的SATA和SAS磁盘,常见的应用场景有三种:1)替换MySQL数据库的硬盘,目前社区还没有针对SSD优化的MySQL版本,即使这样,直接升级SSD硬盘也能带来8倍左右的IOPS提升;2)替换Redis的硬盘,提升其性能;3)用在CDN中,加快静态资源加载速度。

微博平台将SSD应用在分布式缓存场景中,将传统的Redis/MC + Mysql方式,扩展为 Redis/MC + SSD Cache + Mysql方式,SSD Cache作为L2缓存使用,第一降低了MC/Redis成本过高,容量小的问题,也解决了穿透DB带来的数据库访问压力。
 

微博发布模式

  • 同步推模式 

早期的架构中,用户发表微博后,系统会立即将这条微博插入到数据库所有分析的订阅列表中。当用户量较大时,特别是明星用户发布微博时,会引起大量的数据库写操作,系统性能急剧下降,发布微博延迟加剧。

 

异步推拉模式 
用户发表微博后,系统将微博写入消息队列后立即返回,用户响应迅速。消息队列的消费者任务将微博推送给所有当前在线的粉丝的订阅列表中,非在线用户在登录后再根据关注列表拉去微博订阅列表
我们再看一下目前即将推出的微博平台的新架构。我们知道API大部分的请求都为了获取最新的数据。API请求有一个特点,它大目前调用都是空返回的,比如说一款手机的客户端每隔一分钟它都要调用服务器一下,就是有没有新数据,大目前的调用都是空返回,就是说不管服务器有没有数据都要调用一次。这次询问到下一次询问中间,如果有新的数据来了,你是不会马上知道的。因此我们想API能不能改用推的方式,就是客户端不需要持续的调用,如果有新数据就会推过去。技术特点,显而易见低延迟,就是从发表到接受1秒内完成,实际上可能用不了1秒。然后服务端的连接就是高并发长连接服务,就是多点都连接在我们的服务器上,这个比传统的API要大。
  我们再看一下内部细节,就是我们收到数据之后首先要经过最上面RECEIVER。然后推到我们的引擎里面,这个引擎会做两个事情,首先会把用户的关系拿过来,然后按照用户关系马上推送给他相应的粉丝。所以我们调用方已经在那儿等待了,我们需要有一个唤醒操作,就是说在接口这儿把它唤醒,然后把它发送过去。最后是一个高并发的长连服务器,就是一台服务器支持10万以上的并发连接。最右边中间有一个圆圈叫做Stream Buffer,我们需要Stream Buffer是要保存用户最近的数据。因为用户可能会有断线的,比如说他发送数据的时候断线半分钟,我们需要把这半分钟补给他。这就是我们的推送架构。

 

 

微博类系统我认为是互联网业务系统中最复杂和最吃性能的。简单举两个最常用的操作为例:

pull操作分析:假设平均一个用户关注30个人,那么他的一次pull就会包含查询所有这30个人的最新若干条消息。然后拉通按照时间进行从近到远排序,选出若干条(假设15条)。然后再去查询出这15条微博的内容,再返回给该用户。这才完成一个用户的一次pull。而微博系统中pull的触发量是非常大的。一个在线用户如果在浏览时间线,那可能一分多钟就会触发一次。一百万在线光pull就会产生10万的QPS。
post操作分析:一个大V就是几百万几千万的粉丝量。他的一次post,就会触发对这些粉丝的消息推送。其中有在线的,有不在线的。twitter对推送的要求是一个5千万粉的大V的一条消息需要在5秒内送达所有粉丝
 

Feed 平台系统架构

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

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

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

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

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

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

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

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

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

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

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

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

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

基于这个原因,我们引入了 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 进行压缩。

刚才讲到简单的 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 里面去,读写的整个效率就会非常高。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

哪些产品是feed流典型业务?

:微博,微信朋友圈,Pinterest是典型的feed流业务,系统中的每一条消息就是一个feed。

这类业务的特点是:

  • 有好友关系,例如关注,粉丝

  • 我们的主页由别人发布的feed组成

这类业务的典型动作是:

  • 关注,取关

  • 发布feed

  • 拉取自己的主页feed流

这类业务的核心元数据是:

  • 关系数据

  • feed数据

feed流的“拉取”与“推送”实现,是个怎么回事?

:feed流业务最大的特点是“我们的主页由别人发布的feed组成”,获得朋友圈消息feed流集合,从技术上说,主要有“拉取”与“推送”两种方式。feed流的推与拉主要指的是这里。

 

上一篇《feed流拉取,读扩散,究竟是啥?》关于feed流的拉取还是推送,只写了一半“拉取”,今天把另一半“推送”(写扩散)的坑填完。

在拉模式中,用户A获取“由别人发布的feed组成的主页”的过程及其复杂,此时需要:

  • 获取A的关注列表

  • 获取所关注列表中,所有用户发布的feed

  • 对消息进行rank排序(假设按照发布时间排序),分页取出对应的一页feeds

feed流的拉模式(“读扩散”)的优点是:

  • 存储结构简单,数据存储量较小,关系数据与feed数据都只存一份

  • 关注,取关,发布feed的业务流程非常简单

  • 存储结构,业务流程都比较容易理解,适合项目早期用户量、数据量、并发量不大时的快速实现

缺点也显而易见:

  • 拉取朋友圈feed流列表的业务流程非常复杂

  • 有多次数据访问,并且要进行大量的内存计算,网络传输,性能较低

二、推模式 “写扩散”方案简介

推模式(写扩散),关系数据的存储与拉模式(读扩散)完全一样。

feed数据,每个用户也存储自己发布的feed

feed数据存储,与拉(读扩散)不同的是,每个用户还需要存储自己收到的feed流

如上图:

  • A关注了BC,所以A的接收队列是1,2,3,5,8,10

  • D关注了B,所以D的接收队列是1,3,5,10

在推模式(写扩散)中,获取“由别人发布的feed组成的主页”会变得异常简单,假设一页消息为3条feed,A如果要看自己朋友圈的第二页消息,直接返回1,2,3即可。

画外音:第一页朋友圈是最新的消息,即5,8,10。

其缺点是:

  • 极大极大消耗存储资源,feed数据会存储很多份,例如杨幂5KW粉丝,她每次一发博文,消息会冗余5KW份

画外音:有朋友提出,可以存储一份消息实体,只冗余msgid,这样的话,拉取feed流列表时,还要再次拉取实体,网络时延会更长,所以很多公司选择直接冗余消息实体,当然,这是一个用户体验与存储量的折衷设计。

  • 新增关注,取消关注,发布feed的业务流会更复杂

三、小结

feed流业务的推拉模式小结:

  • 拉模式,读扩散,feed存一份,存储小,用户集中访问数据,性能差

  • 推模式,写扩散,feed存多份,用冗余存储换锁冲突,性能高

新浪微博使用推拉结合的方式,大号不推送,小号则推送,看Feeds的时候,需要将推过来的Feeds索引数据与关注的大号的Feed进行聚合,小小的牺牲下拉的性能,不过一下子就将大号的推送问题解决掉了!

大V账号采用拉取模式,小账号则采取推送方式

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值