背景
字节跳动特征存储痛点
当时行业界的特征存储全体流程首要分为以下四步:
特征存储的全体流程
- 事务在线进行特征模块抽取;
- 抽取后的特征以行的格局存储在 HDFS ,考虑到本钱,此时不存储原始特征,只存抽取后的特征;
- 字节跳动自研的分布式结构会将存储的特征并发读取并解码发送给练习器;
- 练习器担任高速练习。
字节跳动特征存储总量为 EB 级别,每天的增量达到 PB 级别,而且每天用于练习的资源也达到了百万中心,所以全体上字节的存储和核算的体量都是十分大的。在如此的体量之下,咱们遇到了以下三大痛点:
- 特征抽取周期长。 在特征抽取上,当时选用的是在线抽取的方法。很多的算法工程师,每天都在进行很多的特征相关的试验。在当时的在线抽取模式下,假如有算法工程师想要调研一个新的特征,那么他首先需求界说特征的核算方法,等候在线模块的统一上线,然后需求等在线抽取的特征堆集到必定的量级后才能够进行练习,然后判断这个特征是否有作用。这个进程一般需求2周甚至更长的时刻。而且,假如发现特征的核算逻辑写错或想要更改核算逻辑,则需重复上述进程。在线特征抽取导致当时字节特征调研的效率十分低。依据当时的架构,离线特征调研的本钱又十分高。
- 特征存储空间占用大。 字节的特征存储当时是以行存的方式进行存储。假如依据当时的行存做特征调研,则需求依据本来的途径额定生成新的数据集。一方面需求额定的空间对新的数据集进行存储,另一方面还需求额定的核算资源去读取本来的全量数据生成新的数据,且很难做数据的管理和复用。行存关于特征存储来说,也很难进行优化,占用空间较大。
- 模型练习 带宽大,数据读取有瓶颈。 字节当时将每个事务线的绝大部分特征都存储在一个途径下,练习的时候会直接依据这个途径进行练习。关于每个模型,练习所需的特征是不一样的,每个事务线或许存有上万个特征,而大部分模型练习往往只需求几百个特征,但因为特征是以行存格局进行存储,所以练习时需求将上万特征悉数读取后,再在内存中进行过滤,这就使得模型练习的带宽需求十分大,数据的读取成为了整个练习的瓶颈。
依据痛点的需求梳理
依据上述问题,咱们与事务方一同总结了若干需求:
- 存储原始特征:因为在线特征抽取在特征调研上的低效率,咱们期望能够存储原始特征;
- 离线调研才能:在原始特征的基础上,能够进行离线调研,然后提高特征调研效率;
- 支撑特征回填:支撑特征回填,在调研完成后,能够将前史数据悉数刷上调研好的特征;
- 下降存储本钱:充分利用数据分布的特殊性,下降存储本钱,腾出资源来存储原始特征;
- 下降练习本钱:练习时只读需求的特征,而非全量特征,下降练习本钱;
- 提高练习速度:练习时尽量下降数据的拷贝和序列化反序列化开支。
字节跳动海量特征存储处理计划
在字节的全体架构中,最上层是事务层,包含抖音、头条、小说等字节绝大部分事务线;
其下咱们经过平台层,给事务同学供给简单易用的 UI 和拜访控制等功能;
在结构层,咱们使用 Spark 作为特征处理结构(包含预处理和离线特征调研等),字节自研的 Primus 作为练习结构;
在格局层,咱们选用 Parquet 作为文件格局,Iceberg 作为表格局;
最基层是调度器 Yarn & K8s 以及存储 HDFS。
下面咱们要点针对格局层进行详细介绍。
技术选型
为了满足事务方说到的6个需求,咱们首先想到的是经过 Parquet 列存的格局,下降行存的存储本钱,节省的空间可用来存储原始特征。一起因为 Parquet 选列能够下推到存储层的特性,在练习时能够只读需求的特征,然后下降练习时反序列化的本钱,提高练习的速度。
可是使用 Parquet 引入了额定的问题,本来的行存是依据 Protobuf 界说的半结构化数据,不需求预先界说 Schema,而使用 Parquet 以后,咱们需求先知道 Schema,然后才能进行数据的存取,那么在特征新增和筛选时,Schema 的更新就是一个很难处理的问题。Parquet 并不支撑数据回填,假如要回填前史几年的数据,就需求将数据全量读取,添加新列,再全量写回,这一方面会浪费很多的核算资源,另一方面做特征回填时的 overwrite 操作,会导致当时正在进行练习的使命因为文件被替换而失败。
为了处理这几个问题,咱们引入了 Iceberg 来支撑模式演进、特征回填和并发读写。
Iceberg 是适用于大型数据集的一个开源表格局,具有模式演进、躲藏分区&分区演进、事务、MVCC、核算存储引擎解耦等特性,这些特性匹配了咱们一切的需求。因而,咱们挑选了 Iceberg。
全体上 Iceberg 是一个分层的结构,snapshot 层存储了当时表的一切快照;manifest list 层存储了每个快照包含的 manifest 云数据,这一层的用处首要是为了多个 snapshot 能够复用下一层的 manifest; manifest 层,存储了基层 Data Files 元数据;最下面的 Data File 是就是实践的数据文件。经过这样的多层结构,Iceberg 能够支撑上述包含模式演进等几个特性。
下面咱们 来一一 介绍 Iceberg 怎么支撑这些功能。
依据 Iceberg 的特征存储实践经验
并发读写
在并发读取方面,Iceberg 是依据快照的读取,对 Iceberg 的每个操作都会生成新的快照,不影响正在读取的快照,然后确保读写互不影响。
在并发写入方面,Iceberg 是选用乐观并发的方法,利用HDFS mv 的原子性语义确保只有一个能写入成功,而其他的并发写入会被检查是否有抵触,若没有抵触,则写入下一个 snapshot。
模式演进
Iceberg 的模式演进原理
咱们知道,Iceberg 元数据和 Parquet 元数据都有 Column,而中心的映射联系,是经过 ID 字段来进行一对一映射。
例如上面左图中,Iceberg 和 Parquet 分别有 ABC 三列,对应 ID 1、2、3。那终究读取出的 Dataframe 就是 和 Parquet 中一致包含 ID 为1、2、3的 ABC 三列。而当咱们对左图进行两个操作,删去旧的 B 列,写入新的 B 列后, Iceberg 对应的三列 ID 会变成1、3、4,所以右图中读出来的 Dataframe,尽管也是 ABC 三列,可是这个 B 列的 ID 并非 Parquet 中 B 列的 ID,因而终究实践的数据中,B 列为空值。
特征回填
- 写时仿制
如上图所示,COW 方法的特征回填经过一个 Backfill 使命将原快照中的数据悉数读出,然后写入新列,再写出到新的 Data File 中,并生成新的快照。
这种方法的缺点在于尽管咱们只需求写一列数据,可是需求将全体数据悉数读出,再悉数写回,不仅浪费了很多的核算资源用来对整个 Parquet 文件进行编码解码,还浪费了很多的 IO 来读取全量数据,且浪费了很多的存储资源来存储重复的 ABC 列。
因而咱们依据开源 Iceberg 自研了 MOR 的 Backfill 计划。
- 读时合并
如上图所示,在 MOR 计划中,咱们仍然需求一个 Backfill 使命来读取原始的 Data File 文件,可是这里咱们只读取需求的字段。比如咱们只需求 A 列经过某些核算逻辑生成 D 列,那么 Backfill 使命则只读取 A 的数据,而且 Snapshot2 中只需求写包含 D 列的 update 文件。随着新增列的增多,咱们也需求将 Update 文件合并回 Data File 文件中。
为此,咱们又供给了 Compaction 逻辑,即读取旧的 Data File 和 Update File,并合并成一个独自的 Data File。
MOR原理如上图,假设本来有一个逻辑 Dataframe 是由两个 Data File 构成, 现在需求回填一个 ColD 的内容。咱们会写入一个包含 ColD 的 Update File,这样 Snapshot2 中的逻辑 Dataframe 就会包含ABCD 四列。
完成细节:
-
- Data File 和 Update File 都需求一个主键,而且每个文件都需求依照主键排序,在这个比如中是 ID;
- 读取时,会依据用户挑选的列,剖析详细需求哪些 Update File 和 Data File;
- 依据 Data File 中主键的 min-max 值去挑选与该 Data File 相对应的 Update File;
- MOR 整个进程是多个 Data File 和 Update File 多路归并的进程;
- 归并的顺序由 SEQ 来决定,SEQ 大的数据会掩盖 SEQ 小的数据。
- COW 与 MOR 特性 比较
比较于 COW 方法全量读取和写入一切列,MOR 的优势是只读取需求的列,也只写入更新的列,没有读写放大问题。在核算上节省了很多的资源,读写的 IO 也大大下降,比较 COW 方法每次 COW 都翻倍的状况, MOR 只需求存储新增列,也大大避免了存储资源浪费。
考虑到性能的开支,咱们需求定时 Compaction,Compaction 是一个比较重的操作,和 COW 适当。可是 Compaction 是一个异步的进程,能够在多次 MOR 后进行一次 Compaction。那么一次 Compaction 的开支就能够摊销到多次 MOR 上,例如10次 COW 和10次 MOR + 1次 Compaction 比较,存储和读写本钱都从本来的 10x 降到当时的 2x 。
MOR 的完成本钱较高,但这能够经过良好的设计和很多的测试来处理。
而关于模型练习来说,因为大多数模型练习只需求自己的列,所以很多的线上模型都不需求走 MOR 的逻辑,能够说基本没有开支。而少量的调研模型,往往只需读自己的 Update File 而不必读其他的 Update File ,所以全体上读取的额定资源也并未添加太多。
练习优化
从行存改为 Iceberg 后,咱们也在练习上也做了很多的优化。
在咱们的原始架构中,分布式练习结构并不解析实践的数据内容,而是直接以行的方式把数据透传给练习器,练习器在内部进行反序列化、选列等操作。
原始架构
引入 Iceberg 后,咱们要拿到选列带来的 CPU 和 IO 收益就需求将选列下推到存储层。开始为了确保下流练习器感知不到,咱们在练习结构层面,将选列反序列化后,构造本钱来的 ROW 格局,发送给下流练习器。比较本来,多了一层序列化反序列化的开支。
这就导致迁移到 Iceberg 后,全体练习速度反而变慢,资源也添加了。
列式改造
为了提高练习速度,咱们经过向量化读取的方法,将 Iceberg 数据直接读成 Batch 数据,发送给练习器,这一步提高了练习速度,并下降了部分资源耗费。
向量化读取
为了达到最优作用,咱们与练习器团队协作,直接修改了练习器内部,使练习器能够直接识别 Arrow 数据,这样咱们就完成了从 Iceberg 到练习器端到端的 Arrow 格局打通,这样只需求在最开始反序列化为 Arrow ,后续的操作就彻底依据 Arrow 进行,然后下降了序列化和反序列化开支,进一步提高练习速度,下降资源耗费。
Arrow
优化收益
终究,咱们达到了开始的目标,取得了离线特征工程的才能 。
在存储本钱上,遍及下降了40%以上 ; 在同样 的 练习速度下,CPU 下降了13%,网络 IO 下降40% 。
未来规划
未来,咱们规划支撑以下4种才能:
- Upsert 的才能,支撑用户的部分数据回流;
- 物化视图的才能,支撑用户在常用的数据集上树立物化视图,提高读取效率;
- Data Skipping 才能,进一步优化数据排布,下推更多逻辑,进一步优化 IO 和核算资源;
- 依据 Arrow 的数据预处理才能,向用户供给良好的数据处理接口,一起将预处理提前预期,进一步加速后续的练习。
字节跳动基础架构批式核算团队继续招聘中,包含 Spark、Ray、ML等方向,支撑字节一切事务线,海量的数据和事务场景等你来探究。
- 作业地点:北京/杭州/新加坡
- 联系方法:欢迎添加微信 bupt001,或发送简历至邮件 qianhan@bytedance.com