刘石伟:字节跳动埋点数据流建设与治理实践
分享嘉宾:刘石伟 字节跳动
编辑整理:Rissy 易显智能科技
出品平台:DataFunTalk
导读: 埋点数据是数据分析、推荐、运营的基础,低延时、稳定、高效的埋点数据流对提高用户体验有着非常重要的作用。而随着流量的增大,埋点的增多,在大流量场景下,埋点数据流的建设和治理也面临不同的挑战。本文将介绍字节跳动在埋点数据流业务场景遇到的需求和挑战,以及为了应对这些需求和挑战在建设和治理过程中的具体实践。主要包含以下几个部分内容:
- 埋点数据流简介
- 埋点数据流建设实践
- 埋点数据流治理实践
- 未来规划
01 埋点数据流简介
1. 埋点数据流在字节
埋点数据流主要处理的数据是埋点,埋点也叫Event Tracking,是数据和业务之间的桥梁,也是数据分析、推荐、运营的基石。
用户在使用 App 、小程序、 Web 等各种线上应用时产生的行为数据主要通过埋点的形式进行采集上报,按不同的来源可以分为:
① 客户端埋点
② Web端埋点
③ 服务端埋点
埋点通过埋点收集服务接收到MQ,经过一系列的Flink实时ETL对埋点进行数据标准化、数据清洗、数据字段扩充、实时风控反作弊等处理,最终分发到不同的下游。下游主要包括推荐、广告、ABTest、行为分析系统、实时数仓、离线数仓等。因为埋点数据流处在整个数据处理链路的最上游,所以决定了“稳定性”是埋点数据流最为关注的一点。
2. 字节的埋点数据流规模
字节跳动埋点数据流的规模比较大,体现在以下几个方面:
① 接入的业务数量很多,包括抖音、今日头条、西瓜视频、番茄小说在内的多个大大小小的App和服务,都接入了埋点数据流。
② 流量很大,当前字节跳动埋点数据流峰值流量超过1亿每秒,每天处理超过万亿量级埋点,PB级数据存储增量。
③ ETL任务规模体量较大,在多个机房部署了超过1000个Flink任务和超过1000个MQ Topic,使用了超过50万Core CPU资源,单个任务最大超过12万Core CPU,单个MQ Topic最大达到10000个partition。
在这么巨大的流量和任务规模下,埋点数据流主要处理的是哪些问题?我们来看几个具体的业务场景。
3. 业务场景-UserAction ETL
在推荐场景中,由于埋点种类多、流量巨大,而推荐只关注其中部分埋点,因此需要通过UserAction ETL对埋点流进行处理,对这个场景来说 有两个需求点 :
① 数据流的时效性
② ETL规则动态更新
为了提升下流推荐系统的处理效率,我们在数据流配置ETL规则对推荐关注的埋点进行过滤,并对字段进行删减、映射、标准化等清洗处理,将埋点打上不同的动作类型标识 ,处理之后的埋点内部一般称为UserAction。UserAction与服务端展现、Feature等数据会在推荐Joiner任务的分钟级窗口中进行拼接处理,产出instance训练样本。
举个例子: 一个客户端的文章点赞埋点,描述了一个用户在某一个时间点对某一篇文章进行了点赞操作,这个埋点经过埋点收集服务进入ETL链路,通过UserAction ETL处理后,实时地进入推荐Joiner任务中拼接生成样本,更新推荐模型,从而提升用户的使用体验。
如果产出UserAction数据的ETL链路出现比较大的延迟,就不能在拼接窗口内及时地完成训练样本的拼接,可能会导致用户体验下降。因此, 对于推荐来说,数据流的时效性是比较强的需求 。而推荐模型的迭代和产品埋点的变动都可能导致UserAction ETL规则的变动,如果我们把这个ETL规则硬编码在代码中,每次修改都需要升级代码并重启相关的Flink ETL任务,这样会影响数据流的稳定性和数据的时效性,因此这个场景的另一个需求是ETL规则的动态更新。
4. 业务场景-数据分流
抖音的埋点Topic晚高峰超过一亿每秒,而下游电商、直播、短视频等不同业务关注的埋点都只是其中一部分。如果每个业务都分别使用一个Flink任务去消费抖音的全量埋点去过滤出自己关注的埋点,会消耗大量的计算资源,同时也会造成MQ集群带宽扇出非常严重,影响MQ集群的稳定性。
因此我们提供了数据分流服务。如何实现? 我们使用一个Flink任务去消费上游埋点Topic,通过在任务中配置分流规则的方式,将各个业务关注的埋点分流到下游的小Topic中提供给各业务消费,减少不必要的资源开销,同时也降低了MQ集群出带宽。
分流需求大多对SLA有一定要求,断流和数据延迟可能会影响下流的推荐效果、广告收入以及数据报表更新等。另外随着业务的发展,实时数据需求日益增加,分流规则新增和修改变得非常频繁,如果每次规则变动都需要修改代码和重启任务会对下游造成较大影响,因此在数据分流这个场景,规则的动态更新也是比较强的需求。
5. 业务场景-容灾降级
另一个场景是容灾降级。数据流容灾首先考虑的是防止单个机房级别的故障导致埋点数据流完全不可用,因此埋点数据流需要支持多机房的容灾部署。其次当出现机房级别的故障时,需要将故障机房的流量快速调度到可用机房实现服务的容灾恢复,因此需要埋点数据流具备机房间快速切流的能力。
而数据流降级主要考虑的是埋点数据流容量不足以承载全部流量的场景,比如春晚活动、电商大促这类有较大突发流量的场景。为了保障链路的稳定性和可用性,需要服务具备主动或者被动的降级能力。
6. 埋点数据流遇到的挑战
挑战主要是流量大和业务多导致的。流量大服务规模就大,不仅会导致成本治理的问题,还会带来单机故障多、性能瓶颈等因素引发的稳定性问题。而下游业务多、需求变化频繁,推荐、广告、实时数仓等下游业务对稳定性和实时性都有比较高的要求。
在流量大、业务多这样的背景下,如何保障埋点数据流稳定性的同时降低成本、提高效率,是埋点数据流稳定性治理和成本治理面对的挑战。
02 埋点数据流建设实践
上文我们了解了埋点数据流的业务场景和面对的挑战,接下来会介绍埋点数据流在ETL链路建设和容灾与降级能力上的一些实践。
1. 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链路建设的一些具体实践。
2. 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中。
3. ETL链路建设-规则引擎
规则引擎为埋点数据流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倍。
除了规则引擎的迭代,我们在平台侧的测试发布和监控方面也做了很多建设 。测试发布环节支持了规则的线下测试、线上调试,以及灰度发布的功能。监控环节支持了字段、规则、任务等不同粒度的异常监控,如规则的流量波动报警、任务的资源报警等。
4. ETL链路建设-Flink拆分任务
规则引擎的应用解决了埋点数据流ETL链路如何快速响应业务需求的问题,实现了ETL规则的动态更新,从而修改ETL规则不需要修改代码和重启任务。
但规则引擎本身的迭代、流量增长导致的资源扩容等场景,还是需要升级重启Flink任务,导致下游断流。
除了重启断流外,大任务还可能在重启时遇到启动慢、队列资源不足或者资源碎片导致起不来等情况。
针对这些痛点我们上线了Flink拆分任务,本质上是将一个大任务拆分为一组子任务,每个子任务按比例去消费上游Topic的部分Partition,按相同的逻辑处理后再分别写出到下游Topic。
举个例子:上游Topic有200个Partition,我们在一站式开发平台上去配置Flink拆分任务时只需要指定每个子任务的流量比例,每个子任务就能自动计算出它需要消费的topic partition区间,其余参数也支持按流量比例自动调整。
拆分任务的应用使得数据流除了规则粒度的灰度发布能力之外,还具备了Job粒度的灰度发布能力,升级扩容的时候不会发生断流,上线的风险更可控。同时由于拆分任务的各子任务是独立的,因此单个子任务出现反压、Failover对下游的影响更小。另一个优点是,单个子任务的资源使用量更小,资源可以同时在多个队列进行灵活的部署。
5. 容灾与降级能力建设
说到ETL链路建设,埋点数据流在容灾与降级能力建设方面也进行了一些实践。
首先是容灾能力的建设 ,埋点数据流在Flink, MQ, Yarn, HDFS等组件支持多机房容灾的基础上完成了多机房容灾部署,并准备了多种流量调度的预案。
正常情况下流量会均匀打到多个机房,MQ在多个机房间同步,Flink ETL Job默认从本地MQ进行消费,如果某个机房出现故障,我们根据情况可以选择通过配置下发的方式从客户端将流量调度到其他非受灾机房,也可以在CDN侧将流量调度到其他非受灾机房。埋点数据流ETL链路可以分钟级地进入容灾模式,迅速将故障机房的Flink Job切换到可用的机房。
其次是服务降级能力的建设 ,主要包含服务端降级策略和客户端降级策略。服务端降级策略主要通过LB限流、客户端进行退避重试的机制来实现,客户端降级策略通过配置下发可以降低埋点的上报频率。
举个例子:在春晚活动中参与的用户很多,口播期间更是有着非常巨大的流量洪峰,2021年春晚活动期间为了应对口播期间的流量洪峰,埋点数据流开启了客户端的降级策略,动态降低了一定比例用户的埋点上报频率,在口播期间不上报,口播结束后迅速恢复上报。在降级场景下,下游的指标计算是通过消费未降级用户上报的埋点去估算整体指标。目前我们在此基础上进行了优化,客户端目前的降级策略可以更近一步地根据埋点的分级信息去保障高优的埋点不降级,这样可以在活动场景下保障活动相关的埋点不降级的上报,支持下游指标的准确计算。
03 埋点数据流治理实践
介绍完埋点数据流建设的实践,接下来给大家分享的是埋点数据流治理方面的一些实践。埋点数据流治理包含多个治理领域,比如稳定性、成本、埋点质量等,每个治理领域下面又有很多具体的治理项目。
比如在稳定性治理中 我们通过优化减少了由于单机问题、MQ性能问题和混布问题等导致的各种稳定性问题;
成本治理方面 ,我们通过组件选型、性能优化、埋点治理等方式取得了显著降本增效的成果;
埋点质量治理方面 ,我们对脏数据问题、埋点字段类型错误问题和埋点数据的丢失重复问题进行了监控和治理。
这次我们主要选取了其中部分治理项目和大家分享。
1. 单机问题优化-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%。
2. 单机问题优化-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调度到这些节点。
3. 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集群的压力。
4. MQ优化-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%的资源收益。
5. 成本治理-埋点治理
在埋点治理方面,通过对流量平台的建设,提供了从埋点设计、埋点注册、埋点验证、埋点上报、埋点采样、流式ETL处理,再到埋点下线的埋点全生命周期的管理能力。
6. 成本治理-埋点管控
目前字节跳动所有的产品都开启了埋点管控。所有的埋点都需要在我们的流量平台上注册埋点元数据之后才能上报。而我们的埋点数据流ETL也只会处理已经注册的埋点,这是从埋点接入流程上进行的管控。
在埋点上报环节,通过在流量平台配置埋点的采样率对指定的埋点进行采样上报,在一些不需要统计全量埋点的场景能显著地降低埋点的上报量。
对于已经上报的埋点,通过埋点血缘统计出已经没有在使用的埋点,自动通知埋点负责人在流量平台进行自助下线。埋点下线流程完成后会通过服务端动态下发配置到埋点SDK以及埋点数据流ETL任务中,确保未注册的埋点在上报或者ETL环节被丢弃掉。还支持通过埋点黑名单的方式对一些异常的埋点进行动态的封禁。
7. 埋点治理-埋点分级
埋点分级主要是针对离线存储成本进行优化,首先在流量平台上对埋点进行分级,埋点数据流ETL任务会将分级信息写入到埋点数据中。埋点数据在从MQ Dump到HDFS这个阶段根据这些分级的信息将埋点数据写入不同的HDFS分区路径下。然后通过不同的Spark任务消费不同分级分区的HDFS数据写入Hive Table。不同等级的分区可以优先保障高优埋点的产出,另外不同分区也可以配置不同的TTL,通过缩减低优数据的TTL节省了大量的存储资源。
04 未来规划
目前Flink能做到计算层面的流批一体,但计算和存储的流批一体还在探索阶段,接下来我们也会继续关注社区的进展。另外我们会尝试探索一些云原生的实时数据处理框架,尝试解决资源动态rescale的问题,以此来提升资源利用率。最后是在一些高优链路上,我们希望保障更高的SLA,比如端到端的exactly-once语义。
相关技术实践已经通过火山引擎数据中台产品对外输出,大家感兴趣的话也可以登陆火山引擎的官网进行了解。
最后打个小广告这是我们字节跳动数据平台的官方公众号,我们会在上面分享我们数据平台的技术干货、产品动态和招聘信息。
05 精彩问答
Q:业务方查不到自己的埋点数据或者和业务系统的埋点数据对比UV不对,字节跳动是否会遇到这种情况,你们排查的思路是怎样的?
A:在埋点治理环节有讲到,在字节跳动我们把埋点的整个生命周期都管理起来了,从埋点设计开始,到埋点先注册后上报的管控流程。在埋点上报之前需要通过平台提供的埋点验证功能验证埋点是否上报成功以及埋点字段跟它注册的元数据是否是匹配的。我们通过这样的流程可以很大程度上减少埋点上报缺失和埋点质量问题。
Q:中间队列用MQ不用Kafka是出于什么样的考虑?
A:如果是问我们为什么从Kafka切换到BMQ,可以了解一下Kafka和一些存储计算分离MQ的对比如Apache Pulsar,通常来说存储计算分离架构的MQ会具备更好的伸缩性以及不会因为数据复制带来读写性能的下降,成本也会更低,会更贴近云原生的形态。
Q:埋点的数据质量治理还有什么好的例子吗?
A:我们现在做的比较多的是埋点上报前的埋点质量治理,比如刚才提到了在上报前通过注册埋点的信息开发完埋点后用验证工具去做自动化测试以保障埋点开发的准确性和质量,上报后我们也会用一些离线工具对埋点质量进行监控,但从我们的经验上来看,上报前埋点质量的把控会解决大部分的问题,上报后的埋点质量治理目前主要是离线的,也能解决一些场景的问题。
Q:埋点数据准确性怎么保障的?埋点数据处理的过程中是给到一个什么样级别的保障?
A:目前我们的处理链路是基于Flink实现的,大多数场景是没有开启checkpoint,原因是我们的下游需求很多样化,有的下游不接受数据的大量重复,有的下游不接受数据延迟,如果开checkpoint,会在任务Failover过程中有大量的数据重复和延迟,这个是下游没有办法接受的。我们通过SLA数据质量监控指标是可以观测出整个链路各个环节的数据丢失率和重复率,通过SLA指标来反映和保障数据准确性。
分享嘉宾: