极客时间已完结课程限时免费阅读

40 | 信息流设计(二):通用信息流系统的拉模式要如何做?

40 | 信息流设计(二):通用信息流系统的拉模式要如何做?-极客时间

40 | 信息流设计(二):通用信息流系统的拉模式要如何做?

讲述:唐扬

时长11:20大小10.39M

你好,我是唐扬。
在前一节课中,我带你了解了如何用推模式来实现信息流系统,从中你应该了解到了推模式存在的问题,比如它在面对需要支撑很大粉丝数量的场景时,会出现消息推送延迟、存储成本高、方案可扩展性差等问题。虽然我们也会有一些应对的措施,比如说选择插入性能更高的数据库存储引擎来提升数据写入速度,降低数据推送延迟;定期删除冷数据以减小存储成本等等,但是由于微博大 V 用户粉丝量巨大,如果我们使用推模式实现信息流系统,那么只能缓解这些用户的微博推送延迟问题,没有办法彻底解决。
这个时候你可能会问了:那么有没有一种方案可以一劳永逸地解决这个问题呢?当然有了,你不妨试试用拉模式来实现微博信息流系统。那么具体要怎么做呢?

如何使用拉模式设计信息流系统

所谓拉模式,就是指用户主动拉取他关注的所有人的微博,将这些微博按照发布时间的倒序进行排序和聚合之后,生成信息流数据的方法。
按照这个思路实现微博信息流系统的时候你会发现:用户的收件箱不再有用,因为信息流数据不再出自收件箱,而是出自发件箱。发件箱里是用户关注的所有人数据的聚合。因此用户在发微博的时候就只需要写入自己的发件箱,而不再需要推送给粉丝的收件箱了,这样在获取信息流的时候,就要查询发件箱的数据了。
这个逻辑我还用 SQL 的形式直观地表达出来,方便你理解。假设用户 A 关注了用户 B、C、D,那么当用户 B 发送一条微博的时候,他会执行这样的操作:
insert into outbox(userId, feedId, create_time) values("B", $feedId, $current_time); //写入B的发件箱
当用户 A 想要获取他的信息流的时候,就要聚合 B、C、D 三个用户收件箱的内容了:
select feedId from outbox where userId in (select userId from follower where fanId = "A") order by create_time desc
你看,拉模式的实现思想并不复杂,并且相比推模式来说,它有几点明显的优势。
首先,拉模式彻底解决了推送延迟的问题,大 V 发微博的时候不再需要推送到粉丝的收件箱,自然就不存在延迟的问题了。
其次,存储成本大大降低了。在推模式下,谢娜的粉丝有 1.2 亿,那么谢娜发送一条微博就要被复制 1.2 亿条,写入到存储系统中。在拉模式下只保留了发件箱,微博数据不再需要复制,成本也就随之降低了。
最后,功能扩展性更好了。比如,微博增加了分组的功能,而你想把关注的 A 和 B 分成一个单独的组,那么 A 和 B 发布的微博就形成了一个新的信息流,这个信息流要如何实现呢?很简单,你只需要查询这个分组下所有用户(也就是 A 和 B),然后查询这些用户的发件箱,再把发件箱中的数据,按照时间倒序重新排序聚合就好了。
List<Long> uids = getFromGroup(groupId); //获取分组下的所有用户
Long<List<Long>> ids = new ArrayList<List<Long>>();
for(Long id : uids) {
ids.add(getOutboxByUid(id)); //获取发件箱的内容id列表
}
return merge(ids); //合并排序所有的id
拉模式之所以可以解决推模式下的所有问题,是因为在业务上关注数始终是有上限的,那么它是不是一个无懈可击的方案呢?当然不是,拉模式也会有一些问题,在我看来主要有这样两点。
第一点,不同于推模式下获取信息流的时候,只是简单地查询收件箱中的数据,在拉模式下,我们需要对多个发件箱的数据做聚合,这个查询和聚合的成本比较高。微博的关注上限是 2000,假如你关注了 2000 人,就要查询这 2000 人发布的微博信息,然后再对查询出来的信息做聚合。
那么,如何保证在毫秒级别完成这些信息的查询和聚合呢?答案还是缓存。我们可以把用户发布的微博 ID 放在缓存中,不过如果把全部用户的所有微博都缓存起来,消耗的硬件成本也是很高的。所以我们需要关注用户浏览信息流的特点,看看是否可能对缓存的存储成本做一些优化。
在实际执行中,我们对用户的浏览行为做了分析,发现 97% 的用户都是在浏览最近 5 天之内的微博,也就是说,用户很少翻看五天之前的微博内容,所以我们只缓存了每个用户最近 5 天发布的微博 ID。假设我们部署 6 个缓存节点来存储这些微博 ID,在每次聚合时并行从这几个缓存节点中批量查询多个用户的微博 ID,获取到之后再在应用服务内存中排序后就好了,这就是对缓存的 6 次请求,可以保证在 5 毫秒之内返回结果。
第二,缓存节点的带宽成本比较高。你想一下,假设微博信息流的访问量是每秒 10 万次请求,也就是说,每个缓存节点每秒要被查询 10 万次。假设一共部署 6 个缓存节点,用户人均关注是 90,平均来说每个缓存节点要存储 15 个用户的数据。如果每个人平均每天发布 2 条微博,5 天就是发布 10 条微博,15 个用户就要存储 150 个微博 ID。每个微博 ID 要是 8 个字节,150 个微博 ID 大概就是 1kB 的数据,单个缓存节点的带宽就是 1kB * 10 万 = 100MB,基本上跑满了机器网卡带宽了。那么我们要如何对缓存的带宽做优化呢?
14 讲中我提到,部署多个缓存副本提升缓存可用性,其实,缓存副本也可以分摊带宽的压力。我们知道在部署缓存副本之后,请求会先查询副本中的数据,只有不命中的请求才会查询主缓存的数据。假如原本缓存带宽是 100M,我们部署 4 组缓存副本,缓存副本的命中率是 60%,那么主缓存带宽就降到 100M * 40% = 40M,而每组缓存副本的带宽为 100M / 4 = 25M,这样每一组缓存的带宽都降为可接受的范围之内了。
在经过了上面的优化之后,基本上完成了基于拉模式信息流系统方案的设计,你在设计自己的信息流系统时可以参考借鉴这个方案。另外,使用缓存副本来抗流量也是一种常见的缓存设计思路,你在项目中必要的时候也可以使用。

推拉结合的方案是怎样的

但是,有的同学可能会说:我在系统搭建初期已经基于推模式实现了一套信息流系统,如果把它推倒重新使用拉模式实现的话,系统的改造成本未免太高了。有没有一种基于推模式的折中的方案呢?
其实我在网易微博的时候,网易微博的信息流就是基于推模式来实现的,当用户的粉丝量大量上涨之后,我们通过对原有系统的改造实现了一套推拉结合的方案,也能够基本解决推模式存在的问题,具体怎么做呢?
方案的核心在于大 V 用户在发布微博的时候,不再推送到全量用户,而是只推送给活跃的用户。这个方案在实现的时候有几个关键的点。
首先,我们要如何判断哪些是大 V 用户呢?或者说,哪些用户在发送微博时需要推送全量用户,哪些用户需要推送活跃用户呢?在我看来,还是应该以粉丝数作为判断标准,比如,粉丝数超过 50 万就算作大 V,需要只推送活跃用户。
其次,我们要如何标记活跃用户呢?活跃用户可以定义为最近几天内在微博中有过操作的用户,比如说刷新过信息流、发布过微博、转发评论点赞过微博,关注过其他用户等等,一旦有用户有过这些操作,我们就把他标记为活跃的用户。
而对大 V 来说,我们可以存储一个活跃粉丝的列表,这个列表里面就是我们标记的活跃用户。当某一个用户从不活跃用户变为活跃用户时,我们会查询这个用户的关注者中哪些是大 V,然后把这个用户写入到这些大 V 的活跃粉丝列表里面,这个活跃粉丝列表是定长的,如果活跃粉丝数量超过了长度,就把最先加入的粉丝从列表里剔除,这样可以保证推送的效率。
最后,一个用户被从活跃粉丝列表中剔除,或者是他从不活跃变成了活跃后,由于他不在大 V 用户的活跃粉丝列表中,所以也就不会收到微博的实时推送,因此,我们需要异步地把大 V 用户最近发布的微博插入到他的收件箱中,保证他的信息流数据的完整性。
采用推拉结合的方式可以一定程度上弥补推模式的缺陷,不过也带来了一些维护的成本,比如说系统需要维护用户的在线状态,还需要多维护一套活跃的粉丝列表数据,在存储上的成本就更高了。
因此,这种方式一般适合中等体量的项目,当粉丝量级在百万左右,活跃粉丝数量在 10 万级别时,一般可以实现比较低的信息传播延迟以及信息流获取延迟,但是当你的粉丝数量继续上涨,流量不断提升之后,无论是活跃粉丝的存储还是推送的延迟都会成为瓶颈,所以改成拉模式会更好的支撑业务。

课程小结

以上就是本节课的全部内容了。本节课我带你了解了基于拉模式和推拉结合模式实现信息流系统的方案,这里你需要了解的几个重点是:
在拉模式下,我们只需要保存用户的发件箱,用户的信息流是通过聚合关注者发件箱数据来实现的;
拉模式会有比较大的聚合成本,缓存节点也会存在带宽的瓶颈,所以我们可以通过一些权衡策略尽量减少获取数据的大小,以及部署缓存副本的方式来抗并发;
推拉结合的模式核心是只推送活跃的粉丝用户,需要维护用户的在线状态以及活跃粉丝的列表,所以需要增加多余的空间成本来存储,这个你需要来权衡。
拉模式和推拉结合模式比较适合微博这种粉丝量很大的业务场景,因为它们都会有比较可控的消息推送延迟。你可以看到,在这两节课程中我们灵活使用数据库分库分表、缓存消息队列、发号器等技术,实现了基于推模式、拉模式以及推拉结合模式的信息流系统,你在做自己系统的方案设计时,应该充分发挥每种技术的优势,权衡业务自身的特性,最终实现技术和业务上的平衡,也就是既能在业务上满足用户需求,又能在技术上保证系统的高性能和高可用。

一课一思

在你的项目中是否有使用过拉模式来实现信息流系统呢?在方案设计过程中都遇到过哪些问题呢?你是如何解决的呢?欢迎在留言区与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 9

提建议

上一篇
39 | 信息流设计(一):通用信息流系统的推模式要如何做?
下一篇
结束语 | 学不可以已
 写留言

精选留言(31)

  • 👽
    2020-02-24
    推 的概念近似于邮箱,杂志提供商,把杂志直接投到每订阅用户的邮箱里; 拉 的概念类似于广告牌,广告商只把自己的广告挂出来,而用户端来获取自己想看的广告内容; 起初用户量小,微博删除不频繁,可以用推。让邮递员挨个去送就好。 随着用户体量增大,邮递员跑步过来了。于是干脆直接改用广告牌的形式,直接发布,谁爱看谁看。 另外,就业务与现实中的例子的考量,个人认为拉更合适。因为收件箱的概念,只适用于不频繁的修改。类似于邮件,发到你邮箱里就是发到了。而微博,发到你收件箱了,如果删除了,还得从你收件箱里把邮件拿出来。就不是很合理。
    展开

    作者回复: 是的,微博的场景用拉比较合适

    共 2 条评论
    14
  • 小白哥哥
    2019-12-25
    “如果活跃粉丝数量超过了长度,就把最先加入的粉丝从列表里剔除”,这块是不是会有问题。 最先假如的活跃粉丝很可能一直是活跃的,因为长度有限的原因就把他挑出来会不会不太好。

    作者回复: 剔除了无非就是无法实时获取到消息,但是他还是会被加入到活跃粉丝列表中,而且,在刷新信息流的时候可以拉取大V的发件箱做数据补偿

    10
  • 旅途
    2019-12-25
    老师有两个地方没懂希望能解答下 1.缓存副本 100m缓存了60% 使用了60m带宽 ,主缓存 消耗剩下40%就是40m带宽 这样的话 加起来不还是100m带宽吗 2.推拉模式的例子,只有活跃用户是实时推送,不活跃用户异步推送,这样的话不是都使用的推模式吗?

    作者回复: 1. 但是每一个缓存的带宽降低了 2. 不活跃用户是不推送,但是不活跃用户上线后会拉取发件箱,所以叫推拉结合

    共 3 条评论
    5
  • QQ怪
    2019-12-25
    这两篇真的实践出真理,好厉害老师

    作者回复: 谢谢,我只是站在前人的肩膀上😂😂

    6
  • flycun
    2020-04-05
    干货满满

    作者回复: 谢谢🙏

    4
  • nestle
    2020-02-03
    请问缓存中只放微博ID,实际内容还要去DB查吗?

    作者回复: 当然了~

    3
  • null
    2020-11-27
    当粉丝量级在百万左右,活跃粉丝数量在 10 万级别时,一般可以实现比较低的信息传播延迟以及信息流获取延迟,但是当你的粉丝数量继续上涨,流量不断提升之后,无论是活跃粉丝的存储还是推送的延迟都会成为瓶颈。 拉模式和推拉结合模式比较适合微博这种粉丝量很大的业务场景,因为它们都会有比较可控的消息推送延迟。 这两段是不是前后矛盾了? 前一段的意思是推拉结合模式,不太适合粉丝数量大的场景。 后一段又说推拉结合模式,比较适合微博这种大粉丝量的场景。
    展开
    共 1 条评论
    1
  • 静心
    2020-11-07
    请教老师:select in 2000个用户,会不会有数据库查询的性能瓶颈?在毫秒级能查出来数据吗?用的是什么数据库?
    共 1 条评论
    1
  • rain
    2020-07-16
    如果关注了2000个用户,并行读redis会造成redis阻塞吧?
    1
  • X
    2020-07-14
    做过类似的系统,一开始用推的模式,用户每发布一条内容,就会全量写入到所有粉丝对应的timeline表中,这里用的是nosql,后来引入了大V的概念,如把粉丝数超过1W的即认定为大V,大V发布的内容不同步到粉丝的timeline表中,这样用户在刷信息流的时候就需要采用推拉结合的模式,即将timeline表中的数据与该用户关注的所有大V的发布内容全部查出来后做聚合,这样应该比单纯的拉模式的聚合成本要低。
    1
  • rumly
    2020-07-12
    文中提到:“我们对用户的浏览行为做了分析,发现 97% 的用户都是在浏览最近 5 天之内的微博,也就是说,用户很少翻看五天之前的微博内容,所以我们只缓存了每个用户最近 5 天发布的微博 ID”;那么对于另外的3%的用户该如何处理?走数据库查询吗? 另外,如果用户关注的人最近5天的更新只有1条,那么用户的首页如何展示?感谢。。。
    1
  • zenk
    2020-03-25
    干货,深感老师功力深厚

    作者回复: 谢谢🙏

    1
  • kamida
    2020-03-22
    redis在集群模式下好像是不支持多个key的 所以如果我们要找九十个用户的最近微博 是不是得一个用户一个用户的从redis查啊
    共 1 条评论
    2
  • guyun
    2020-03-21
    “在拉模式下只保留了发件箱,微博数据不再需要复制,成本也就随之降低了。”如果谢娜发送了一条微博,1.2亿粉丝中有2千万是活跃粉丝,其中3百万正在刷feed,单个node肯定扛不住这么高的读请求(因为是同一个热key)。那么只能用多个副本来解决分担了。但是假设一个副本能扛住10万读请求,那么岂不是要30个副本来抗?而且一个node上可能有多个热key存在。望老师赐教。

    作者回复: 其实并发不会到单个节点300万,一般整体核心缓存几十万的qps就差不多了

    1
  • Li Yao
    2020-03-09
    为什么必须用缓存副本,而不是水平扩容主缓存的方式来缓解网卡压力呢?

    作者回复: 扩容主缓存会造成命中率下降,并且同一组缓存节点数不能过多

    共 3 条评论
    1
  • 南山
    2020-01-28
    春节把整个专栏理了一边,收获颇多,非常喜欢老师的实际案例的分享,结合理论,每篇都都值得细读借鉴。 后续希望有机会应用于实际工作!

    作者回复: 谢谢~

    2
  • 黄海峰
    2019-12-25
    这个专栏真的篇篇干货,每篇都能让我理通一些以前似懂非懂的疑惑,忍不住留言感恩一下

    作者回复: 谢谢,有帮助就好~

    1
  • 哆啦C梦丶cium
    2022-03-11
    老师你好,请问下,用户上了微博后首次会拉取发件箱微博数据,后面如果用户关注的人刚好发了微博,用户这边是如何实时感知的并展示的?是定时轮训后端接口嘛?
  • null
    2020-11-27
    原文: 最后,一个用户被从活跃粉丝列表中剔除,或者是他从不活跃变成了活跃后,由于他不在大 V 用户的活跃粉丝列表中,所以也就不会收到微博的实时推送,因此,我们需要异步地把大 V 用户最近发布的微博插入到他的收件箱中,保证他的信息流数据的完整性。 这一段是不是2两个表述不对的地方: 1. 他从不活跃变成了活跃后 从活跃变为不活跃? 2. 由于他不在大 V 用户的活跃粉丝列表中,所以也就不会收到微博的实时推送,因此,我们需要异步地把大 V 用户最近发布的微博插入到他的收件箱中,保证他的信息流数据的完整性。 写入收件箱,不就是推模式么?推模式是针对活跃用户的吧?非活跃粉丝,主动去 outbox 拉取并聚合消息。 还是说上述2点是我理解错了,谢谢!
    展开
    共 1 条评论
  • null
    2020-11-27
    文章开始时,所用用户共用一个 outbox: select feedId from outbox where userId in (select userId from follower where fanId = "A") order by create_time desc 而之后介绍却是所有用户不再共用 outbox 表: for(Long id : uids) { ids.add(getOutboxByUid(id)); //获取发件箱的内容id列表 } 这是因为消息变多后,outbox 按 uid 分库分表了么?谢谢老师。
    展开
    共 1 条评论