动态业务架构演进
本期作者
金斌武
哔哩哔哩资深开发工程师
一、业务概述
哔哩哔哩动态是哔哩哔哩平台上的一项社交功能,可以让用户分享自己的生活、兴趣、观点等内容,并与其他用户进行互动和交流。用户可以在动态中发布文字、图片、视频等形式的内容,也可以分享自己的观看记录、点评、收藏等。
除了普通用户,哔哩哔哩的视频创作者(也被称为UP主)也会通过动态与粉丝互动。他们可以发布新视频的预告、幕后花絮、直播间链接等内容,也可以回答粉丝的问题、分享自己的创作经验等。
此外,哔哩哔哩动态还支持一种叫做“动态视频”的功能。用户可以录制一段15秒以内的视频,发布到自己的动态中,让其他用户更直观地了解自己的状态和想法。
总的来说,哔哩哔哩动态和直播功能为用户提供了更加多元化的内容和社交体验,让用户可以更加丰富地在平台上表达自己的想法和兴趣,并与其他用户、视频创作者进行互动和交流。
业务特点
流量池子大:日均综合页pv 十亿级以上
功能逻辑多:动态卡片类型现存近30种,要维护动态,必须先弄懂相关的业务逻辑
覆盖业务广:据不完全统计,非动态业务对接约20+。业务间经常互相联动,业务功能需要耦合,所以很多业务开发工作聚焦在如何在这种网状联动的业务复杂性造成的软件复杂性中,寻求业务满意,开发愿意持续维护的方案
导流效果强:稿件&图文&直播 三大内容金刚里,日均近亿次点击,发生在动态场景下
迭代时间长:2017.9月上线,动态出现在B站,历时已5年有余
到这里,围绕动态业务特点,会有一些问题,基于当时的业务思考提出了一些办法,这也引出了今天的话题——动态业务架构的演进。希望能回答下述问题
动态架构演进的路径是什么?
每一次关键节点的业务思考有哪些?
二、架构概述
图2.1 内容工作流角度
如图2.1所示,总体看,业务范围按照内容生产流程描述,分为4个流程,内容产出、内容预处理、内容持久化、形成索引。覆盖内容收录、编辑、审核、投稿创作端流程,以及投后数据管理、社区氛围管控等流程。
图2.2 B站内业务分层
如图2.2所示,该四层架构是现在微服务架构设计中的典型分层架构。动态网关是动态重构时引入的bff层服务,动态负责提供元数据、列表数据、部分内容数据、运营数据等,动态网关负责列表渲染、内容布局、格式控制等,将展示逻辑从原架构抽离,提供统一API适配多端。
动机:原C/S架构无法很好的为哔哩哔哩移动客户端、web/H5、pc客户端、ott端提供优质体验,每一种用户端均存在不同程度的定制化诉求,而动态服务端本身服务较多,且逻辑复杂,提供的数据很难满足每一种前端的需要,开发成本居高不下。
图2.3 动态服务端内部模块分层
如图2.3所示,动态内部为达到尽可能复用,会有数据抽象层,类似目前领域概念,将相近的业务领域内容集中在一个或几个服务中。
存储设计
缓存列表
用户列表缓存
缓存定长设计,只存储动态ID(dynID)
dyn_max:cache中的up_bound 哨兵,空哨兵,防止缓存穿透; 列表只要有缓存数据,均有此缓存标记。
dyn_zero:cache中的low_bound 哨兵,如果存在,表示数据库所有数据已在缓存中
feed流具有很强的时效性,用户不断地发状态,且大多数用户只消费最新的一页动态,绝大多数用户消费不超过20页(约400条feed),缓存只存储最新的若干条,一定程度上达到了冷热数据隔离的效果,保证用户体验的同时,降低成本。
小红点缓存,业务形态上展示最多99条,缓存即全量数据内容,无需回源补充。
持久层数据库
账号、用户行为均由平台方存储
索引和内容分离
动态只存储动态的内容数据,关系型数据库+分库分表
多写场景使用KV
原因:
模块、服务独立,数据维度隔离
数据增量、访问量、更新量均有差异,qps不均衡
易于做冷热分离
三、架构演进
feed流架构
feed消息特点
数据量大,而且在Feed流系统里面很多时候都会选择写扩散(推模式)模式,这时候数据量会再膨胀几个数量级,所以这里的数据量很容易达到100TB,甚至PB级别。
数据格式简单
数据不能丢失,可靠性要求高
自增主键功能,保证个人发的Feed的消息ID在个人发件箱中都是严格递增的,这样读取时只需要一个范围读取即可。由于个人发布的Feed并发度很低,这里用时间戳也能满足基本需求,但是当应用层队列堵塞,网络延迟变大或时间回退时,用时间戳还是无法保证严格递增。
数据量大,所以成本越低越好
推拉模式对比
在讨论feed流架构时,先来看下推模式(push)和拉模式(pull)之间的对比,我们需要从不同的角度来进行比较。关于读写比(读取和写入操作的频率),这两种模式有明显的差异。
这里是一个对比推模式和拉模式的概述:
推模式(push)
在推模式中,数据的生成者(例如,服务器、设备或服务)主动将数据推送给数据消费者(例如,客户端、用户或其他服务)。这种模式主要用于实时数据处理,提供了较低的延迟和实时性。
读写比:推模式通常适用于写入操作比读取操作更频繁的场景,因为数据生成者可以在有新数据时立即将其推送给消费者。这有助于减少对数据的重复请求,提高系统的效率。
拉模式(pull)
在拉模式中,数据消费者主动请求数据生成者提供数据。数据生成者只在收到请求时才提供数据。这种模式适用于需要按需获取数据的场景。
读写比:拉模式适合读取操作比写入操作更频繁的场景,因为数据消费者可以根据需要请求数据。这种模式可能导致重复请求相同数据,从而降低系统效率,但它可以更好地适应变化的数据需求。
以下是推模式和拉模式的一些优缺点:
推模式优缺点:
优点:
实时性:推模式可以在数据生成后立即将其推送给消费者,实现低延迟。
节省带宽:数据生成者仅在有新数据时发送数据,减少了重复请求。
缺点:
可能导致消费者无法处理数据:如果消费者处理速度跟不上生成速度,可能导致数据堆积。
可能导致网络拥塞:大量数据生成者同时推送数据可能导致网络拥塞。
拉模式优缺点:
优点:
按需获取:消费者可以根据需要请求数据,更灵活地适应变化的需求。
降低服务器压力:数据生成者只在收到请求时提供数据,降低了服务器处理负担。
缺点:
延迟较高:拉模式需要消费者主动请求数据,可能导致较高的延迟。
重复请求:如果消费者频繁请求相同数据,可能导致带宽浪费和系统效率降低。
演进过程
阶段一:增量拉,更新内容同步存入收件箱
最开始动态基于拉模式实现,每次获取N个关注关系,然后获取其outbox内容存在较严重的读放大问题。
一个朴素的想法,为什么不把每次获取的内容做一个快照呢?
基于这个想法,实现了inbox的最大动态ID作为下限,同步拉取关注列表发件箱的更新,并同步写入inbox,用作快照,下一次拉取则可以避免重复拉取outbox,降低outbox压力。
随着流量的快速增长,这个方案很快出现了一个新的问题,同步写入inbox产生了瓶颈
当用户长时间未登录,关注的up主更新超过500条,因为同步更新500条,会导致inbox会有断层
读接口中耦合了写接口
增量拉,异步构建inbox:
以inbox_max_dyn为动态下限,将此次大于inbox_max_dyn的动态ID列表投递kafka,以inbox_max_dyn_id为基准为增量写入 >max 的所有关注人的更新动态,从旧到新。执行一段时间后,我们发现仍存在一些问题,比较棘手。
异常不好恢复,历史多次inbox出现问题,导致用户丢失动态,且不易恢复
消息投递失败,会造成动态丢失
对用户outbox造成了额外压力,异步构建inbox时,拉取更新写入
Inbox必须存储全量动态,包括临时不可见的动态(因为有可能管理后台恢复该index可见性)。若连续100条或更多不可见动态,会显示拉到底,无法翻页,导致动态获取断层
主要问题还是断层问题,如下图所示
业务场景需求专栏、视频页等单独列表,而非全类型动态feed流时,动态内容在每个用户的feed流分布不均,专栏和番剧视频显著少,会导致有限的几次翻页无内容,也就是断层。
解决方法:对特殊类型的动态做了分类型缓存索引,额外的空间成本,换取用户体验提升与响应时长缩减,解决该问题
增量更新存续期间,经常发生丢失动态的情况,关注up的更新我看不到,且重试无法恢复,代码的patch补丁也越来越多,我们开始觉得,这个方式可能不是一个好的分发方案。显著问题如下:
数据库压力:降低outbox压力与事实不符,实际上增量更新带来的outbox数据库请求流量占到一半
不能容错:数据不可通过重试自动回复,需要通过对账,人工恢复等高成本方式恢复,无法自愈。假如出现故障,无法随着故障恢复而恢复,需要人工介入修数据。
inbox KV:因为高写入要求,组里基于levelDB研发KV数据存储,后续随着人员流动,无人维护,问题较多,导致维护压力大。
我们提出,弃用目前的增量更新模式,转向业界更通用的迭代路径,采用推拉结合模式。但推拉模式有两个前置条件:1. 全量粉丝列表 2. 定制化泰山kv存储。因其有一段开发时间,过渡阶段采用纯拉模式,缓解当前的人力黑洞。
*泰山kv存储:B站内部自研kv存储系统
阶段二:纯拉模式
动态上限=首页?max_int64:上一页页尾动态ID
用户根据最大上限向下获取一页动态,根据n个UP的一页,merge成用户的一页。
由于纯拉模式会随着平均关注数增加同比放大读扩散的幅度,动态做了首页红点缓存隔离,可降级 。
阶段三:推拉结合
关系链全量粉丝列表完成后,泰山改造部署也相继完成,推拉结合提上日程。基于纯拉模式,我们要做的就是新增推模式,达成推拉结合。
为什么要引入推拉结合,或者说纯拉模式存在什么问题?
在纯拉模式下,随着用户平均关注数上升,读扩散被放大,发件箱的压力日益增大,转而采用推拉结合的方式获取feed流,用收件箱inbox分担发件箱outbox的压力,一定程度上解决长尾问题。
推拉结合的方向选择
大UP/小UP划分:大UP的内容采用拉模式,小UP采用推模式
优势:实现简单,解决长尾问题,有效降低内存占用
缺陷:大UP可能存在热点问题,过于集中,会遇到缓存瓶颈
活跃/非活跃用户划分:活跃用户推模式,非活跃用户拉模式
优势:热点问题,不影响活跃用户
缺陷:实现过于复杂,活跃/非活跃状态判定经常变换,需要补偿数据
最终,我们选择了大UP/小UP的方式,热点问题可以使用本地cache多级缓存的方式有效支撑,通过主动检测热点的方式,也可缓冲大UP压力。
设计上,分为几个模块
列表合并:大小UP分别获取发件箱收件箱,组成一页动态
收件箱inbox:用户收件箱存储,存储UP主动发布的内容索引
推模块:UP发布内容后,执行推送内容ID至粉丝收件箱的任务模块
关系链补偿模块:用户关注/取关行为,对inbox的内容做相应改变
如上图所示是动态feed流发布流程示意图,用户通过动态发布服务发布一条动态,并进入其发件箱,推送服务job通过订阅binlog识别是否为小UP,然后通过查询创作者粉丝列表,将该发布动态写入其粉丝收件箱,达到推的效果。推送的时机在用户发布内容时,失败重试即可恢复inbox,无增量拉取做快照的方案导致的feed流断层问题,维护成本和服务稳定性均有提升。
具体设计上,
为尽量减少inbox断层,inbox定义为存储仅用户主动发布且处于可见状态的内容
为避免相对大的UP发布造成堵塞的问题,基于粉丝分布情况,做了多级队列,并对于粉丝列表接口的使用做了并发处理加速
多机房,topic隔离,各自写入对应机房kv
图:发件箱推模式,多级队列示意图
如上图所示,列表服务,根据关注列表与大UP配置,区分大小UP,大UP直接读outbox发件箱,小UP读取收件箱,根据dynID合并成一页。
具体流程如下图所示,以刷新动态为例
其中,由于每次刷新,要同时返回xx条更新的toast,所以在拉取中,尽量保证一页(20条)为精确的更新,剩余动态ID列表只用于计数。
上图展示了关系链补偿模块的流程,红色为调用outbox,绿色为调用inbox。这是为了实现当用户新关注一个小UP,其近期发布的内容并没有加入到inbox,为用户获得更好体验,
当我们实现了推拉结合之后,后续又做了两点优化:
为更好的应对本地热点问题及redis集群不能快速扩容的限制,我们做了outbox本地缓存优化
为应对inbox及推送模块异常,保证小UP的内容仍能被分发,我们做了推拉结合自动降级为纯拉模式的实现
四、未来发展
动态feed流
从平台维护人员看,随着流量增长、关系链上限的提升、僵尸号、非活跃账号占比的提升
推模式的存储成本会变得逐渐显眼
基于用户画像的兴趣推荐可能是一个角度
从用户角度看,目前主要问题有
大UP成本低,经常批量发低质内容,如pdd广告,造成feed流一页可能都是pdd大促
基于兴趣消费的体验不是很好。可能因为某个视频,基于兴趣关注某个UP,但该UP平常发的内容并不与兴趣相关
内容平台化,私域到生态
目前,动态架构随着图文项目等建设,关于图文内容,我们已经形成了展示(详情页统一)、收录/创建(发布器统一)、存储(ID统一)的图文内容平台化能力。
主要问题仍是平台化能力不完善,技术包袱重两方面。
后续将
继续完善图文平台化能力,做更好的图文平台,为业务增长打好基础
下线旧功能并做服务治理,使旧项目搭上现在的基建快车,能更稳定、更高效运行
业务场景扩展,图文能力扩展到更多图文场景中
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路