抖音推送机制 为什么会刷到前任(5 年迭代 5 次,抖音推荐系统演进历程)

2021年,字节跳动产品的总MAU已经超过19亿。以Tik Tok、西瓜视频等为代表的产品业务背景下。,一个强大的推荐系统尤为重要。Flink提供了非常强大的SQL模块和有状态计算模块。目前在字节推荐场景中,实时简单计数特性、窗口计数特性、序列特性已经完全迁移到Flink SQL方案中。结合Flink SQL和Flink有状态计算能力,我们正在构建基础特征计算的下一代通用统一架构,希望能够高效支持常见有状态和无状态基础特征的产生。

业务背景



对于字节跳动产品如,Tik Tok,西瓜视频等。,基于提要流和短时限的推荐是核心业务场景。推荐系统最基本的燃料是特性,高效生产的基本特性对商业推荐系统的迭代非常重要。

主要业务场景



  • Tik Tok、火山短视频等短视频应用推荐场景,如Feed流推荐、关注、社交、同城等场景,拥有约6亿+DAU;在整个中国;
  • 头条、西瓜等饲料信息流推荐场景,如饲料流量、关注度、分频道等场景,拥有上亿DAU;在整个中国;
  • 业务痛点和挑战



    目前字节跳动推荐场景基础特写的制作状态是“百花齐放”。离线特征计算的基本模式是消费卡夫卡、BMQ、Hive、HDFS、Abase、RPC等数据源,基于Spark和Flink计算引擎实现特征计算,然后将特征结果写入在线和离线存储。不同类型的基础特征计算分散在不同的服务中,缺乏业务抽象,带来了极大的运维成本和稳定性问题。

    更重要的是,缺乏统一的基础特征生产平台,使得业务特征开发的迭代速度和维护不方便。比如业务端需要自行维护大量离线任务,缺乏对特色生产环节的监控,无法满足不断发展的业务需求等。



    在字节的业务规模下,构建统一的实时特征生产系统面临着巨大的挑战,主要来自四个方面:

    庞大的业务规模:Tik Tok、今日头条、西瓜、火山等产品的数据规模可以达到日均PB级别。比如Tik Tok的场景,晚高峰的Feed播放量达到百万QPS,客户端上报的用户行为数据达到千万IOPS。业务端期望功能任务可以随时保持流动,消耗上没有滞后,这就要求功能制作有非常高的稳定性。

    对实时特性的要求更高:在以直播、电商、短视频为代表的推荐场景中,为了保证推荐效果,实时特性线下制作的时效性需要正常稳定在分钟级别。

    更好的可扩展性和灵活性:随着业务场景的日益复杂,功能需求更加灵活多变。从统计、系列、属性类型的特征生产,到窗口特征、多维特征的灵活支持,业务方需要特征中间件来支持逐渐衍生的新的特征类型和需求。

    服务迭代速度快:功能中间站提供的面向服务的DSL需要足够多的场景,功能生产环节让服务尽可能少写代码,底层计算引擎和存储引擎对服务完全透明,从而彻底释放服务计算、存储选择和优化的负担,彻底实现实时基础功能的规模化生产,不断提升功能生产力;

    迭代演进过程

    在字节业务爆发式增长的过程中,为了满足各种业务特性的需求,很多特性服务都是从推荐场景中衍生出来的。这些服务在特定的业务场景和历史条件下,更好地支持业务的快速发展,大致过程如下:



    推荐场景特征服务的演进

    2020年初是其中的一个重要节点。我们开始将Flink SQL和Flink State技术系统引入到特征制作中,并逐步落地到计数特征系统、模型训练的样本拼接、窗口特征等场景中。,从而探索出新一代专题制作方案的思路。

    新一代系统架构

    结合上述业务背景,我们重新设计了基于Flink SQL和Flink状态计算能力的新一代实时特征计算方案。新方案的定位是解决基本特征的计算和在线服务,提供更抽象的基本特征服务层DSL。在计算层,我们基于Flink SQL灵活的数据处理和表达能力,以及Flink State状态存储和计算能力,支持各种复杂的窗口计算。大大缩短业务基础功能的制作周期,提高功能输出环节的稳定性。在新架构中,我们将特征产生的环节分为数据源提取/拼接、状态存储和计算三个阶段。Flink SQL完成特征数据提取和流式拼接,Flink State完成特征计算的中间状态存储。

    有状态特征是非常重要的一类特征,其中最常用的是那些有各种窗口的特征,比如统计最近5分钟视频的VV。针对窗口类型的特点,有一些基于字节内存储引擎的方案。总体思路是“轻离线,轻在线”,即所有的窗口状态存储和特征聚合计算都放在存储层,在线完成。离线数据流负责过滤和写入基础数据。离线详细数据按照时间划分进行汇总存储(类似微批量)。底层的存储大部分是KV存储或者专门优化的存储引擎。在线层完成复杂的窗口聚合计算逻辑。每个请求到来后,线上层拉取存储层的详细数据进行聚合计算。

    我们新的解决方案是“轻上线,重离线”,即所有重的时间片细节数据状态存储和窗口聚合计算都放在离线层。结果聚合由离线窗口触发机制完成,特征结果推送到在线KV存储。在线模块非常轻量,只负责简单的在线服务,大大简化了在线层的架构复杂度。在离线状态存储层。我们主要依靠Flink提供的 native state存储引擎RocksDB,充分利用离线计算集群的本地SSD磁盘资源,大大减轻在线KV存储的资源压力。

    对于长窗口的特征(7天以上的窗口特征),由于Flink状态层细节数据的回溯过程,Flink嵌入式状态存储引擎没有提供特别好的外部数据回注机制(或者说不适合它)。因此,针对这种“态冷启动”场景,我们引入集中存储作为底层态存储层的存储介质,整体结构为混合架构。比如7天内的状态存储在本地SSD,7 ~ 30天内的状态存储在集中存储引擎。离线数据回溯可以非常方便地写入集中存储。

    除了窗口特征之外,这种机制也适用于其他类型的有状态特征(例如序列类型特征)。

    实时特征分类体系

    特征类型

    定义

    功能示例

    状态特征

    有状态特征是一种非常重要的特征。我们对有状态特性的定义是:计算特性需要缓存上下文数据。

  • 带有窗口的特性,比如最近一个小时的视频的点赞(滑动窗口),直播间用户最后一次会话的观看时间(会话窗口)等。
  • 特色,比如最近100个推荐视频。
  • 无状态特征

    简单的ETL特性,可以通过简单的数据过滤来计算的特性。

    模型预测特征

    需要由外部复杂模型估计的特征

    用户的年龄、性别和其他特征。

    图形特征

    直播和社交关系场景中需要两跳关系的图类型有很多特点。

    许多图形特征也是有状态特征。

  • 礼物排序:用户观看最多的主播收到的礼物最多,首选找到用户观看最多的主播ArchorId,然后通过archon_id得到收到礼物最多的主播的ID;
  • 社交关系:朋友(可能是已经被发现的关系)关注、看播、送礼、联系麦的房间。社交关系自然是图数据结构。
  • 整体架构



    数据源层

    在新的集成特征架构中,我们将各类数据源统一抽象为Schema表,因为底层依赖的Flink SQL计算引擎层为数据源提供了非常友好的表格式抽象。在推荐场景中,依赖的数据源非常多样,每个特性的上游都依赖于一个或多个数据源。数据源可以是卡夫卡,RMQ,KV存储,RPC服务。对于多数据源,支持数据源的流式和批量拼接。拼接类型包括基于键粒度的窗口连接和窗口联合连接。维度表连接支持base、RPC、HIVE等。每种类型的拼接逻辑如下:

    数据源类型

    架构分析

    卡夫卡、BMQ

    卡夫卡、BMQ等消息类型基本都是JSON和PB,都是自描述的数据类型。可以很容易的映射成SchemaTable格式,其中对于PB类型,业务需要上传PB idl来完成表模式定义。

    KV存储

    KV中存储的大多数值都是JSON和PB格式的,类似于MQ。业务端通过提供PB IDL来完成表模式定义。通过FlinkSQL的维度表连接能力,我们将获取外部存储数据源的普通过程抽象为基本的维度表连接操作,简化了业务开发周期。

    位置遥控(remote position control)

    FlinkSQL提供了对rpc维度表的连接能力,业务提供了RPC thriftiidl完整的RPC响应表模式定义。通过维度表连接,我们将通过RPC获取外部数据源的常见流程抽象为一个基本的维度表连接模型,简化了业务开发周期。

    储备

    Hive本身就是SchemaTable的存储格式。对于离线Hive数据(实际上是map侧连接)有少量在线连接数据,可以通过Hive维度表连接来实现。

    三种类型的Join和Union可以结合起来实现复杂的多数据流拼接。例如(联合b)窗口连接(查找连接d)

    拼接类型

    拼接逻辑

    备注

    窗口连接

    使用Flink native API提供的连接操作符来连接来自同一个窗口中的多个数据流的数据。

    将TumblingWindow直接应用于原始数据流进行分段,根据event_time或process_time对齐两个窗口,然后关联数据。

    基于键粒度的区间状态连接

    类似于示例拼接逻辑。通过Union上游的多个数据源,在每个关联的主键上注册timer,等待固定的时间窗口完成多个数据源的Join操作。

    区间状态连接是对状态存储的数据的再处理。两个上游数据流经过Union后,同一个uid的实例数据和标签数据落入同一个运算符中,Joiner中的正负样本就是通过这种join方法生成的。

    查找维度表联接

    抖音推送机制

    通过关联主键,从base、RPC、Hive等服务中查看要关联的数据,完成数据的连接操作。

    多数据源联合

    多数据源联合

    此外,Flink SQL支持复杂字段的计算能力,即业务端可以基于数据源定义的TableSchema基本字段计算扩展字段。商业逻辑的本质是UDF。我们将向业务方提供UDF API接口,然后上传JAR以实现后台加载。此外,对于简单的计算逻辑,后台还通过提交简单的Python代码来支持多语言计算。

    业务 DSL

    从业务角度提供高度抽象的特性生产DSL语言,屏蔽底层计算和存储引擎的细节,让业务端专注于业务特性的定义。服务DSL层提供:数据源、数据格式、数据提取逻辑、数据生成特征类型、数据输出方式等。



    状态存储层



    如前所述,新的特征集成方案解决的主要痛点是:如何处理各种类型(一般是滑动窗口)带有状态特征的计算问题。对于这类特征,离线计算层架构中会有一个状态存储层,按照片槽存储提取层提取的RawFeature(片可以是时间片,也可以是会话片等。).切片类型在内部是一种接口类型,在架构上可以根据业务需求自行扩展。实际上,状态中存储的并不是原始的RawFeature(存储原始行为数据浪费存储空空间),而是一个转换成FeaturePayload的POJO结构,它支持各种常见的数据结构类型:

  • Int:存储简单的计数值类型(多维计数器);
  • HashMap:存储二维计数值,如Action Counter,其中key为target_id,value为计数值;
  • SortedMap:存储topk二维计数;
  • LinkedList:存储id_list类型的数据;
  • HashMap & gt:存储2D id _ list;;
  • 用户自定义类型,业务可以根据需求自定义FeaturePayload中的数据类型。
  • 状态更新的业务接口:输入是从SQL提取/拼接层提取的RawFeature,业务端可以根据业务需求使用updateFeatureInfo接口更新状态层。对于常用的特性类型,更新接口是内置的,业务端定义的特性类型可以继承更新接口的实现。

    /** *功能状态更新接口 */ 公共接口功能状态API扩展可序列化{ /* * *功能更新接口,每个日志上游都会提取必要的字段并转换成字段。用于更新对应的功能状态 * * @ paramfields *上下文:保存功能名称、主键和一些配置参数; * oldFeature:功能 *字段之前的状态:平台/配置文件中的提取字段 * @ return */ 功能负载分配(context上下文、功能负载功能、映射

    当然,无状态ETL特性不需要状态存储层。

    计算层

    特征计算层完成特征计算聚合逻辑,状态特征计算的输入数据是状态存储层存储切片的FeaturePayload对象。简单ETL特性没有状态存储层,输入直接是SQL提取层的data RawFeature对象。具体界面如下:

    /** *有状态特征计算接口 */ 公共接口特征状态API扩展可序列化{ /* * *特征聚合接口将根据配置的特征计算窗口读取窗口中的所有特征状态。排序后传入接口 * * @param featureInfos,包含两个字段 *时隙:特征状态对应的时隙 *特征:该时隙的特征状态 * @ return */[tuple 2 & lt;插槽,功能有效负载& gt& gtslot States); }复制代码

    状态特征聚合接口

    /** *无状态特征计算接口 */ 公共接口特征转换API扩展serializable { /* * *转换接口,上游的每个日志都会提取必要的字段并转换成字段,在无状态计算的情况下,转换成内部特征类型; * * @ param fields * fields:平台/配置文件中的提取字段 * @ return */ 功能负载转换(context上下文,功能负载功能快照,map & String,Object & gt原始特征); }复制代码

    无状态特征计算接口

    此外,触发机制用于触发特征计算层的执行。目前支持的触发机制主要包括:

    策略

    解释

    OnTimerTrigger

    周期性触发特性的计算逻辑

    OnUpdateTrigger

    上游状态图层的每次更新都会触发要素计算。

    自定义触发器

    自定义特征计算的触发时间

    业务落地

    目前,在字节推荐场景中,新一代功能架构已经上线 Tik Tok直播、电商、推送、Tik Tok推荐等场景中的部分实时功能。主要以状态类型为特征,如带窗口的一维统计型、二维倒拉链型、二维TOPK型、实时CTR/CVR率型特征、序列型特征等。

    核心业务指标取得显著成绩。在直播场景中,基于新功能架构强大的表达能力,多项功能上线后,商业看播的核心指标和互动指标收益非常显著。在电商场景,基于新的功能架构推出了400+实时功能。其中,在直播电商方面,业务核心GMV和订单率指标均取得了显著回报。在Tik Tok推送场景中,基于新功能架构的离线存储能力,将用户行为数据进行聚合后写入下游存储,极大地缓解了业务下游数据库的压力,在某些场景下,QPS可以降低到之前的10%左右。此外,Tik Tok推荐的Feed、评论等服务,都是基于新的特征架构来重构原有的特征体系。

    值得一提的是,在电商和Tik Tok的直播场景中,Flink流任务的最大状态已经达到了60T,而且这个量级还在不断增加。预测在不久的将来,单个任务的状态可能会超过100T,这对架构的稳定性是一个很大的挑战。

    性能优化Flink State Cache

    目前Flink提供了两种StateBackend:基于Heap的FileSystemStateBackend和基于rocksdb的RocksDBstatebackend。对于FileSystemStateBackend,因为数据在内存中,所以访问速度非常快,没有额外的开销。但是RocksDBStateBackend有磁盘搜索、序列化/反序列化等额外开销,CPU使用率会明显增加。有很多作业使用字节内的状态。对于大型状态作业,RocksDBStateBackend通常用于管理本地状态数据。RocksDB是一个KV数据库,它以LSM的形式组织数据。在实际使用中,它有以下特点:

  • 应用层与RocksDB的数据交互是以字节数组的形式,应用层的每次访问都需要序列化/反序列化。
  • 数据以追加的形式不断写入RocksDB,RocksDB后台会不断进行压缩删除无效数据。
  • Get-update是业务方使用状态的最常见场景。在使用RocksDB作为本地状态存储的过程中,出现了以下问题:

  • 爬虫数据导致热键,状态会不断更新(get-update),单KV数据达到5MB。但是RocksDB的额外更新导致后台持续刷新和压缩,单任务出现慢节点(Tik Tok现场)。
  • 电商场景作业大多是大作业(目前线上作业状态约60TB),业务逻辑中会频繁执行状态操作。在合并Flink状态的过程中,发现与原来基于内存或数据库的实现相比,CPU开销增加了40%~80%。优化后,CPU开销主要集中在序列化/反序列化的过程中。
  • 为了解决上述问题,我们可以在内存中维护一个对象缓存,以优化热数据访问并减少CPU开销。通过以上背景介绍,我们希望为StateBackend提供一个通用的缓存功能,通过Flink StateBackend缓存功能的设计方案,达到以下目的:

  • 降低CPU开销:通过缓存热数据,减少与底层StateBackend的交互次数,达到降低序列化/反序列化开销的目的。
  • 提高状态吞吐量:通过添加缓存,状态吞吐量应该高于原来StateBackend提供的吞吐量。理论上,如果缓存足够大,吞吐量应该类似于基于堆的StateBackend。
  • 缓存功能的泛化:不同的StateBackend可以直接适配缓存功能。目前主要支持RocksDB。未来我们希望可以直接提供给其他statebackends,比如远程StateBackend。
  • 通过与字节基础设施Flink团队的合作,在实时特性制作升级中,在线缓存大部分场景的CPU利用率将可能高达50%左右。

    PB IDL 裁剪

    在字节内实时特征的离线生成环节,我们主要依靠Kafka作为数据流。这些卡夫卡都是PB定义的数据,字段很多。公司级主题通常有100+个字段,但大多数功能制作任务只使用其中的一些字段。对于Protobuf格式的数据源,我们可以完全剪切数据流,屏蔽掉一些不必要的字段,以节省反序列化的成本。PB类型的log可以直接裁剪idl,保持必要字段的序号不变,反序列化时会跳过未知字段的解析,这样对CPU更经济,但是网络带宽不会盈利,预计裁剪后会节省大量CPU资源。大部分任务在线PB IDL剪裁后的CPU收益在30%左右。

    遇到的问题

    新架构特征生产任务的本质是一个有状态的Flink任务,底层的状态存储StateBackend主要是本地的RocksDB。主要有两个难题,一个是任务DAG变化导致的检查点失效,另一个是本地存储不能很好的支持特征状态历史数据的追溯。

  • 实时特征任务不能动态添加新特征:对于一个在线Flink实时特征制作任务,我们不能随意添加新特征。这是因为新特性的引入会改变Flink任务计算的DAG,使得Flink任务的检查点不可恢复,这对于有状态特性的实时生产任务来说是不可接受的。目前我们的解决方案是禁止改变线上部署的功能任务的配置,但这也导致了线上生成的功能不能随便下线。这个问题暂时没有更好的解决方案,后期还需要继续探索。
  • 特性冷启动问题:目前主要的状态存储引擎是RocksDB,不能很好的支持状态数据的回溯。
  • 后续规划

    目前新一代架构在字节推荐场景下快速演进,解决了实时窗口特性的制作问题。

    以统一推荐场景下的特征生产为目的,基于Flink SQL流批量集成能力,在批量特征生产方面继续努力。此外,基于胡迪数据湖技术,将完成实时特征入湖,高效支撑模型训练场景的离线特征回溯痛点。规则引擎方向,计划继续探索CEP,推动更多电商场景落地实践。在实时窗口计算方向,将继续深入研究Flink原生窗口的机制,以解决当前方案面临的窗口特征数据的退出问题。

  • 支持批量特征:这种特征生产方案主要解决实时有状态特征的问题。但目前在字节离线场景下,仍然存在大量Spark SQL任务产生的批量特性。后续还将基于Flink SQL流批量集成的计算能力,提供批量场景特性的统一支持,目前已初步落地多个场景;
  • 特征离线入湖:基于Flink上胡迪的离线数据仓库构建支持实时特征,主要支持模型训练样本拼接场景的离线特征回溯;
  • Flinccep规则引擎支持:FlincSQL本质上是一个规则引擎。目前在线上,我们使用FlincSQL作为业务DSL过滤语义底层的执行引擎。而Flink SQL擅长表达ETL类型的过滤规则,却无法用时序类型表达规则语义。直播和电商场景下的计时规则需要尝试Flink CEP更复杂的规则引擎。
  • Flink原生窗口化机制介绍:对于窗口类型的有状态特性,我们目前采用上面描述的抽象SlotState时间切片方案来统一支持。此外,Flink本身提供了完善的窗口机制,可以通过窗口分配器、窗口触发器等组件灵活支持各种窗口语义。因此,后续我们会将Flink的原生开窗机制引入到窗口特征计算场景中,更灵活地支持窗口特征迭代。
  • Flink HybridState后端架构:目前在字节在线场景下,Flink底层的State Backend默认使用RocksDB存储引擎。这种嵌入式存储引擎无法通过外部机制提供状态数据充值和多任务共享,需要支持KV集中存储方案,实现灵活的特征状态回溯。
  • 静态属性类型特征的统一管理:通过特征平台提供统一的DSL语义,统一管理其他外部静态类型特征服务。比如一些其他业务团队维度的用户分类,标签服务等。
  • 作者简介:

    郭文飞,字节跳动推荐系统基础服务方向负责人。字节于2015年初加入,主要负责推荐系统的基本服务方向,如消重、计数、特性等。

    字节跳动推荐架构团队实时计算方向,负责Tik Tok、西瓜视频等超10亿用户的产品推荐系统架构实时计算系统的设计与开发,保证系统的稳定性和高可用性。抽象通用实时计算系统,构建统一的推荐特征中间平台,实现灵活可扩展的高性能存储系统和计算模型,实现推荐业务的去重、计数、特征服务等先进的实时推荐数据流系统。目前非常缺人。欢迎追求技术的同学加入我们,构建世界一流的先进实时推荐数据流系统。联系人:guowenfei@bytedance.com。

    您可以还会对下面的文章感兴趣

    使用微信扫描二维码后

    点击右上角发送给好友