vivo 短视频推荐去重服务的设计实践
一、概述
1.1 业务背景
vivo短视频在视频推荐时需要对用户已经看过的视频进行过滤去重,避免给用户重复推荐同一个视频影响体验。在一次推荐请求处理流程中,会基于用户兴趣进行视频召回,大约召回2000~10000条不等的视频,然后进行视频去重,过滤用户已经看过的视频,仅保留用户未观看过的视频进行排序,选取得分高的视频下发给用户。
1.2 当前现状
当前推荐去重基于Redis Zset实现,服务端将播放埋点上报的视频和下发给客户端的视频分别以不同的Key写入Redis ZSet,推荐算法在视频召回后直接读取Redis里对应用户的播放和下发记录(整个ZSet),基于内存中的Set结构实现去重,即判断当前召回视频是否已存在下发或播放视频Set中,大致的流程如图1所示。
(图1:短视频去重当前现状)
视频去重本身是基于用户实际观看过的视频进行过滤,但考虑到实际观看的视频是通过客户端埋点上报,存在一定的时延,因此服务端会保存用户最近100条下发记录用于去重,这样就保证了即使客户端埋点还未上报上来,也不会给用户推荐了已经看过的视频(即重复推荐)。而下发给用户的视频并不一定会被曝光,因此仅保存100条,使得未被用户观看的视频在100条下发记录之后仍然可以继续推荐。
当前方案主要问题是占用Redis内存非常大,因为视频ID是以原始字符串形式存在Redis Zset中,为了控制内存占用并且保证读写性能,我们对每个用户的播放记录最大长度进行了限制,当前限制单用户最大存储长度为10000,但这会影响重度用户产品体验。
二、方案调研
2.1 主流方案
第一,存储形式。视频去重场景是典型的只需要判断是否存在即可,因此并不需要把原始的视频ID存储下来,目前比较常用的方案是使用布隆过滤器存储视频的多个Hash值,可降低存储空间数倍甚至十几倍。
第二,存储介质。如果要支持存储90天(三个月)播放记录,而不是当前粗暴地限制最大存储10000条,那么需要的Redis存储容量非常大。比如,按照5000万用户,平均单用户90天播放10000条视频,每个视频ID占内存25B,共计需要12.5TB。视频去重最终会读取到内存中完成,可以考虑牺牲一些读取性能换取更大的存储空间。而且,当前使用的Redis未进行持久化,如果出现Redis故障会造成数据丢失,且很难恢复(因数据量大,恢复时间会很长)。
目前业界比较常用的方案是使用磁盘KV(一般底层基于RocksDB实现持久化存储,硬盘使用SSD),读写性能相比Redis稍逊色,但是相比内存而言,磁盘在容量上的优势非常明显。
2.2 技术选型
第一,播放记录。因需要支持至少三个月的播放历史记录,因此选用布隆过滤器存储用户观看过的视频记录,这样相比存储原始视频ID,空间占用上会极大压缩。我们按照5000万用户来设计,如果使用Redis来存储布隆过滤器形式的播放记录,也将是TB级别以上的数据,考虑到我们最终在主机本地内存中执行过滤操作,因此可以接受稍微低一点的读取性能,选用磁盘KV持久化存储布隆过滤器形式的播放记录。
第二,下发记录。因只需存储100条下发视频记录,整体的数据量不大,而且考虑到要对100条之前的数据淘汰,仍然使用Redis存储最近100条的下发记录。
三、方案设计
基于如上的技术选型,我们计划新增统一去重服务来支持写入下发和播放记录、根据下发和播放记录实现视频去重等功能。其中,重点要考虑的就是接收到播放埋点以后将其存入布隆过滤器。在收到播放埋点以后,以布隆过滤器形式写入磁盘KV需要经过三步,如图2所示:第一,读取并反序列化布隆过滤器,如布隆过滤器不存在则需创建布隆过滤器;第二,将播放视频ID更新到布隆过滤器中;第三,将更新后的布隆过滤器序列化并回写到磁盘KV中。
(图2:统一去重服务主要步骤)
整个过程很清晰,但是考虑到需要支持千万级用户量,假设按照5000万用户目标设计,我们还需要考虑四个问题:
第一,视频按刷次下发(一刷5~10条视频),而播放埋点按照视频粒度上报,那么就视频推荐消重而言,数据的写入QPS比读取更高,然而,相比Redis磁盘KV的性能要逊色,磁盘KV本身的写性能比读性能低,要支持5000万用户量级,那么如何实现布隆过滤器写入磁盘KV是一个要考虑的重要问题。第二,由于布隆过滤器不支持删除,超过一定时间的数据需要过期淘汰,否则不再使用的数据将会一直占用存储资源,那么如何实现布隆过滤器过期淘汰也是一个要考虑的重要问题。第三,服务端和算法当前直接通过Redis交互,我们希望构建统一去重服务,算法调用该服务来实现过滤已看视频,而服务端基于Java技术栈,算法基于C 技术栈,那么需要在Java技术栈中提供服务给C 技术栈调用。我们最终采用gRPC提供接口给算法调用,注册中心采用了Consul,该部分非重点,就不详细展开阐述。第四,切换到新方案后我们希望将之前存储在Redis ZSet中的播放记录迁移到布隆过滤器,做到平滑升级以保证用户体验,那么设计迁移方案也是要考虑的重要问题。3.1 整体流程
统一去重服务的整体流程及其与上下游之间的交互如图3所示。服务端在下发视频的时候,将当次下发记录通过统一去重服务的Dubbo接口保存到Redis下发记录对应的Key下,使用Dubbo接口可以确保立即将下发记录写入。同时,监听视频播放埋点并将其以布隆过滤器形式存放到磁盘KV中,考虑到性能我们采用了批量写入方案,具体下文详述。统一去重服务提供RPC接口供推荐算法调用,实现对召回视频过滤掉用户已观看的视频。
(图3:统一去重服务整体流程)
磁盘KV写性能相比读性能差很多,尤其是在Value比较大的情况下写QPS会更差,考虑日活千万级情况下磁盘KV写性能没法满足直接写入要求,因此需要设计写流量汇聚方案,即将一段时间以内同一个用户的播放记录汇聚起来一次写入,这样就大大降低写入频率,降低对磁盘KV的写压力。
3.2