乐趣区

关于Flink:5-年迭代-5-次抖音基于-Flink-的推荐系统演进历程

本文基于字节跳动举荐零碎根底服务方向负责人郭文飞在 5 月 22 日 Apache Flink Meetup 分享的《Flink 在字节跳动举荐特色体系中的落地实际》整顿,次要内容包含:

  1. 业务背景
  2. 新一代零碎架构
  3. 后续布局

2021 年,字节跳动旗下产品总 MAU 已超过 19 亿。在以抖音、今日头条、西瓜视频等为代表的产品业务背景下,弱小的举荐零碎显得尤为重要。Flink 提供了十分弱小的 SQL 模块和有状态计算模块。目前在字节举荐场景,实时简略计数特色、窗口计数特色、序列特色曾经齐全迁徙到 Flink SQL 计划上。联合 Flink SQL 和 Flink 有状态计算能力,咱们正在构建下一代通用的根底特色计算对立架构,冀望能够高效反对罕用有状态、无状态根底特色的生产。

一、业务背景

对于今日头条、抖音、西瓜视频等字节跳动旗下产品,基于 Feed 流和短时效的举荐是外围业务场景。而举荐零碎最根底的燃料是特色,高效生产根底特色对业务举荐零碎的迭代至关重要。

1. 次要业务场景

  • 抖音、火山短视频等为代表的短视频利用举荐场景,例如 Feed 流举荐、关注、社交、同城等各个场景,整体在国内大略有 6 亿 + 规模 DAU;
  • 头条、西瓜等为代表的 Feed 信息流举荐场景,例如 Feed 流、关注、子频道等各个场景,整体在国内有数亿规模 DAU;

2. 业务痛点和挑战

目前字节跳动举荐场景根底特色的生产现状是“百花齐放”。离线特色计算的基本模式都是通过生产 Kafka、BMQ、Hive、HDFS、Abase、RPC 等数据源,基于 Spark、Flink 计算引擎实现特色的计算,而后把特色的后果写入在线、离线存储。各种不同类型的根底特色计算散落在不同的服务中,不足业务形象,带来了较大的运维老本和稳定性问题。

而更重要的是,不足对立的根底特色生产平台,使业务特色开发迭代速度和保护存在诸多不便。如业务方需自行保护大量离线工作、特色生产链路不足监控、无奈满足一直倒退的业务需要等。

在字节的业务规模下,构建对立的实时特色生产零碎面临着较大挑战,次要来自四个方面:

微小的业务规模 :抖音、头条、西瓜、火山等产品的数据规模可达到日均 PB 级别。例如在抖音场景下,晚顶峰 Feed 播放量达数百万 QPS,客户端上报用户行为数据 高达数千万 IOPS。 业务方冀望在任何时候,特色工作都能够做到一直流、生产没有 lag 等,这就要求特色生产具备十分高的稳定性。

较高的特色实时化要求:在以直播、电商、短视频为代表的举荐场景下,为保障举荐成果,实时特色离线生产的时效性需实现常态稳固于分钟级别。

更好的扩展性和灵活性:随着业务场景一直简单,特色需要更为灵便多变。从统计、序列、属性类型的特色生产,到须要灵便反对窗口特色、多维特色等,业务方须要特色中台可能反对逐步衍生而来的新特色类型和需要。

业务迭代速度快:特色中台提供的面向业务的 DSL 须要足够场景,特色生产链路尽量让业务少写代码,底层的计算引擎、存储引擎对业务齐全通明,彻底开释业务计算、存储选型、调优的累赘,彻底实现实时根底特色的规模化生产,一直晋升特色生产力;

3. 迭代演进过程

在字节业务爆发式增长的过程中,为了满足各式各样的业务特色的需要,举荐场景衍生出了泛滥特色服务。这些服务在特定的业务场景和历史条件下较好反对了业务疾速倒退,大体的历程如下:

在这其中 2020 年初是一个重要节点,咱们开始在特色生产中引入 Flink SQL、Flink State 技术体系,逐渐在计数特色零碎、模型训练的样本拼接、窗口特色等场景进行落地,摸索出新一代特色生产计划的思路。

二、新一代零碎架构

联合上述业务背景,咱们基于 Flink SQL 和 Flink 有状态计算能力从新设计了新一代实时特色计算计划。新计划的定位是:解决根底特色的计算和在线 Serving,提供更加形象的根底特色业务层 DSL。

在计算层,咱们基于 Flink SQL 灵便的数据处理表达能力,以及 Flink State 状态存储和计算能力等技术,反对各种简单的窗口计算。极大地缩短业务根底特色的生产周期,晋升特色产出链路的稳定性。新的架构里,咱们将特色生产的链路分为数据源抽取 / 拼接、状态存储、计算三个阶段。Flink SQL 实现特色数据的抽取和流式拼接,Flink State 实现特色计算的中间状态存储。

有状态特色是十分重要的一类特色,其中最罕用的就是带有各种窗口的特色,例如统计最近 5 分钟视频的播放 VV 等。对于窗口类型的特色在字节外部有一些基于存储引擎的计划,整体思路是“轻离线重在线”,即把窗口状态存储、特色聚合计算全副放在存储层和在线实现。离线数据流负责根本数据过滤和写入,离线明细数据依照工夫切分聚合存储(相似于 micro batch),底层的存储大部分是 KV 存储、或者专门优化的存储引擎,在线层实现简单的窗口聚合计算逻辑,每个申请来了之后在线层拉取存储层的明细数据做聚合计算。

咱们新的解决思路是“轻在线重离线 ”,即把比拟重的 工夫切片明细数据 状态存储和窗口聚合计算全副放在离线层。窗口后果聚合通过 离线窗口触发机制 实现,把特色后果 推到 在线 KV 存储。在线模块十分轻量级,只负责简略的在线 serving,极大地简化了在线层的架构复杂度。在离线状态存储层。咱们次要依赖 Flink 提供的 原生状态存储引擎 RocksDB,充分利用离线计算集群本地的 SSD 磁盘资源,极大加重在线 KV 存储的资源压力。

对于长窗口的特色(7 天以上窗口特色),因为波及 Flink 状态层明细数据的回溯过程,Flink Embedded 状态存储引擎没有提供特地好的内部数据回灌机制(或者说不适宜做)。因而对于这种“状态冷启动”场景,咱们引入了中心化存储作为底层状态存储层的存储介质,整体是 Hybrid 架构。例如 7 天以内的状态存储在本地 SSD,7~30 天状态存储到中心化的存储引擎,离线数据回溯能够十分不便的写入中心化存储。

除窗口特色外,这套机制同样实用于其余类型的有状态特色(如序列类型的特色)。

1. 实时特色分类体系

2. 整体架构

带有窗口的特色,例如抖音视频最近 1h 的点赞量(滑动窗口)、直播间用户最近一个 session 的看播时长(session 窗口)等;

2.1 数据源层

在新的一体化特色架构中,咱们对立把各种类型数据源形象为 Schema Table,这是因为底层依赖的 Flink SQL 计算引擎层对数据源提供了十分敌对的 Table Format 形象。在举荐场景,依赖的数据源十分多样,每个特色上游依赖一个或者多个数据源。数据源能够是 Kafka、RMQ、KV 存储、RPC 服务。对于多个数据源,反对数据源流式、批式拼接,拼接类型包含 Window Join 和基于 key 粒度的 Window Union Join,维表 Join 反对 Abase、RPC、HIVE 等。具体每种类型的拼接逻辑如下:

三种类型的 Join 和 Union 能够组合应用,实现简单的多数据流拼接。例如 (A union B) Window Join (C Lookup Join D)。

另外,Flink SQL 反对简单字段的计算能力,也就是业务方能够基于数据源定义的 TableSchema 根底字段实现扩大字段的计算。业务计算逻辑实质是一个 UDF,咱们会提供 UDF API 接口给业务方,而后上传 JAR 到特色后盾加载。另外对于比较简单的计算逻辑,后盾也反对通过提交简略的 Python 代码实现多语言计算。

2.2 业务 DSL

从业务视角提供高度形象的特色生产 DSL 语言,屏蔽底层计算、存储引擎细节,让业务方聚焦于业务特色定义。业务 DSL 层提供:数据起源、数据格式、数据抽取逻辑、数据生成特色类型、数据输入形式等。

2.3 状态存储层

如上文所述,新的特色一体化计划解决的次要痛点是:如何应答各种类型(个别是滑动窗口)有状态特色的计算问题。对于这类特色,在离线计算层架构里会有一个状态存储层,把抽取层提取的 RawFeature 依照切片 Slot 存储起来 (切片能够是工夫切片、也能够是 Session 切片等)。切片类型在外部是一个接口类型,在架构上能够依据业务需要自行扩大。状态外面其实存储的不是原始 RawFeature(存储原始的行为数据太节约存储空间),而是转化为 FeaturePayload 的一种 POJO 构造,这个构造外面反对了常见的各种数据结构类型:

  • Int:存储简略的计数值类型 (多维度 counter);
  • HashMap<int, int>:存储二维计数值,例如 Action Counter,key 为 target_id,value 为计数值;
  • SortedMap<int, int>: 存储 topk 二维计数;
  • LinkedList

    :存储 id_list 类型数据;

  • HashMap<int, List

    \>:存储二维 id_list;

  • 自定义类型,业务能够依据需要 FeaturePayload 外面自定义数据类型

状态层更新的业务接口:输出是 SQL 抽取 / 拼接层抽取进去的 RawFeature,业务方能够依据业务需要实现 updateFeatureInfo 接口对状态层的更新。对于罕用的特色类型内置实现了 update 接口,业务方自定义特色类型能够继承 update 接口实现。

/**
 *  特色状态 update 接口
 */
public interface FeatureStateApi extends Serializable {
    /**
     * 特色更新接口, 上游每条日志会提取必要字段转换为 fields, 用来更新对应的特色状态
     *
     * @param fields
     *      context: 保留特色名称、主键 和 一些配置参数 ;
     *      oldFeature: 特色之前的状态
     *      fields: 平台 / 配置文件 中的抽取字段
     * @return
     */
FeaturePayLoad assign(Context context,FeaturePayLoad feature, Map<String, Object> rawFeature);
}

当然对于无状态的 ETL 特色是不须要状态存储层的。

2.4 计算层

特色计算层实现特色计算聚合逻辑,有状态特色计算输出的数据是状态存储层存储的带有切片的 FeaturePayload 对象。简略的 ETL 特色没有状态存储层,输出间接是 SQL 抽取层的数据 RawFeature 对象,具体的接口如下:

有状态特色聚合接口:

/**
 *  有状态特色计算接口
 */
public interface FeatureStateApi extends Serializable {

    /**
     * 特色聚合接口,会依据配置的特色计算窗口, 读取窗口内所有特色状态,排序后传入该接口
     *
     * @param featureInfos, 蕴含 2 个 field
     *      timeslot: 特色状态对应的时间槽
     *      Feature: 该时间槽的特色状态
     * @return
     */
    FeaturePayLoad aggregate(Context context, List<Tuple2<Slot, FeaturePayLoad>> slotStates);

}

无状态特色计算接口:

/**
 *  无状态特色计算接口
 */
public interface FeatureConvertApi extends Serializable {

    /**
     * 转换接口, 上游每条日志会提取必要字段转换为 fields, 无状态计算时,转换为外部的 feature 类型 ;
     *
     * @param fields
     *      fields: 平台 / 配置文件 中的抽取字段
     * @return
     */
    FeaturePayLoad convert(Context context,  FeaturePayLoad featureSnapshot, Map<String, Object> rawFeatures);

}

另外通过触发机制来触发特色计算层的执行,目前反对的触发机制次要有:

3. 业务落地

目前在字节举荐场景,新一代特色架构曾经在 抖音直播、电商、推送、抖音举荐等 场景陆续上线了一些实时特色。次要是有状态类型的特色,带有窗口的一维统计类型、二维倒排拉链类型、二维 TOPK 类型、实时 CTR/CVR Rate 类型特色、序列类型特色等。

在业务外围指标达成方面成效显著。在直播场景,依靠新特色架构弱小的表达能力上线了一批特色之后,业务看播外围指标、互动指标收益十分显著。在电商场景,基于新特色架构上线了 400+ 实时特色。其中在直播电商方面,业务外围 GMV、下单率指标收益显著。在抖音推送场景,基于新特色架构离线状态的存储能力,聚合用户行为数据而后写入上游各路存储,极大地缓解了业务上游数据库的压力,在一些场景中 QPS 能够降落到之前的 10% 左右。此外,抖音举荐 Feed、评论等业务都在基于新特色架构重构原有的特色体系。

值得一提的是,在电商和抖音直播场景,Flink 流式工作状态最大曾经达到 60T,而且这个量级还在一直增大。预计不久的未来,单任务的状态有可能会冲破 100T,这对架构的稳定性是一个不小的挑战。

4. 性能优化

4.1 Flink State Cache

目前 Flink 提供两类 StateBackend:基于 Heap 的 FileSystemStateBackend 和基于 RocksDB 的 RocksDBStateBackend。对于 FileSystemStateBackend,因为数据都在内存中,拜访速率很快,没有额定开销。而 RocksDBStateBackend 存在查盘、序列化 / 反序列化等额定开销,CPU 使用量会有显著回升。在字节外部有大量应用 State 的作业,对于大状态作业,通常会应用 RocksDBStateBackend 来治理本地状态数据。RocksDB 是一个 KV 数据库,以 LSM 的模式组织数据,在理论应用的过程中,有以下特点

  1. 应用层和 RocksDB 的数据交互是以 Bytes 数组的模式进行,应用层每次拜访都须要序列化 / 反序列化;
  2. 数据以追加的模式一直写入 RocksDB 中,RocksDB 后盾会一直进行 compaction 来删除有效数据。

业务方应用 State 的场景多是 get-update,在应用 RocksDB 作为本地状态存储的过程中,呈现过以下问题:

  1. 爬虫数据导致热 key,状态会一直进行更新 (get-update),单 KV 数据达到 5MB,而 RocksDB 追加更新的特点导致后盾在一直进行 flush 和 compaction,单 task 呈现慢节点(抖音直播场景)。
  2. 电商场景作业少数为大状态作业 (目前已上线作业状态约 60TB),业务逻辑中会频繁进行 State 操作。在交融 Flink State 过程中发现 CPU 的开销和原有 基于内存或 abase 的实现有 40%~80% 的升高。经优化后,CPU 开销次要集中在序列化 / 反序列化的过程中。

针对上述问题,能够通过在内存保护一个对象 Cache,达到优化热点数据拜访和升高 CPU 开销的目标。通过上述背景介绍,咱们心愿能为 StateBackend 提供一个通用的 Cache 性能,通过 Flink StateBackend Cache 性能设计方案达成以下指标:

  1. 缩小 CPU 开销 通过对热点数据进行缓存,缩小和底层 StateBackend 的交互次数,达到缩小序列化 / 反序列化开销的目标。
  2. 晋升 State 吞吐能力 通过减少 Cache 后,State 吞吐能力应比原有的 StateBackend 提供的吞吐能力更高。实践上在 Cache 足够大的状况下,吞吐能力应和基于 Heap 的 StateBackend 近似。
  3. Cache 性能通用化 不同的 StateBackend 能够间接适配该 Cache 性能。目前咱们次要反对 RocksDB,将来心愿能够间接提供给别的 StateBackend 应用,例如 RemoteStateBackend。

通过和字节基础架构 Flink 团队的单干,在实时特色生产降级 ,上线 Cache 大部分场景的 CPU 使用率大略会有高达 50% 左右的收益;

4.2 PB IDL 裁剪

在字节外部的实时特色离线生成链路当中,咱们次要依赖的数据流是 Kafka。这些 Kafka 都是通过 PB 定义的数据,字段繁多。公司级别的大 Topic 个别会有 100+ 的字段,但大部分的特色生产工作只应用了其中的局部字段。对于 Protobuf 格局的数据源,咱们能够齐全通过裁剪数据流,mask 一些非必要的字段来节俭反序列化的开销。PB 类型的日志,能够间接裁剪 idl,放弃必要字段的序号不变,在反序列化的时候会跳过 unknown field 的解析,这 对于 CPU 来说是更节俭的,然而网络带宽不会有收益, 预计裁剪后能节俭十分多的 CPU 资源。在上线了 PB IDL 裁剪之后,大部分工作的 CPU 收益在 30% 左右。

5. 遇到的问题

新架构特色生产工作实质就是一个有状态的 Flink 工作,底层的状态存储 StateBackend 次要是本地的 RocksDB。次要面临两个比拟难解的问题,一是工作 DAG 变动 Checkpoint 生效,二是本地存储不能很好地反对特色状态历史数据回溯。

  • 实时特色工作不能动静增加新的特色:对于一个线上的 Flink 实时特色生产工作,咱们不能随便增加新的特色。这是因为引入新的特色会导致 Flink 工作计算的 DAG 产生扭转,从而导致 Flink 工作的 Checkpoint 无奈复原,这对实时有状态特色生产工作来说是不能承受的。目前咱们的解法是禁止更改线上部署的特色工作配置,但这也就导致了线上生成的特色是不能轻易下线的。对于这个问题临时没有找到更好的解决办法,前期仍需一直摸索。
  • 特色状态冷启动问题:目前次要的状态存储引擎是 RocksDB,不能很好地反对状态数据的回溯。

三、后续布局

以后新一代架构还在字节举荐场景中疾速演进,目前已较好解决了实时窗口特色的生产问题。

出于实现对立举荐场景下特色生产的目标,咱们后续会持续基于 Flink SQL 流批一体能力,在批式特色生产发力。此外也会基于 Hudi 数据湖技术,实现特色的实时入湖,高效反对模型训练场景离线特色回溯痛点。规定引擎方向,打算持续摸索 CEP,推动在电商场景有更多落地实际。在实时窗口计算方向,将持续深刻调研 Flink 原生窗口机制,以期解决目前计划面临的窗口特色数据登场问题。

  • 反对批式特色:这套特色生产计划次要是解决实时有状态特色的问题,而目前字节离线场景下还有大量批式特色是通过 Spark SQL 工作生产的。后续咱们也会基于 Flink SQL 流批一体的计算能力,提供对批式场景特色的对立反对,目前也初步有了几个场景的落地;
  • 特色离线入湖:基于 Hudi On Flink 反对实时特色的离线数仓建设,次要是为了反对模型训练样本拼接场景离线特色回溯;
  • Flink CEP 规定引擎反对:Flink SQL 实质上就是一种规定引擎,目前在线上咱们把 Flink SQL 作为业务 DSL 过滤语义底层的执行引擎。但 Flink SQL 善于表白的 ETL 类型的过滤规定,不能表白带有时序类型的规定语义。在直播、电商场景的时序规定须要尝试 Flink CEP 更加简单的规定引擎。
  • Flink Native Windowing 机制引入:对于窗口类型的有状态特色,咱们目前采纳上文所述的形象 SlotState 工夫切片计划对立进行反对。另外 Flink 自身提供了十分欠缺的窗口机制,通过 Window Assigner、Window Trigger 等组件能够非常灵活地反对各种窗口语义。因而后续咱们也会在窗口特色计算场景引入 Flink 原生的 Windowing 机制,更加灵便地反对窗口特色迭代。
  • Flink HybridState Backend 架构:目前在字节的线上场景中,Flink 底层的 StateBackend 默认都是应用 RocksDB 存储引擎。这种内嵌的存储引擎不能通过内部机制去提供状态数据的回灌和多任务共享,因而咱们须要反对 KV 中心化存储计划,实现灵便的特色状态回溯。
  • 动态属性类型特色对立治理:通过特色平台提供对立的 DSL 语义,对立治理其余内部动态类型的特色服务。例如一些其余业务团队维度的用户分类、标签服务等。

更多 Flink 相干技术问题,可扫码退出社区钉钉交换群;

第一工夫获取最新技术文章和社区动静,请关注公众号~

退出移动版