(15.1.23)Feed流设计

一、Feed流系统特点

Feed流本质上是一个数据流,是将 “N个发布者的信息单元” 通过 “关注关系” 传送给 “M个接收者”。

在这里插入图片描述

  • 有一方是消息生产者。
  • 有一方是消息消费者。
  • 生产者产生的一条消息可能会被一个或多个消费者消费。
  • 消费者需要聚合来自多个生产者的消息在一个页面展现。

1.1 特点

  • 多账号内容流:Feed流系统中肯定会存在成千上万的账号,账号之间可以关注,取关,加好友和拉黑等操作。只要满足这一条,那么就可以当做Feed流系统来设计。
  • 非稳定的账号关系:由于存在关注,取关等操作,所以系统中的用户之间的关系就会一直在变化,是一种非稳定的状态。
  • 读写比例100:1:读写严重不平衡,读多写少,一般读写比例在10:1,甚至100:1以上。
  • 消息必达性要求高:比如发送了一条朋友圈后,结果部分朋友看到了,部分朋友没看到,如果偏偏女朋友没看到,那么可能会产生很严重的感情矛盾,后果很严重。

1.2 Feed流中的数据

Feed流系统是一个数据流系统,所以我们核心要看数据。

从数据层面看,数据分为三类,分别是:

  1. 发布者的数据:发布者产生数据,然后数据需要按照发布者组织,需要根据发布者查到所有数据,比如微博的个人页面、朋友圈的个人相册等。
  2. 关注关系:系统中个体间的关系,微博中是关注,是单向流,朋友圈是好友,是双向流。不管是单向还是双向,当发布者发布一条信息时,该条信息的流动永远是单向的。
  3. 接收者的数据:从不同发布者那里获取到的数据,然后通过某种顺序(一般为时间)组织在一起,比如微博的首页、朋友圈首页等。这些数据具有时间热度属性,越新的数据越有价值,越新的数据就要排在最前面。

针对这三类数据,我们可以有如下定义:

  1. 存储库:存储发布者的数据,永久保存。
  2. 关注表:用户关系表,永久保存。
  3. 同步库:存储接收者的时间热度数据,只需要保留最近一段时间的数据即可。

设计Feed流系统时最核心的是确定清楚产品层面的定义,需要考虑的因素包括:

  1. 产品用户规模:用户规模在十万、千万、十亿级时,设计难度和侧重点会不同。
  2. 关注关系(单向、双写):如果是双向,那么就不会有大V,否则会有大V存在。

上述是选择数据存储系统最核心的几个考虑点,除此之外,还有一些需要考虑的:

  1. 如何实现Meta和Feed内容搜索?
    • 虽然Feed流系统本身可以不需要搜索,但是一个Feed流产品必须要有搜索,否则信息发现难度会加大,用户留存率会大幅下降。
  2. Feed流的顺序是时间还是其他分数,比如个人的喜好程度?
    • 双向关系时由于关系很紧密,一定是按时间排序,就算一个关系很紧密的人发了一条空消息或者低价值消息,那我们也会需要关注了解的。
    • 单向关系时,那么可能就会存在大V,大V的粉丝数量理论极限就是整个系统的用户数,有一些产品会让所有用户都默认关注产品负责人,这种产品中,该负责人就是最大的大V,粉丝数就是用户规模。

接下来,我们看看整个Feed流系统如何设计

二、Feed流系统设计

2.1 产品定义

类型关注关系是否有大V时效性排序
微博类单向秒~分时间
抖音类单向无or有秒~分推荐
朋友圈类双向时间
私信类双向时间

上述对比中,只对比各类产品最核心、或者最根本特点,其他次要的不考虑。比如微博中互相关注后就是双向关注了,但是这个不是微博的立命之本,只是补充,无法撼动根本。

  • 关注关系是单向还是双向:

    • 如果是单向,那么可能就会存在大V效应,同时时效性可以低一些,比如到分钟级别;
    • 如果是双向,那就是好友,好友的数量有限,那么就不会有大V,因为每个人的精力有限,他不可能主动加几千万的好友,这时候因为关系更精密,时效性要求会更高,需要都秒级别。
  • 排序是时间还是推荐:

    • 用户对feed流最容易接受的就是时间,目前大部分都是时间。
    • 但是有一些场景,是从全网数据里面根据用户的喜好给用户推荐和用户喜好度最匹配的内容,这个时候就需要用推荐了,这种情况一般也会省略掉关注了,相对于关注了全网所有用户,比如抖音、头条等。
  • 最大并发

    • 确定了产品类型后,还需要继续确定的是系统设计目标:需要支持的最大用户数是多少?十万、百万、千万还是亿?

2.2 存储库

不管是哪种同步模式,在存储上都是一样的,我们定义用户消息的存储为存储库。

存储库主要满足三个需求:

  1. 可靠存储用户发送的消息,不能丢失。否则就找不到自己曾经发布到朋友圈状态了。
  2. 读取某个人发布过的所有消息,比如个人主页等。
  3. 数据永久保存。
特点分布式NoSQL关系型数据库(分库分表)
可靠性极高
水平扩展能力线性需要改造
水平扩展速度毫秒
常见系统Tablestore、BigtableMySQL、PostgreSQL

如果使用Tablestore,那么存储库表设计结构如下:

主键列第一列主键第二列主键属性列属性列
列名user_idmessage_idcontentother
解释消息发送者用户ID消息顺序ID,可以使用timestamp。内容其他内容

2.3 同步方式和同步库

2.3.1 推模式(也叫写扩散)

和名字一样,就是一种推的方式,发送者发送了一个消息后,立即将这个消息推送给接收者,但是接收者此时不一定在线,那么就需要有一个地方存储这个数据,这个存储的地方我们称为:同步库。

推模式也叫写扩散的原因是,一个消息需要发送个多个粉丝,那么这条消息就会复制多份,写放大,所以也叫写扩散

这种模式下,对同步库的要求就是写入能力极强和稳定。读取的时候因为消息已经发到接收者的收件箱了,只需要读一次自己的收件箱即可,读请求的量极小,所以对读的QPS需求不大。

推模式要远远比拉模式更好一些,但是也有一个副作用:

  • 数据会极大膨胀。

归纳下,推模式中对同步库的要求只有一个:写入能力强。

2.3.2 拉模式(也叫读扩散)

这种是一种拉的方式,发送者发送了一条消息后,这条消息不会立即推送给粉丝,而是写入自己的发件箱,当粉丝上线后再去自己关注者的发件箱里面去读取

一条消息的写入只有一次,但是读取最多会和粉丝数一样,读会放大,所以也叫读扩散。

拉模式的读写比例刚好和写扩散相反,那么对系统的要求是:读取能力强。

另外这里还有一个误区,很多人在最开始设计feed流系统时,首先想到的是拉模式,因为这种和用户的使用体感是一样的,但是在系统设计上这种方式有不少痛点,最大的是每个粉丝需要记录自己上次读到了关注者的哪条消息,如果有1000个关注者,那么这个人需要记录1000个位置信息,这个量和关注量成正比的,远比用户数要大的多,这里要特别注意,虽然在产品前期数据量少的时候这种方式可以应付,但是量大了后就会事倍功半,得不偿失,切记切记。

2.3.3 推拉结合模式

推模式在单向关系中,因为存在大V,那么一条消息可能会扩散几百万次,但是这些用户中可能有一半多是僵尸,永远不会上线,那么就存在资源浪费。

而拉模式下,在系统架构上会很复杂,同时需要记录的位置信息是天量,不好解决,尤其是用户量多了后会成为第一个故障点。

基于此,所以有了推拉结合模式,大部分用户的消息都是写扩散,只有大V是读扩散,这样既控制了资源浪费,又减少了系统设计复杂度。但是整体设计复杂度还是要比推模式复杂。

2.3.4 总结 和 同步库设计

  • 如果产品中是双向关系,那么就采用推模式。
  • 如果产品中是单向关系,且用户数少于1000万,那么也采用推模式,足够了。
  • 如果产品是单向关系,单用户数大于1000万,那么采用推拉结合模式,这时候可以从推模式演进过来,不需要额外重新推翻重做。
  • 永远不要只用拉模式。
  • 如果是一个初创企业,先用推模式,快速把系统设计出来,然后让产品去验证、迭代,等客户数大幅上涨到1000万后,再考虑升级为推拉集合模式。
  • 如果是按推荐排序,那么是另外的考虑了,架构会完全不一样,这个后面专门文章介绍。
    如果选择了Tablestore,那么同步库表设计结构如下:
主键列第一列主键第二列主键属性列属性列属性列
列名user_idsequence_idsender_idmessage_idother
解释消息接收者用户ID消息顺序ID,可以使用timestamp + send_user_id,也可以直接使用Tablestore的自增列。发送者的用户IDstore_table中的message_id列的值,也就是消息ID。通过sender_id和message_id可以到store_table中查询到消息内容其他内容,同步库中不需要包括消息内

2.3.5 推拉模式实例

在这里插入图片描述

  • 存储
    • 个人页Timeline:这个是每个用户的发件箱,也就是自己的个人页页面。
    • 关注页Timeline:这个是每个用户的收件箱,也就是自己的关注页页面,内容都是自己关注人发布的消息。
    • 关注列表:保存账号关系,比如朋友圈中的好友关系;微博中的关注列表等。
    • 虚拟关注列表:这个主要用来个性化和广告。
  • 发布Feed流程
    Feed消息先进入一个队列服务。
    1. 先从关注列表中读取到自己的粉丝列表,以及判断自己是否是大V。
    2. 将自己的Feed消息写入个人页Timeline(发件箱)。如果是大V,写入流程到此就结束了。
    3. 如果是普通用户,还需要将自己的Feed消息写给自己的粉丝,如果有100个粉丝,那么就要写给100个用户,包括Feed ID 或者 Feed内容。
    4. 第三步和第四步可以合并在一起,使用BatchWriteRow接口一次性将多行数据写入TableStore。
    5. 发布Feed的流程到此结束
  • 读取Feed流流程
    1. 先去读取自己关注的大V列表
    2. 去读取自己的收件箱,只需要一个GetRange读取一个范围即可,范围起始位置是上次读取到的最新Feed的ID,结束位置可以使当前时间,也可以是MAX,建议是MAX值。由于之前使用了主键自增功能,所以这里可以使用GetRange读取。
    3. 如果有关注的大V,则再次并发读取每一个大V的发件箱,如果关注了10个大V,那么则需要10次访问。
    4. 合并2和3步的结果,然后按时间排序,返回给用户

2.3.6 大V&普通用户的推拉模式缺陷

比如某个大V突然发了一个很有话题性的Feed,那么就有可能导致整个Feed产品中的所有用户都没法读取新内容了,原因是这样的:

  • 大V发送Feed消息,写入自己的存储库中。
  • 读取大V,都使用拉模式。
    • 大V的活跃粉丝(用户群A)开始通过拉模式(架构图中读取的步骤3,简称读3)读取大V的新Feed
    • Feed内容太有话题性了,快速传播。
      • 未登录的大V粉丝(用户群B)开始登陆产品,登陆进去后自动刷新,再次通过读3步骤读取大V的Feed内容。
      • 非粉丝(用户群C)去大V的个人页Timeline里面去围观,再次需要读取大V个人的Timeline,同读3.

结果就是,平时正常流量只有用户群A,结果现在却是用户群A + 用户群B+ 用户群C,流量增加了好几倍,甚至几十倍,导致读3路径的服务模块被打到server busy或者机器资源被打满,导致读取大V的读3路径无法返回请求

如果Feed产品中的用户都有关注大V,那么基本上所有用户都会卡死在读取大V的读3路径上,然后就没法刷新了

设计的时候就需要重点关心下面两点:

  1. 单个模块的不可用,不应该阻止整个关键的读Feed流路径,如果大V的无法读取,但是普通用户的要能返回,等服务恢复后,再补齐大V的内容即可。
  2. 当模块无法承受这么大流量的时候,模块不应该完全不可服务,而应该能继续提供最大的服务能力,超过的拒绝掉。

解决方式:

  1. 完全使用推模式就可以彻底解决这个问题,但是会带来存储量增大,大V微博发送总时间增大,从发给第一个粉丝到发给最后一个粉丝可能要几分钟时间
  2. 不使用大V/普通用户的优化方式,使用活跃用户/非活跃用户的优化方式。
    • 活跃用户使用写扩散,非活跃用户使用读扩散
    • 这样的话,就能把用户群A和部分用户群B分流到其他更分散的多个路径上去。而且,就算读3路径不可用,仍然对活跃用户无任何影响。

2.3.7 推拉模式的额外配置

2.3.7.1 排序型

潜在Feed内容非常多,用户无法全部看完,也不需要全部看完,那么需要为用户选出最热门的

在这里插入图片描述

  • 写流程基本一样
  • 读流程里面会先读取所有的Feed内容
    • 这个和Timeline也一样,Timeline里面的话,这里会直接返回给用户
    • 但是Rank类型需要在一个排序模块里面,按照某个属性值排序,然后将所有结果存入一个timeline cache中,并返回分数最高的N个结果,下次读取的时候再返回[N+1, 2N]的结果。
2.3.7.2 个性化推荐型

在这里插入图片描述

  • 写流程也和Timeline一样。
  • 每个用户有两个收件箱:
    • 一个是关注页Timeline,保存原始的Feed内容,用户无法直接查看这个收件箱。
    • 一个是rank timeline,保存为用户精选的Feed内容,用户直接查看这个收件箱。
  • 写流程结束后还有一个数据处理的流程。
    • 个性化排序系统从原始Feed收件箱中获取到新的Feed 内容,按照用户的特征,Feed的特征计算出一个分数,每个Feed在不同用户的Timeline中可能分数不一样的,计算完成后再排序然后写入最终的rank timeline。

2.4 其他数据

Feed流系统中的元数据主要包括:

  • 用户详情和列表。
  • 关注或好友关系。
  • 推送session池。

2.4.1 用户详情和列表

主键顺序第一列主键属性列-1属性列-2
字段名user_idnick_namegenderother
备注主键列,用于唯一确定一个用户用户昵称,用户自定义属性用户性别,用户自定义属性其他属性,包括用户自定义属性列和系统附加属性列。Tablestore是FreeSchema类型的,可以随时在任何一行增加新列而不影响原有数据。

2.4.2 关注或好友关系

主键顺序第一列主键第一列主键属性列属性列
Table字段名user_idfollow_user_idtimestampother
备注用户ID粉丝用户ID关注时间其他属性列

2.4.3 推送session池

  • 思考一个问题,发送者将消息发送后,接收者如何知道自己有新消息来了?
    • 客户端周期性去刷新?
    • 如果是这样子,那么系统的读请求压力会随着客户端增长而增长,这时候就会有一个风险
    • 比如平时的设备在线率是20%~30%,突然某天平台爆发了一个热点消息,大量休眠设备登陆,这个时候就会出现“查询风暴”,一下子就把系统打垮了,所有的用户都不能用了。

解决这个问题的一个思路是

  1. 在服务端维护一个推送session池,这个里面记录哪些用户在线
  2. 然后当用户A发送了一条消息给用户B后,服务端在写入存储库和同步库后
  3. 再通知一下session池中的用户B的session,告诉他:你有新消息了。
  4. 然后session-B再去读消息,然后有消息后将消息推送给客户端。或者有消息后给客户端推送一下有消息了,客户端再去拉。

这个session池使用在同步中,但是本质还是一个元数据,一般只需要存在于内存中即可,但是考虑到failover情况,那就需要持久化,这部分数据由于只需要指定单Key查询,用分布式NoSQL或关系型数据库都可以,一般复用当前的系统即可。

主键列顺序第一列主键第二列主键属性列
列名user_iddevice_idlast_sequence_id
备注接收者用户ID设备ID,同一个用户可能会有多个设备,不同设备的读取位置可能不一致,所以这里需要一个设备ID。如果不需要支持多终端,则这一列可以省略。该接收者已经推送给客户端的最新的顺序ID

2.4.5 评论 || 赞

评论的属性和存储库差不多,但是多了一层关系:被评论的消息

所以只要将评论按照被被评论消息分组组织即可,然后查询时也是一个范围查询就行

|主键列顺序| 第一列主键| 第二列主键| 属性列 |属性列| 属性列|
|字段名 |message_id |comment_id |comment_content| reply_to| other|
|备注| 微博ID或朋友圈ID等消息的ID |这一条评论的ID |评论内容| 回复给哪个用户 |其他|

2.5 操作

2.5.1 删除Feed内容

在Feed流应用中有一个问题,就是如果用户删除了之前发表的内容,系统该如何处理?因为系统里面有写扩散,那么删除的时候是不是也要写扩散一遍?这样的话,删除就不及时了,很难应对法律法规要求的快速删除。

针对这个问题,我们在之前设计的时候,同步表中只有消息ID,没有消息内容,在用户读取的时候需要到存储库中去读消息内容,那么我们可以直接删除存储库中的这一条消息,这样用户读取的时候使用消息ID是读不到数据的,也就相当于删除的内容,而且删除速度会很快。

除了直接删除外,另外一种办法是逻辑删除,对于删除的feed内容,只做标记,当查询到带有标记的数据时就认为删除了

2.5.2 更新Feed内容

更新和删除Feed处理逻辑一样,如果使用了支持多版本的存储系统,比如Tablestore,那么也可以支持编辑版本,和现在的微博一样

三、分析

3.1 IM

每个用户只有一个同步库Timeline,就算用户A在10个群里面,那么这个10个群的同步消息都是发送给用户A的这一个同步库Timeline中。
用户A的这一个同步库Timeline收到信息后,根据信息转化为对应的会话中显示

每次信息,不仅写入存储库,还要写入自己的同步库和别人的同步库

  • AB单聊就是三个Timeline:

    • 每个单聊对应一个会话Timeline(存储历史消息)
    • 用户A的收件箱(A的同步库Timeline)
    • 用户B的收件箱(B的同步库Timeline)
  • ABC…N群聊就是1 + N个Timeline:

    • 每个群聊对应一个会话Timeline(存储历史消息)
    • 用户A的收件箱(A的同步库Timeline)
    • 。。。。。。
    • 用户N的收件箱(N的同步库Timeline)
      在这里插入图片描述
  • ABC三个人,所以一定是三个同步Timeline

  • 三个会话,所以肯定是三个 存储Timeline

3.1.1 消息存储表–每个会话一个

  • timelineId 会话id
  • type 会话类型
  • sequenceId 会话中的消息ID, 全量自增
  • sendtime
  • sender
  • content
    在这里插入图片描述
  1. 通过timelineId获取该会话的Queue实例,然后调用Queue的scan接口与ScanParam参数(sequenceId范围+倒序)拉取最新的一页消息。
  2. 当用户向上滚动,展示完这一页消息后,客户端会基于第一次请求的最小sequencId发起第二次请求,获取第二页消息记录
3.1.2 消息同步表—每个人一个
  • 基本和上述表结构类似
  • 只记录和自己相关会话的信息数据
  • 记录端侧最后一次同步的信息id

3.1.3 会话元数据

  • 会话类别(群、单聊、公众号等)
  • 群名称
  • 公告
  • 创建时间
    在这里插入图片描述

3.2 朋友圈

朋友圈是一种典型的Feed流系统,关系是双写关系,关系有上限,排序按照时间

如果有个人持续产生垃圾内容,那就只能屏蔽掉TA

这一种类型就是典型的写扩散模型。

  • 发一条朋友圈状态就是1 + N个Timeline:
    • 自己历史消息Timeline(自己的存储库Timeline)
    • 朋友A的收件箱(A的同步库Timeline)
    • 。。。。。。
    • 朋友N的收件箱(N的同步库Timeline)

3.3 微博

微博也是一种非常典型的Feed流系统,但不同于朋友圈,关系是单向的,那么也就会产生大V,这个时候就需要读写扩散模式,用读扩散解决大V问题。同时,微博也是主动关注类型的产品,所以排序也只能是时间,如果按照推荐排序,那么效果就会比较差。

  • 大V发一条微博就是 1 + M个Timeline(M << N,N是粉丝数)。
    • 自己历史消息Timeline(自己的存储库Timeline)
    • 粉丝A的收件箱(A的同步库Timeline)
    • 。。。。。。
    • 粉丝M的收件箱(M的同步库Timeline)
    • 剩余N - M的粉丝直接读大V自己的存储库Timeline内容即可,那么怎么设置M和N - M,可以参考《如何打造千万级Feed流系统》这篇文章

3.4 头条

头条是最近几年快速崛起的一款应用,在原有微博的Feed流系统上产生了进化,用户不需要主动关注其他人,只要初始浏览一些内容后,系统就会自动判断出你的喜好,然后后面再根据你的喜好给你推荐你可能会喜好的内容,训练时间长了后,推送的内容都会是你最喜欢看的。

3.5 私信

私信也算是一种简单的Feed流系统,或者也可以认为是一种变相的IM,都是单对单的,没有群。我们后面也会有一篇文章《私信类系统架构设计》中做详细介绍。

参考文献

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值