字节跳动埋点数据流建设与治理实践
更多技术交流、求职机会、试用福利,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群
本文将介绍字节跳动在埋点数据流业务场景遇到的需求和挑战以及具体实践,包含埋点数据流简介、埋点数据流建设实践、埋点数据流治理实践以及未来规划。关注字节跳动数据平台微信公众号,回复【0627】获得本次分享材料。
埋点数据流
埋点数据流在字节跳动
埋点数据流主要处理的数据是埋点,埋点也叫 Event Tracking,是数据和业务之间的桥梁,也是数据分析、推荐、运营的基石。
用户在使用 App 、小程序、 Web 等各种线上应用时产生的用户行为数据主要通过埋点的形式进行采集上报,按不同的来源可以分为:
客户端埋点
Web 端埋点
服务端埋点
埋点通过埋点收集服务接收到 MQ,经过一系列的 Flink 实时 ETL 对埋点进行数据标准化、数据清洗、数据字段扩充、实时风控反作弊等处理,最终分发到不同的下游。下游主要包括推荐、广告、ABTest、行为分析系统、实时数仓、离线数仓等。因为埋点数据流处在整个数据处理链路的最上游,所以决定了“稳定性”是埋点数据流最为关注的一点。
字节跳动的埋点数据流规模
字节跳动埋点数据流的规模比较大,体现在以下几个方面:
接入的业务数量很多,包括抖音、今日头条、西瓜视频、番茄小说在内的多个 App 和服务,都接入了埋点数据流。
流量很大,当前字节跳动埋点数据流峰值流量超过 1 亿每秒,每天处理超过万亿量级埋点,PB 级数据存储增量。
ETL 任务规模体量较大,在多个机房部署了超过 1000 个 Flink 任务和超过 1000 个 MQ Topic,使用了超过 50 万 Core CPU 资源,单个任务最大超过 12 万 Core CPU,单个 MQ Topic 最大达到 10000 个 partition。
那么在这么巨大的流量和任务规模下,埋点数据流主要处理的是哪些问题呢?我们来看几个具体的业务场景。
业务场景
UserAction ETL
在推荐场景中,由于埋点种类多、流量巨大,而推荐只关注其中部分埋点,因此需要通过 UserAction ETL 对埋点流进行处理,对这个场景来说有两个需求点:
数据流的时效性
ETL 规则动态更新
为了提升下流推荐系统的处理效率,我们在数据流配置 ETL 规则对推荐关注的埋点进行过滤,并对字段进行删减、映射、标准化等清洗处理,将埋点打上不同的动作类型标识,处理之后的埋点内部一般称为 UserAction。UserAction 与服务端展现、Feature 等数据会在推荐 Joiner 任务的分钟级窗口中进行拼接处理,产出 instance 训练样本。
举个例子:一个客户端的文章点赞埋点,描述了一个用户在某一个时间点对某一篇文章进行了点赞操作,这个埋点经过埋点收集服务进入 ETL 链路,通过 UserAction ETL 处理后,实时地进入推荐 Joiner 任务中拼接生成样本,更新推荐模型,从而提升用户的使用体验。
如果产出 UserAction 数据的 ETL 链路出现比较大的延迟,就不能在拼接窗口内及时地完成训练样本的拼接,可能会导致用户体验的下降,因此对于推荐来说,数据流的时效性是比较强的需求。而推荐模型的迭代和产品埋点的变动都可能导致 UserAction ETL 规则的变动,如果我们把这个 ETL 规则硬编码在代码中,每次修改都需要升级代码并重启相关的 Flink ETL 任务,这样会影响数据流的稳定性和数据的时效性,因此这个场景的另一个需求是 ETL 规则的动态更新。
数据分流
抖音的埋点 Topic 晚高峰超过一亿每秒,而下游电商、直播、短视频等不同业务关注的埋点都只是其中一部分。如果每个业务都分别使用一个 Flink 任务去消费抖音的全量埋点去过滤出自己关注的埋点,会消耗大量的计算资源,同时也会造成 MQ 集群带宽扇出非常严重,影响 MQ 集群的稳定性。
因此我们提供了数据分流服务,实现上是我们使用一个 Flink 任务去消费上游埋点 Topic,通过在任务中配置分流规则的方式,将各个业务关注的埋点分流到下游的小 Topic 中提供给各业务消费,减少不必要的资源开销,同时也降低了 MQ 集群出带宽。
分流需求大多对 SLA 有一定要求,断流和数据延迟可能会影响下流的推荐效果、广告收入以及数据报表更新等。另外随着业务的发展,实时数据需求日益增加,分流规则新增和修改变得非常频繁,如果每次规则变动都需要修改代码和重启任务会对下游造成较大影响,因此在数据分流这个场景,规则的动态更新也是比较强的需求。
容灾降级
另一个场景是容灾降级。数据流容灾首先考虑的是防止单个机房级别的故障导致埋点数据流完全不可用,因此埋点数据流需要支持多机房的容灾部署。其次当出现机房级别的故障时,需要将故障机房的流量快速调度到可用机房实现服务的容灾恢复,因此需要埋点数据流具备机房间快速切流的能力。
而数据流降级主要考虑的是埋点数据流容量不足以承载全部流量的场景,比如春晚活动、电商大促这类有较大突发流量的场景。为了保障链路的稳定性和可用性,需要服务具备主动或者被动的降级能力。
埋点数据流遇到挑战
挑战主要是流量大和业务多导致的。流量大服务规模就大,不仅会导致成本治理的问题,还会带来单机故障多、性能瓶颈等因素引发的稳定性问题。而下游业务多、需求变化频繁,推荐、广告、实时数仓等下游业务对稳定性和实时性都有比较高的要求。
在流量大、业务多这样的背景下,如何保障埋点数据流稳定性的同时降低成本、提高效率,是埋点数据流稳定性治理和成本治理面对的挑战。
埋点数据流建设实践
上文我们了解了埋点数据流的业务场景和面对的挑战,接下来会介绍埋点数据流在 ETL 链路建设和容灾与降级能力上的一些实践。
ETL 链路建设
发展历程
埋点数据流 ETL 链路发展到现在主要经历了三个阶段。
第一个阶段是 2018 年以前,业务需求快速迭代的早期阶段。那时我们主要使用 PyJStorm 与基于 Python 的规则引擎构建主要的流式处理链路。特点是比较灵活,可以快速支持业务的各种需求,伴随着埋点量的快速上涨,PyJStorm 暴露出很多稳定性和运维上的问题,性能也不足以支撑业务增长。2018 年内部开始大力推广 Flink,并且针对大量旧任务使用 PyJStorm 的情况提供了 PyJStorm 到 PyFlink 的兼容适配,流式任务托管平台的建设一定程度上也解决了流式任务运维管理问题,数据流 ETL 链路也在 2018 年全面迁移到了 PyFlink,进入到 Flink 流式计算的新时代。
第二个阶段是 2018 年到 2020 年,随着流量的进一步上涨,PyFlink 和 kafka 的性能瓶颈以及当时使用的 JSON 数据格式带来的性能和数据质量问题纷纷显现出来。与此同时,下流业务对数据延迟、数据质量的敏感程度与日俱增。我们不仅对一些痛点进行了针对性优化,还花费一年多的时间将整个 ETL 链路从 PyFlink 切换到 Java Flink,使用基于 Groovy 的规则引擎替换了基于 Python 的规则引擎,使用 Protobuf 替代了 JSON,新链路相比旧链路性能提升了数倍。同时大数据开发平台和流量平台的建设提升了埋点数据流在任务开发、ETL 规则管理、埋点管理、多机房容灾降级等多方面的能力。
第三个阶段是从 2021 年开始至今,进一步提升数据流 ETL 链路的性能和稳定性,在满足流量增长和需求增长的同时,降低资源成本和运维成本是这一阶段的主要目标。我们主要从三个方面进行了优化。
优化了引擎性能,随着流量和 ETL 规则的不断增加,我们基于 Groovy 的规则引擎使用的资源也在不断增加,所以基于 Janino 对规则引擎进行了重构,引擎的性能得到了十倍的提升。
基于流量平台建设了一套比较完善的埋点治理体系,通过埋点下线、埋点管控、埋点采样等手段降低埋点成本。
将链路进行了分级,不同的等级的链路保障不同的 SLA,在资源不足的情况下,优先保障高优链路。
接下来是我们 2018 至 2020 年之间埋点数据流 ETL 链路建设的一些具体实践。
基于规则引擎的 Flink ETL
在介绍业务场景时,提到我们一个主要的需求是 ETL 规则的动态更新,那么我们来看一下埋点数据流 Flink ETL 任务是如何基于规则引擎支持动态更新的,如何在不重启任务的情况下,实时的更新上下游的 Schema 信息、规则的处理逻辑以及修改路由拓扑。
首先,我们在流量平台上配置了上下游数据集的拓扑关系、Schema 和 ETL 规则,然后通过 ConfigCenter 将这些元数据发送给 Flink ETL Job,每个 Flink ETL Job 的 TaskManager 都有一个 Meta Updater 更新线程,更新线程每分钟通过 RPC 请求从流量平台拉取并更新相关的元数据,Source operator 从 MQ Topic 中消费到的数据传入 ProcessFunction,根据 MQ Topic 对应的 Schema 信息反序列化为 InputMessage,然后进入到规则引擎中,通过规则索引算法匹配出需要运行的规则,每条规则我们抽象为一个 Filter 模块和一个 Action 模块,Fliter 和 Action 都支持 UDF,Filter 筛选命中后,会通过 Action 模块对数据进行字段的映射和清洗,然后输出到 OutputMessage 中,每条规则也指定了对应的下游数据集,路由信息也会一并写出。
当 OutputMessage 输出到 Slink 后,Slink 根据其中的路由信息将数据发送到 SlinkManager 管理的不同的 Client 中,然后由对应的 Client 发送到下游的 MQ 中。
规则引擎
规则引擎为埋点数据流 ETL 链路提供了动态更新规则的能力,而埋点数据流 Flink ETL Job 使用的规则引擎也经历了从 Python 到 Groovy 再到 Janino 的迭代。
由于 Python 脚本语言本身的灵活性,基于 Python 实现动态加载规则比较简单。通过 Compile 函数可以将一段代码片段编译成字节代码,再通过 eval 函数进行调用就可以实现。但 Python 规则引擎存在性能较弱、规则缺乏管理等问题。
迁移到 Java Flink 后,在流量平台上统一管理运维 ETL 规则以及 schema、数据集等元数据,用户在流量平台编辑相应的 ETL 规则,从前端发送到后端,经过一系列的校验最终保存为逻辑规则。引擎会将这个逻辑规则编译为实际执行的物理规则,基于 Groovy 的引擎通过 GroovyClassLoader 动态加载规则和对应的 UDF。虽然 Groovy 引擎性能比 Python 引擎提升了多倍,但 Groovy 本身也存在额外的性能开销,因此我们又借助 Janino 可以动态高效地编译 Java 代码直接执行的能力,将 Groovy 替换成了 Janino,同时也将处理 Protobuf 数据时使用的 DynamicMessage 替换成了 GeneratedMessage,整体性能提升了 10 倍。
除了规则引擎的迭代,我们在平台侧的测试发布和监控方面也做了很多建设。测试发布环节支持了规则的线下测试,线上调试,以及灰度发布的功能。监控环节支持了字段、规则、任务等不同粒度的异常监控,如规则的流量波动报警、任务的资源报警等。
Flink 拆分任务
规则引擎的应用解决了埋点数据流 ETL 链路如何快速响应业务需求的问题,实现了 ETL 规则的动态更新,从而修改 ETL 规则不需要修改代码和重启任务。
但规则引擎本身的迭代、流量增长导致的资源扩容等场景,还是需要升级重启 Flink 任务,导致下游断流。
除了重启断流外,大任务还可能在重启时遇到启动慢、队列资源不足或者资源碎片导致起不来等情况。
针对这些痛点我们上线了 Flink 拆分任务,本质上是将一个大任务拆分为一组子任务,每个子任务按比例去消费上游 Topic 的部分 Partition,按相同的逻辑处理后再分别写出到下游 Topic。
举个例子:上游 Topic 有 200 个 Partition,我们在一站式开发平台上去配置 Flink 拆分任务时只需要指定每个子任务的流量比例,每个子任务就能自动计算出它需要消费的 topic partition 区间,其余参数也支持按流量比例自动调整。
拆分任务的应用使得数据流除了规则粒度的灰度发布能力之外,还具备了 Job 粒度的灰度发布能力,升级扩容的时候不会发生断流,上线的风险更可控。同时由于拆分任务的各子任务是独立的,因此单个子任务出现反压、Failover 对下游的影响更小。另一个优点是,单个子任务的资源使用量更小,资源可以同时在多个队列进行灵活的部署。
容灾与降级能力建设
说到 ETL 链路建设,埋点数据流在容灾与降级能力建设方面也进行了一些实践。
首先是容灾能力的建设,埋点数据流在 Flink、MQ、Yarn、HDFS 等组件支持多机房容灾的基础上完成了多机房容灾部署,并准备了多种流量调度的预案。
正常情况下流量会均匀打到多个机房,MQ 在多个机房间同步,Flink ETL Job 默认从本地 MQ 进行消费,如果某个机房出现故障,我们根据情况可以选择通过配置下发的方式从客户端将流量调度到其他非受灾机房,也可以在 CDN 侧将流量调度到其他非受灾机房。埋点数据流 ETL 链路可以分钟级地进入容灾模式,迅速将故障机房的 Flink Job 切换到可用的机房。
其次是服务降级能力的建设,主要包含服务端降级策略和客户端降级策略。服务端降级策略主要通过 LB 限流、客户端进行退避重试的机制来实现,客户端降级策略通过配置下发可以降低埋点的上报频率。
举个例子:在春晚活动中参与的用户很多,口播期间更是有着非常巨大的流量洪峰,2021 年春晚活动期间为了应对口播期间的流量洪峰,埋点数据流开启了客户端的降级策略,动态降低了一定比例用户的埋点上报频率,在口播期间不上报,口播结束后迅速恢复上报。在降级场景下,下游的指标计算是通过消费未降级用户上报的埋点去估算整体指标。目前我们在此基础上进行了优化,客户端目前的降级策略可以更近一步的根据埋点的分级信息去保障高优的埋点不降级,这样可以在活动场景下保障活动相关的埋点不降级的上报,支持下游指标的准确计算。
埋点数据流治理实践
介绍完埋点数据流建设的实践,接下来给大家分享的是埋点数据流治理方面的一些实践。埋点数据流治理包含多个治理领域,比如稳定性、成本、埋点质量等,每个治理领域下面又有很多具体的治理项目。
比如在稳定性治理中我们通过优化减少了由于单机问题、MQ 性能问题和混布问题等导致的各种稳定性问题;
成本治理方面,我们通过组件选型、性能优化、埋点治理等方式取得了显著降本增效的成果;
埋点质量治理方面,我们对脏数据问题、埋点字段类型错误问题和埋点数据的丢失重复问题进行了监控和治理。
这次我们主要选取了其中部分治理项目和大家分享。
单机问题优化
Flink BacklogRescale
Yarn 单机问题导致 Flink 任务 Failover、反压、消费能力下降是比较常见的 case。
单机问题的类型有很多:队列负载不均、单机 load 高或者其他进程导致 CPU 负载高,以及一些硬件故障都可能导致 Yarn 单机问题。针对 Yarn 单机问题,我们从 Flink 和 Yarn 两个层面分别进行了优化,最终使单机 load 高导致的数据延迟减少了 80%以上。
首先是 Flink 层面的优化,在埋点数据流 ETL 场景中,为了减少不必要的网络传输,我们的 Partitioner 主要采用的是 Rescale Partitioner,而 Rescale Partitioner 会使用 Round-Robin 的方式发送数据到下游 Channel 中。由于单机问题可能导致下游个别 Task 反压或者处理延迟从而引起反压,而实际上在这个场景里面,数据从上游 task 发送到任何一个下游的 Task 都是可以的,合理的策略应该是根据下游的 Task 的处理能力去发送数据,而不是用 Round-Robin 方式。
另一方面我们注意到 Flink Credit-Based flow control 反压机制中,可以用 backlog size 去判断下游 Task 的处理负载,我们也就可以将 Round Robin 的发送方式修改为根据 Channel 的 Backlog size 信息,去选择负载更低的下游 Channel 进行发送。这个 Feature 上线后,队列的负载变得更加均衡,CPU 的使用率也提升了 10%。
Yarn 优化
Yarn 层面的优化,第一个是队列资源层面,我们使用独立的 Label 队列可以避免高峰期被其他低优任务影响。
第二个是对于 Yarn 节点上的 DataNode 把带宽打满或者 CPU 使用比较高影响节点上埋点数据流 Flink 任务稳定性的情况,通过给 DataNode 进行网络限速,CPU 绑核等操作,避免了 DataNode 对 Flink 进程的影响。
第三个是 Yarn 反调度的策略,目前字节跳动 Flink 使用的 Yarn Gang Scheduler 会按条件约束选择性地分配 Yarn 资源,在任务启动时均衡的放置 Container,但是由于时间的推移,流量的变化等各种因素,队列还是会出现负载不均衡的情况,所以反调度策略就是为了解决这种负载不均衡而生的二次调度机制。
反调度策略中,Yarn 会定期检查不满足原有约束的 Container,并在这些 Container 所在节点上筛选出需要重新调度的 Container 返还给 Flink Job Manager,然后 Flink 会重新调度这些 Container,重新调度会按照原有的约束条件尝试申请等量的可用资源,申请成功后再进行迁移。
另外我们会针对一些频繁出问题的节点把它们加入调度的黑名单,在调度的时候避免将 container 调度到这些节点。
MQ 优化
Databus 应用
在流量迅速增长的阶段,埋点数据流 Flink 任务一开始是通过 Kafka Connecter 直接写入 Kafka。但由于任务处理的流量非常大,Flink 任务中 Sink 并发比较多,导致批量发送的效率不高,Kafka 集群写入的请求量非常大。并且由于每个 Sink 一个或多个 Client,Client 与 Kafka 之间建立的连接数也非常多。而 Kafka 由于 Controller 的性能瓶颈无法继续扩容,所以为了缓解 Kafka 集群的压力,埋点数据流的 Flink 任务引入了 Databus 组件。Databus 是一种以 Agent 方式部署在各个节点上的 MQ 写入组件。Databus Agent 可以配置多个 Channel,每个 Channel 对应一个 Kafka 的 Topic。Flink Job 每个 Task Manager 里面的 Sink 会通过 Unix Domain Socket 的方式将数据发送到节点上 Databus Agent 的 Channel 里面,再由 Databus 将数据批量地发送到对应的 Kafka Topic。由于一个节点上会有多个 Task Manager,每个 Task Manager 都会先把数据发送到节点上的 Databus Agent,Databus Agent 中的每个 Channel 实际上聚合了节点上所有 Task Manager 写往同一个 Topic 数据,因此批量发送的效率非常高,极大地降低了 Kafka 集群的写入请求量,并且与 Kafka 集群之间建立的连接数也更少,通过 Agent 也能方便地设置数据压缩算法,由于批量发送的原因压缩效率比较高。在我们开启了 Zstd 压缩后,Kafka 集群的写入带宽降低了 37%,极大地缓解了 Kafka 集群的压力。
Kafka 迁移 BMQ
在埋点数据流这种大流量场景下使用 Kafka,会经常遇到 Broker 或者磁盘负载不均、磁盘坏掉等情况导致的稳定性问题,以及 Kafka 扩容、Broker 替换等运维操作也会影响集群任务正常的读写性能,除此之外 Kafka 还有 controller 性能瓶颈、多机房容灾部署成本高等缺点。
为了优化这些问题,BMQ 这款字节跳动自研的存储计算分离的 MQ 应运而生。BMQ 的数据存储使用了 HDFS 分布式存储,每个 Partition 的数据切分为多个 segment,每个 segment 对应一个 HDFS 文件,Proxy 和 Broker 都是无状态的,因此可以支持快速的扩缩容,并且由于没有数据拷贝所以扩缩容操作也不会影响读写性能。
受益于 HDFS 已经建设得比较完善的多机房容灾能力,BMQ 多机房容灾部署就变的非常简单,数据同时写入所有容灾机房后再返回成功即可保障多机房容灾。数据消费是在每个机房读取本地的 HDFS 进行消费,减少了跨机房带宽。除此之外,由于基于多机房 HDFS 存储比 Kafka 集群多机房部署所需的副本更少,所以最终实现了单 GB 流量成本对比 Kafka 下降了 50%的资源收益。
成本治理-埋点治理
在埋点治理方面,通过对流量平台的建设,提供了从埋点设计、埋点注册、埋点验证、埋点上报、埋点采样、流式 ETL 处理,再到埋点下线的埋点全生命周期的管理能力。
埋点管控
目前字节跳动所有的产品都开启了埋点管控。所有的埋点都需要在我们的流量平台上注册埋点元数据之后才能上报。而我们的埋点数据流 ETL 也只会处理已经注册的埋点,这是从埋点接入流程上进行的管控。
在埋点上报环节,通过在流量平台配置埋点的采样率对指定的埋点进行采样上报,在一些不需要统计全量埋点的场景能显著地降低埋点的上报量。
对于已经上报的埋点,通过埋点血缘统计出已经没有在使用的埋点,自动通知埋点负责人在流量平台进行自助下线。埋点下线流程完成后会通过服务端动态下发配置到埋点 SDK 以及埋点数据流 ETL 任务中,确保未注册的埋点在上报或者 ETL 环节被丢弃掉。还支持通过埋点黑名单的方式对一些异常的埋点进行动态的封禁。
埋点分级
埋点分级主要是针对离线存储成本进行优化,首先在流量平台上对埋点进行分级,埋点数据流 ETL 任务会将分级信息写入到埋点数据中。埋点数据在从 MQ Dump 到 HDFS 这个阶段根据这些分级的信息将埋点数据写入不同的 HDFS 分区路径下。然后通过不同的 Spark 任务消费不同分级分区的 HDFS 数据写入 Hive Table。不同等级的分区可以优先保障高优埋点的产出,另外不同分区也可以配置不同的 TTL,通过缩减低优数据的 TTL 节省了大量的存储资源。
未来规划
相关技术实践已经通过火山引擎数据中台产品对外输出,大家感兴趣的话也可以登陆火山引擎的官网进行了解。
立即跳转火山引擎大数据研发治理套件官网了解详情!
评论