背景
为什么要做个性化推荐
回顾弹幕工程建设的发展历程,大致可以分几个阶段:
1. 基础能力:在高并发、热点场景下,保证弹幕服务的稳定和高可用
2. 负向治理:以管控为目标,通过删除、自见、打薄等手段过滤低质弹幕内容
3. 正向推荐:以优化视频消费体验为目标,筛选优质弹幕内容上屏展示
这三个阶段的目标始终在并行推进,在1和2的基本问题完成之后,作为B站的特色功能和社区文化重要载体,弹幕业务在承接基础功能之外也需要持续探索优化消费体验的能力。这也就必须在稿件维度优选的基础上通过用户特征交互获得更大的策略空间,建设千人千面的弹幕推荐能力。
这个过程离不开工程、算法、产品等多个团队的倾力合作,本文会以工程团队的视角重点介绍工程架构对推荐系统的能力支持。
第一阶段:基于原架构构建推荐能力
这一阶段,我们基于原有架构搭了一个最简版的推荐能力:
发送弹幕时,工程系统通过数据库binlog同步给推荐系统。在展示侧,由推荐系统负责计算对应视频要展示的弹幕id列表,工程系统获取弹幕内容最终返回。这样就实现了一个具备基础能力的弹幕推荐系统。
1.1 遗留问题
这一阶段的线上表现还有很大的提升空间。主要原因是当时的弹幕系统依赖一个弹幕池的设计,基于视频的长度确定一个视频内可展示弹幕数的上限。当弹幕池满了,会按照时间倒序淘汰,留下最新的N条。
举个例子,一个15分钟长度的视频,弹幕池上限是6000条。这个设计会带来以下的问题:
1. 可展示弹幕少
由于弹幕池上限比较小并且分布不均匀,经常出现弹幕填不满屏幕的情况。
2. 弹幕质量差,优质历史弹幕无法召回
因为弹幕池整体较小,候选集有限,选出优质内容的空间较小。同时因为时间序淘汰,很多优质的弹幕内容无法沉淀下来。
3. 弹幕分布不均匀,长视频出现空屏
举个例子,很多长视频比如ogv,大量弹幕集中出现在开屏打卡。这部分重复内容会把整个视频的弹幕池挤掉,导致后面大段内容出现弹幕空屏。
这些问题,我们会在下一个阶段着重优化解决。
第二阶段:10倍扩大召回池
在前一个阶段,我们发现了基于原有弹幕池工程基建去搭建个性化推荐的问题。我们决定重新搭建一套专门服务于个性化推荐的工程系统。整体架构如下,后面会逐步展开介绍。
2.1 原弹幕系统
先简单介绍一下原弹幕系统的工程实现。弹幕的两个核心场景是,发弹幕时以弹幕维度的唯一id(弹幕id)作为主键落库,同时关联视频id;在读弹幕时,一次性读取一个视频6分钟分片的全部弹幕,以视频+分片维度读取。作为B站的核心功能,弹幕的读写场景都要随时应对高并发流量以及突发的热点场景。原有架构的存储系统主要用了一套三级缓存的结构:
第一级接口级缓存直接承接C端读流量。在第一级缓存miss之后,先通过第二级缓存获取当前视频分片的弹幕id列表,然后从第三级缓存获取获取具体的弹幕内容。在第二级缓存到第三级缓存之间存在弹幕数量的读放大,若当前视频的6分钟分片有1000条弹幕,就会产生1000倍的读放大。若第三级缓存发生miss,则会回源到底层存储TiDB。
这套架构存储相对复杂,因为弹幕池的存储强依赖于Memcached和Redis zset,在容量扩展上,很难支持个性化推荐的业务诉求。同时视频维度时间序淘汰这一固定逻辑也很难变更。这一阶段我们也思考过通过AI侧的弹幕质量粗排模型打分来代替时间序淘汰,但在现有架构下,整体数据刷新的复杂度、模型实验能力的支持、模型版本更新的数据回刷都成为瓶颈点。
2.2 个性化推荐弹幕系统
基于上述问题,我们决定重新搭建一套弹幕的个性化推荐系统。新系统在展示侧的核心链路不变,仍然是通过AI的精排接口获取弹幕ID,在内部存储系统中读取弹幕内容,渲染并返回。新系统的核心目标是最大限度扩大推荐系统的召回池容量,同时选用基于模型的召回策略并支持回刷。在实现上,核心思路是简化存储并提升计算能力,后面也会主要以存储和计算两方面进行介绍。
存储设计
新系统的职责边界是只满足读弹幕的个性化推荐这一个最核心场景,因而在存储上不需要存全量弹幕,也不需要支持复杂的业务逻辑和查询能力。基于这样的场景特点,我们放弃了关系查询支持,选择了B站自研的KV数据库泰山,将一个视频一分钟内经过召回的弹幕存在一个key里面。同时,单条弹幕也以弹幕id作为key进行存储,用于处理弹幕状态或其他元数据的更新。因为KV数据库不支持事务,我们自己通过redis分布式锁保证了并发场景下的数据一致性。受益于KV数据库强大的读写并发能力,在场景有读放大的情况下,我们仍然选择了不加缓存直连数据库的最简单存储结构。最终线上表现,DB完全可以承受百万QPS的并发访问量。
计算优化
原系统中,如果弹幕池存满是基于时间序倒序淘汰。在新系统中,我们选择视频每10秒钟的弹幕作为最小的策略单元,可支持10秒1000条弹幕的召回空间,按照AI策略提供的粗排模型打分进行淘汰。这样做的一个问题是,一旦粗排模型有逻辑更新,需要重新刷新召回池。为此,我们设计了全量回刷模块。AI通过离线Hive获取全量弹幕,打分并推送至工程侧,重新计算淘汰策略并进行存储。经过线上验证,更新百亿以上的弹幕数据大概需要2天左右。增量数据更新模块中,我们通过消息队列聚合的方式减轻数据库的写入压力,通过redis分布式锁保证召回池全量回刷期间增量数据也同时更新的数据一致性。
分片颗粒度选择
在个性化推荐的系统中,多处使用到了分片的概念,用来聚合单位视频长度下的弹幕内容。我们对分片的大小做了不同的选型:
在接口层,如果一次下发的分片过长会造成带宽浪费,比如下发10分钟的弹幕,用户看1分钟就退出了。但如果分片过小,又会因为请求轮询导致服务端QPS变大。这里我们一开始用了6分钟的分片大小,后来优化为服务端根据场景动态下发分片大小,从而节约带宽。
同样,在存储上我们主要的平衡点是读数据时的请求放大和数据更新时的颗粒度。在一个存储单元内部,我们以每10秒作为一个策略单元进行弹幕淘汰,主要为了满足策略目标,让弹幕足量且均匀。
双系统关系
线上维持两套系统的常见问题是职责边界和关系不清晰,导致在迭代中逐渐走向混乱。我们在设计阶段就对两套系统做了明确的职责划分和关系定义:原弹幕系统负责整体的弹幕业务逻辑,新系统(图中个性化推荐弹幕系统)只为个性化推荐这一个场景服务。在存储上,原系统以TiDB存储全量弹幕内容作为source of truth,新系统的KV数据库作为个性化推荐的召回池,是TiDB数据库的子集。同时,在弹幕消费这一主要场景下,原系统以线上热备的形式维护。在个性化推荐系统出现故障时,可以自动降级使用户侧无感。
2.3 核心收益
这套架构通过召回池存储容量的整体扩充,以及视频维度弹幕淘汰到视频每10秒维度淘汰的策略颗粒度提升,最直观的收益是弹幕召回池的上线扩充,是原来的10倍左右。假设视频是15分钟,弹幕上限就从整个视频6000条升级到每10秒1000条,曝光上涨30%,大大提升了消费体验。很好理解,1w选1000和10w选1000的最终质量是完全不一样的。
在稳定性上,我们通过策略降级、工程指标降级以及双系统保障等手段,从上线至今没有出现过全站事故。
当然,业务上最核心的收益是工作思路转变:个性化推荐提供了一个工作通路,可以通过弹幕持续优化视频的消费体验、社区互动氛围和UP主满意度。
第三阶段:工程架构和推荐系统深度结合
在上一个阶段,主要通过优化工程系统提升了个性化推荐的能力支持。经过一段时间的应用,我们发现系统中还存在一些问题,主要有:
1. 工程和AI的数据不对齐。线上经常出现,精排返回的弹幕id在工程系统的召回池中获取不到。经过排查我们发现,核心问题是工程和AI两边对召回和淘汰各自实现。工程侧的召回池通过上述弹幕池分片进行存储,AI侧在系统内部通过离线hdfs存储。虽然工程侧是依据AI的粗排模型进行召回并淘汰,但随着AI的召回策略迭代,两边的策略一致性和数据一致性都很难有效保证。
2. AI侧系统降级频繁。核心原因主要有两个方面,一是模型打分的存储架构不够合理,二是在精排阶段缺少实时退场能力。AI侧的模型索引通过hdfs文件系统存储,每次索引服务启动时热加载到内存中。显而易见的问题是启动效率低且不稳定,且难以支持水平扩展。同时在精排阶段没有做退场淘汰,遇到热点视频时经常出现响应超时甚至OOM。
3. 实验能力不足。当前的工程系统可以通过全量数据回刷的方式来对策略迭代做最基础的支持。但是B站全量弹幕在百亿以上,回刷一次需要2-3天完成,这显然无法支持快速策略迭代的业务诉求。
这三个表象问题都指向了工程侧和AI侧结合之后的系统问题,因此我们这一阶段的主要思路是以工程和AI结合的整体视角来对系统进行设计优化。
3.1 弹幕推荐的场景分析
首先我们重新调研一下弹幕推荐的场景特点,以及和B站的核心内容推荐场景,视频推荐的比较。在第一、二阶段,工程和算法系统的交互模式以参考视频推荐的主流方式为主:工程侧在生产(发送)内容时通过binlog订阅同步给算法侧,在展示场景下调用算法侧接口获取id列表,两边类似黑盒模式,各自进行迭代和优化。
我们比较一下两个场景的特点和工程要求:
视频推荐是动态推理,即将随着视频播放不断产生的交互数据用于模型更新。弹幕推荐是静态推理,即在弹幕发送时就计算好全部的模型分数,不随着线上数据进行变更。这一点上,视频推荐的推理策略复杂度更高,而弹幕推荐的场景下更适合通过批量任务的方式优化离线模型分数的计算和存储。同时,内容查询复杂度、数量级、QPS和响应时间几个方面弹幕系统都需要满足更高的要求。在整体流程上,都由召回、粗排、精排这3个主要步骤构成,在召回和粗排阶段完成模型计算,在精排阶段进行用户特征的实时交互,从而实现个性化。
3.2 设计目标&核心思路
根据这上述的场景特点,我们决定打破之前工程和算法互相黑盒的常规设计模式,在整体视角下调整架构设计,在模块级别通过职能进行分工。由工程团队负责高并发的数据读取,以及各类实时、离线任务的更新;算法团队专注负责计算策略,包括模型推理和精排。这样两个团队都可以发挥自己的优势,将整个系统做好。
首先介绍一些核心概念:
弹幕物料:指弹幕的基础数据,包括内容、状态、点赞、举报、属性标记等。
弹幕索引:指弹幕的全部模型打分,包含基线模型(即粗排模型)、点赞模型、举报模型、内容相关模型等,主要用于精排计算。目前每条弹幕大概有15-20组模型打分。
物料池:指一个视频分片(一分钟)内所有弹幕物料的集合。
索引池:和物料池类似,一个视频分片(一分钟)内所有弹幕索引的集合。
上图将第二阶段架构图中的AI系统进行了详细展开。AI系统主要由打分服务、索引服务、精排服务三块构成,其中打分服务和精排服务主要负责模型和策略,索引服务负责数据召回、粗排淘汰和数据读写。在数据上,用户和稿件特征可以复用公司成熟的平台能力,弹幕索引目前以自建hdfs+内存的方式存储。再结合上述遗留问题和场景分析,核心思路是将目前的系统瓶颈点,负责数据读写的索引服务以及对应的数据存储合并到工程系统中。主要设计目标如下:
1. 合并物料池、索引池:解决物料池、索引池各自进行淘汰导致的数据不对齐问题。
2. 精排优化:前置精排退场逻辑,解决热点场景下OOM导致精排不可用的问题。
3. 提升实验效率:提供小时级模型更新实验能力,支撑策略快速迭代。
3.3 详细设计
3.3.1 发送、展示链路简要流程
3.3.2 个性化推荐弹幕系统模块展开
3.3.3 实时数据存储优化
在索引池的合并上,因为读写场景一致,我们选择了和物料池同样的kv数据库(taishan)实现,也是在一个视频id+分钟作为key的单位下,对应物料池的粗排分排序淘汰进行存储。这里物料池和索引池用不同的key来存,没有使用在物料池的基础上扩展字段来实现,核心原因是:
1. 物料和主要业务数据库tidb、各业务场景下的弹幕模型保持一致,避免排序索引和业务功能的耦合。
2. 可实现索引、物料独立更新,降低索引回刷成本。
3. 分梯度降级保证可用性,一旦出现事故索引失效,可以随机打分降级保证C端无感。
同时,物料池、索引池两者的一致性通过redis分片锁保证,它的颗粒度是视频+分钟,和物料池、索引池key的颗粒度相同,从而在业务存在高并发,且线上增量数据和物料、索引回刷同时进行的情况下实现视频每分钟的弹幕原子性写入。在优先级上以物料池为主,写入索引前先验证物料存在,反之则不做强校验。
3.3.4 实验能力支持
我们设计了三个不同的数据回刷链路:增量数据更新、索引回刷、物料回刷。其中增量数据更新应用于实时发送的弹幕打分落表,索引回刷应用于在召回、粗排策略不变的情况下刷新一组(或多组)模型打分,物料回刷应用于粗排模型更新时通过全量弹幕重新计算召回和粗排淘汰。业务上出现的主要场景是,如何快速支持粗排模型外的一组模型分数更新。在之前一期的设计中,我们需要对全部的存量数据进行回刷,因此耗时非常久。针对这一特点,我们拆分了索引回刷和物料回刷,区别是是否需要更新粗排模型,重新进行召回和淘汰。这样通过索引回刷即可满足大部分策略迭代需求,减轻了整体的流程。结合前面物料池、索引池分离的存储设计,可以减少了一半的数据写入量。
索引回刷设计
消费端的一个特点是流量集中在头部稿件,因此我们的思考方向是如何通过尽量小的数据回刷来覆盖尽量多的vv百分比。在新的系统中,通过ETL任务计算得到小时更新的高热稿件(即视频)表,再通过Clickhouse存储当天的实时增量数据,这样两者结合即可得到完整的高热+实时视频列表,在需要更新索引时只需要更新这部分视频对应的弹幕即可。通过上述冷热数据分离的方式,我们最终通过15%计算量可以覆盖90%的vv,大幅提升了实验效率。
此外,部分策略迭代并不需要刷新全部视频下的弹幕。在这种情况下,通过数仓更新定制化的ETL并投递到数据消费端即可,在1小时内即可完成策略更新。
计算量优化
在推荐系统中,模型推理的计算成本也是一项重要考量,如果能节省掉冗余的推理次数,可以提升整个系统的效率,同时降低运维成本。在前一个阶段,计算一条弹幕所有模型打分的方式是并发进行所有模型的推理。但如果这条弹幕最终因粗排分过低而淘汰,则会造成计算冗余。新系统中的优化方式是,在增量数据更新和物料回刷的过程中,先进行粗排模型分的推理,完成排序淘汰之后再进行全部的模型推理,从而优化计算量。
3.3.5 精排优化
精排场景的难点是如何处理热点视频下大批量数据的计算和排序问题。一些爆火的视频会出现总长度20分钟左右,单视频弹幕量超过100w的情况,且集中在视频开头。同时各组模型分数的覆盖、刷新情况也不尽相同,如何在单组模型推理逻辑出错、部分数据已经写入索引的情况下保证业务可快速恢复降级也是精排阶段的目标之一。
退场逻辑前置
之前的架构中,精排服务器在内存中缓存弹幕索引且没有退场淘汰逻辑,成为了热点下的性能瓶颈。在新的系统中,我们将退场逻辑收敛到粗排阶段,精排时不再需要进行数据缓存,只需读取经过粗排淘汰后的物料和缓存即可。这样,在精排阶段需要排序的数据量大大减少,从而优化了响应时间。
模型版本控制
弹幕索引的模型是离线更新,如果新的模型出现逻辑错误只能在精排阶段通过业务指标分组异常发现。而这时极有可能数据刷新已经完成或者进行了一半。为了解决这种场景下的模型策略迭代可灰度、可观测、可回滚,我们通过弹幕索引的字段冗余和版本控制来实现:
假设目前线上有点赞模型、负向模型2个模型,本次需要更新负向模型版本,则:更新版本(version+1),新版负向模型更新在空白字段score16,同时保留旧版负向模型score2。模型内容和分数字段的映射关系在精排服务内维护。若发现新版模型不符合预期,则更新映射关系,使用旧版模型字段score2即可完成回滚。若经线上验证新版模型符合预期,则打分服务服务停止对旧模型)生成打分,同时在精排服务中标记旧模型字段为空白即可完成发布。
展示侧召回补充
在前面索引回刷的设计中提到,为了支持快速实验策略迭代模型回刷会覆盖大多数视频而非全量视频。如果一些冷的视频又火了起来,需要有方法能发现这些视频,并且覆盖最新的模型策略。这里的实现是,在精排服务中校验索引的版本(即version字段)。若version过低,将视频id推送到补召回队列中,更新全部最新的模型打分。这样就实现了冷热数据的转换。
收益与展望
经过工程和算法系统的整合,数据一致性、稳定性都获得了大幅提升。精排降级率从3%下降到了0.1%,在策略不变的情况下弹幕曝光也有大幅增长,再通过减少曝光、提升上屏弹幕质量的方式优化了弹幕点赞率。同时全站策略实验可以在10小时内完成数据回刷,小规模策略迭代可以在一小时内完成。同时在新的系统中,弹幕点赞、举报、状态、属性位这些特征可以做到实时而非离线,从而提升策略空间和流量冷启动的效率。
在未来,我们也会在策略迭代效率、特征实时性和工程稳定性三个方面对系统进行持续优化探索,从而带来更优质的弹幕消费体验。