首先我们定义一下埋点是什么?埋点主要是描述用户在 APP 内触发的一系列行为,包括点击、侧滑等。基于这些行为,我们可以进行行为分析、个性化推荐、精准营销等很多事情。埋点主要描述的是哪些数据?
- Who:谁操作的数据
- When:什么时候操作的数据
- Where:在哪些页面、模块的数据
- How:用户如何操作的
- What:有哪些附加信息
因为本文介绍的是埋点治理,所以这里再介绍一下什么是数据治理。数据治理是指在数据的生命周期内,对其进行管理的原则性方法,其目标是为了确保数据的安全、及时、准确、可用和易用。数据总是会变得无效甚至无用,因此就涉及到对存量数据的治理。但这里要强调一下,数据治理不只针对存量数据,更重要的是对增量数据的治理,通过一系列手段,能保证数据从源头开始就是正确的。此外,所有的治理都有具体的落地内容,一个稳定的治理链路是所有数据治理的基石。
下面就为大家介绍字节跳动是如何治理埋点数据的。
字节跳动流量平台
流量平台是字节跳动内部统一的埋点平台,覆盖埋点数据定义、采集、生产、应用、治理等埋点全生命周期。当前,流量平台已经覆盖了 2000 多个应用,管理埋点(事件)数 20 万,每天产生的埋点数据量超过万亿,每年能给公司节省的成本超亿元。
上图是字节跳动流量平台的产品概念图,可以看到流量平台主要分为几块:
- 埋点内容:这是用户接触最多的一块,包括埋点生命周期的设计、开发、验证、上线、使用、乃至下线。
- 埋点治理:指的是狭义的对存量数据的治理。涉及成本、SLA 等的治理。
- 链路侧:包含埋点的收集、处理和订阅的全链路,平台目前支持对 iOS、Android 等全端数据的收集。平台已经跟下游使用流量数据的应用进行打通,用户可以订阅数据。
- 链路根基:即自研的动态实时计算平台,也是整个平台的核心技术,它能够支撑起字节跳动万亿+的实时数据的处理。
埋点内容解决方案
埋点内容主要管理埋点生命周期,这里要着重强调一下上图中心位置的埋点模型其实非常重要,因为埋点模型设计的好坏直接影响到埋点的设计、开发、测试甚至使用。
埋点内容用户痛点
埋点内容的用户主要是有两大类:埋点消费者和埋点生产者。对于埋点消费者来说,存在如下痛点:
- 查找难度大:埋点数量非常多,找不到自己想要的埋点。
- 使用难度高:找到埋点之后,用户也很可能不清楚指标埋点口径。
- 埋点难信任:当数据不符合预期时,不确定埋点数据是否可用。
对于埋点生产者来说,也有一些痛点:
- 生产链路长:各方信息对齐、流程推动难度大;
- 模型落地难:不知如何设计、不知是否符合规范;
- 缺乏工具支持:设计、开发、测试纯白手起家。
那怎么去解决用户的这些痛点呢?首先我们要弄清楚埋点的第一站是什么。很多公司都有埋点系统,对于大部分公司而言,埋点的第一站是埋点录入。但是大家会发现,埋点录入并不是一切的源头,埋点设计才是。埋点设计是第一手的资料,根据埋点的设计文稿可以将用户的需求梳理得非常细致。而埋点录入是第二手甚至是第 N 手资料,录入的信息肯定会有丢失,并且只能进行一些基本的校验,满足基本的准确性。
其次,如果没有资产的辅助设计,每一个埋点录入都要从 0 到 1 去实现一遍。但是埋点设计通过资产辅助设计可以变得很简单。因此,我们认为埋点设计才是 the single source of truth,这是我们整体设计的核心。下面来看看用户如何在我们的系统设计埋点。
字节跳动流量平台的产品辅助设计基于灵活的模型支持、设计资产积累、设计辅助,可以方便每一个用户定义高质量数据,让用户愿意在系统进行设计和评审。
设计完埋点,我们在埋点开发上也有对应的工具来服务来发。下面是我们的一段演示:
当用户要设计埋点时,可以通过 ID 找到要开发的埋点,通过点击即可插入代码。同时,系统支持 VSCode 等主流编辑器,针对不同语言和代码风格自定义代码模版,还有类型校验、编辑切换等。
埋点测试
埋点测试比 QA 要难很多,看的是一串数字、类型的值等。在字节跳动流量平台系统中,可以依托埋点设计中的规则辅助测试,针对类型、取值、必填等自动验证,并且可以一键生成报告。
我们是怎么去做好测试这件事的呢?重点还是前面提到的做好埋点设计。只有设计周全,才能积攒足够的规则进行自动化测试,因此埋点设计方案非常重要。
埋点设计者会在方案设计时制定一系列的约束规则,我们会依托这些约束规则生成一系列相匹配的测试用例,并在测试过程中进行自动匹配、测试。
埋点测试时,测试者手机扫码即可将服务器和浏览器建立连接,在 App 上操作后,流量平台可以实时接收到对应的埋点数据。因为已经有测试用例,规则执行引擎便可以自动匹配执行并得到结果,再通过验证结果推送服务实时推送至浏览器。
埋点测试后,用户可以通过报告生成器可以一键生成报告,发送给 RD 进行修改或者 DA 进行验收。这样就完成了整个测试流程。
埋点存量治理
在生成了大量的埋点之后,我们需要进行存量埋点数据的治理,具体涉及 SLA、成本、合规以及数据质量等方面。为什么还要进行存量埋点数据的治理呢?我们有这样一些观点:
- 数据不一定都是重要的。因为业务都不一定总是重要的。
- 数据并不总是有用的。比如活动下线了,埋点就要下线,否则付出了成本却没有收益。
- 数据不一定总是合规的。随着数据隐私的重要性越来越高,合规要求也在不断更新变动。
对于存量埋点数据的治理,也有一些痛点。对于治理负责方来说,数据越来越多,而对数据的实时性要求却越来越高;随着数据量暴增,成本也急剧增加,SLA 等级越来越慢;用户隐私也越来越重要。对于治理实施方来说,他们可能不敢治理,不愿治理,也不懂治理。
我们梳理了用户的需求,发现是这样几层:
- 用户层:自动化识别埋点是否有用,流程化引导埋点的分级和下线。
- 统计层:做到数字化、货币化,治理大盘清晰可知,埋点成本准确无误。
- 甄别层:通过的血缘图谱,对实时统计、离线报表、行为分析、推荐系统等做实时决策。
- 执行层:从 APP 端到数仓全流程,强兜底。
- 链路层:需要高效、稳定、完整的链路方案解决治理难题。
针对这些需求,我们是怎么做的呢?
埋点分级/无用埋点甄别
埋点血缘和离线血缘抽取不太一样。离线血缘是点与点之间的血缘,但埋点血缘关注的是内容与点的血缘,它需要知道一张表的哪些行的信息有用。这是完全不同的一个领域,没有任何前人的经验可以借鉴,我们在埋点血缘做了几个方面的事情:
- 离线处理:通过 SQL 解析,计算埋点与离线表的血缘;
- 实时处理:利用埋点链路优势,掌握实时分流血缘关系;
- 即时分析:与行为分析、SQL 临时查询系统打通;
- 推荐系统:利用数据清洗链路优势,解耦推荐血缘。
在埋点分级上,我们以性能埋点为突破口,给予匹配的 SLA 和 TTL 配置。
埋点链路解决方案
下面着重介绍一下我们的埋点链路解决方案。首先我们还是来看下埋点链路用户的需求是什么。对于非技术的运营、分析师同学,他们需要清楚自己:
- 需要什么样的数据:是埋点数据还是展现数据?
- 需要哪些数据:最好能对不同来源不同时效性的数据进行可视化过滤、清洗。
- 需要在哪里用这些数据:是实时报表,还是行为分析,还是推荐?
埋点链路的挑战
在埋点链路设计上我们也遇到了一些挑战:
- 大数据量下的稳定性:埋点数据在字节跳动是核心数据,其稳定性非常重要。
- 低延迟实时处理:尤其对于推荐,实时性要求非常高。
分级构建+下线。这里分几块内容:
- 数据接入:我们提供全栈 SDK 接入,还在 SDK 内置了管控的机制,利用各个 APP 内终端计算的能力,大大节省成本;而且根据合规要求不能上报的埋点就可以直接在 SDK 端丢弃掉;
- 数据收集:数据收集一般是提供 HTTP 接口,将上报的数据存到消息队列。而埋点数据量特别大,于是我们进行了埋点聚合,将埋点的 Event 数据聚合成 Applog 数据一起上报。数据进入到 Applog 后通过自研的实时数据处理平台来解析。
实时动态处理引擎
上图是我们自研的实时数据平台架构,该平台主要解决两方面的问题:
- 实时处理:快速处理大量数据;
- 动态化:字节跳动服务规模巨大,重启时间过长会导致下游断流,因此我们的实时领域里不接受重启。此外,逻辑有任何修改都要重启,在大量的业务和逻辑下这是不可能实现的。所以该平台一定要做到动态化。
动态实时处理引擎在收到实时数据的 Applog 后将其解析成真正的埋点数据。再通过数据加工,可以转换为其他的(甚至自定义的)数据格式,最后通过数据订阅推送到各应用。
第二种模式是分级。在埋点数据被解析以后,我们会打上标记,然后 dump 到 hdfs 不同的路径下。后续 Hive 进行构建的时候可以区分优先级,优先级高的进入高优队列。Hive 的数据也是分区的,分区的数据可以制定不同的 TTL。这样,数据的 TTL 和 SLA 就都能分级了。
第三是强保证。在埋点数据下线前,先将要下线的数据分流到 pre-discard Hive 表中暂存 30 天。如果在这段时间里没有问题,30 天之后就可以直接下线。
现在,该引擎的处理逻辑、拓扑、函数以及 RPC 都可以做到动态化。用户对于上游而言,一般是写 SQL 或者进行界面化操作。因为用户不懂如何处理,我们就需要特定的模型让用户进行适配。于是我们用声明式表达建立统一的逻辑模型让用户直接适配。在引擎上我们还能以插件化的形式支持 Flink、Pyjstorm、TCE 等多种运行时平台,业务方基于视图表达可以定制化支持业务场景。
Map 计算模型
下面介绍下该引擎的逻辑动态性。我们使用的是简单的 map 模型。
数据进来后判断是否是需要的,过滤清洗之后需要的数据进入下游,不需要的数据就丢弃掉。基于 Groovy 语言的热加载,将语言转换成可执行的逻辑。
拓扑重构
字节跳动的业务新增速度很快,我们希望新增业务下游后也不需要重启。对于业务方来说,用户只关心业务逻辑,运维关心底层稳定性和 Job 执行效率。但是在实际处理中,一个大的困境在数据源。我们以 Kafka 为例,每多一个消费者就多一份网络消耗和数据反序列化的计算成本,对 Kafka 的压力就越大。我们应对的方法原理其实很简单,即基于源数据集来进行重构。
相同 SLA 下的业务线,只要用了相同的 source,就可以把拓扑重构为新的模型。拓扑重构之后,用户侧无感知,SLA 也没有打破,但是效率确实成倍提升,而且对于上游 Kafka 的压力小了许多。
实时动态处理引擎整体架构
我们希望这个引擎是一个会变形的引擎。上游用户可以通过 SQL、图形化/界面化配置,我们可以根据 schema 产生的 catalog 生成一个通用的自定义逻辑规则。之后用户还会对逻辑规则进行修改,比如进行校验或函数重构,我们再会转换成用户的物理规则(physical rule),我们现在是使用 Groovy 进行转换。转换成物理规则之后,还有其他一些问题要处理。
首先是动态化,包括:
- UDF 动态化:我们期望 UDF 改变也不用重启,所以 UDF 需要进行动态化编译。
- 拓扑重构动态化:重构之后拓扑改变,需要新的拓扑结构。
- RPC 动态化:可以加载动态的函数。
这些配置更新以后,经过 Planbuilder 生成 JobGraph,引擎再拉取配置。
这时也有一个问题:我们的规则非常多,不能因为一条规则的更改就更新所有规则。所以我们做的是增量更新,只对有需要的规则进行更新。
引擎拉取之后,会加载新的资源(RPC、Schema),并进行拓扑重构以及编译。因为之前给到的是一些 Groovy 的代码片段,用户可以将其热编译为物理规则。
此外我们还做了很多细致的工作,例如 Object catch。举个例子:大部分埋点上报的是 String 格式的 Json 数据,用户在进行数据清洗时就需要将 String 反序列化为 Json object,如果用户在规则中多次用到该 Json object 就会导致多次反序列化计算。因此,我们将反序列化后的 object 进行缓存,这样再次使用时就可以直接使用,避免重复反序列化成本。
Q&A
Q:新增埋点之后不用重启,那多久会生效,如何保证生效?
A:我们对用户承诺的 SLA 是 2 分钟生效。因为实时动态引擎是动态获取数据的过程,可以更高频地感知变化。在变化完成之后,我们是增量修改,修改的频率也更低。
如何能保证新增埋点生效?前面提到了重视 SLA。SLA 不一样,资源的利用率也就不一样。此外埋点也不可能无限加,当资源利用率达到一定的阈值之后,就需要扩容。资源不足的问题没有办法解决,只能重启。
Q:资源是先申请到位再重启吗?
A:在我们这边是的,资源申请到位后才会进行对应的重启。同时我们的开发套件会进行增量重启,也就是不会一次把所有服务节点全部重启,一次只会对有问题的部分(比如 10% 的服务节点)进行重启,把限度降到最低。但是在大数据量下,重启执行的运维成本依然很高。
Q:平台上的埋点开发代码模版可以复用吗?
A:一般不会复用。不同语言的模版肯定不同,不同产品的工程团队风格要求也不一样,也需要定制,所以模版几乎不会被复用。
Q:埋点的丢失和重复上报的问题是怎么处理的?
A:对于丢失数据的处理分两个方面:
端上日志:上报数据失败后会进行重试,并且端上有监控,可以了解当前客户端上报的情况。
服务端的丢失,我们也有对应的监控,这时候丢失有几种情况:
- 脏数据:没有通过判断校验逻辑的数据不会直接丢弃,而是分到一个 dirty 流,可以重新找回。
- 特殊逻辑:比如风控逻辑。
重复上报的问题很少遇到,更多的是重启之后重新消费 Kafka 的 offset 不是那么精准。但我们的引擎是动态的,不需要重启,就避免了这个问题。