前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >揭秘字节跳动埋点数据实时动态处理引擎(附源码)

揭秘字节跳动埋点数据实时动态处理引擎(附源码)

作者头像
公众号:大数据羊说
发布2021-08-19 14:31:55
2.6K1
发布2021-08-19 14:31:55
举报
文章被收录于专栏:大数据羊说大数据羊说

感谢您的关注 + 点赞 + 再看,对博主的肯定,会督促博主持续的输出更多的优质实战内容!!! ”

1.序篇-先说结论

宝贝们,还记得前几天博主去的火山引擎大数据场嘛,其中比较令大家感兴趣的就是最后一讲,字节一站式埋点平台的 flink 标准化清洗及拆流任务。

其中大家感觉比较流啤的就是的就是字节做到了:

  1. 不重启任务可以上下线新的拆流及清洗规则,所有的规则变更都不需要涉及到任务的重启。
  2. 清洗 udf,rpc 接口热加载

总的来说就是任务永不停,不可能停止的,好么,beiber。

★字节火山引擎 PPT。公众号回复 20210724 获取。 ”

6

★本文博主就主要介绍第一点,即做到规则动态变化,可以做到动态添加一个 sink kafka topic,动态删除一个 sink kafka topic,而不重启任务。相信能抛砖引玉,给大家一些启发。 ”

本文从以下几个章节详细介绍框架实现:

  1. 背景篇-为啥需要这么个框架
  2. 定义、目标篇-做这个框架的目标、预期效果是什么
  3. 难点剖析篇-此框架建设的难点、业界目前的实现
  4. 数据建设篇-框架具体方案设计
  5. 数据保障篇-框架的保障方案
  6. 总结与展望篇

2.背景篇-为啥需要这么个框架

首先来看看字节他们做这件事情的背景:

  1. 任务重启造成数据的延迟:对于字节这种企业来说且每天都会新上线很多的埋点,把这些新的埋点拆流条件加入 flink 任务就要重启,但是字节客户端日志流量都是千万级别 qps 的,就意味着这个 flink 任务一旦重启耗时肯定是很长的,这对时延敏感的业务是不可接受的。
  2. 减少对于原始客户端日志的烟囱式消费,节约资源
  3. 统一标准化的埋点平台:用户能通过埋点平台用到正确的数据
  4. 与埋点平台联动的、统一化的、标准化的流式数据处理平台:用户能通过这个平台去获取想要的统一标准化的数据
  5. 数据的分级保障能力:Dump 日志,日志的产出需要分优先级进行保障(死保、尽力保...),用户能放心的用数据

如图:

因此诞生了这个框架。

项目代码、在博主公众号回复【揭秘字节跳动埋点数据实时动态处理引擎】获取:

3.定义、目标篇-做这个框架的目标、预期效果是什么

上述的痛点很多,本节就从最痛的任务重启的延迟角度出发解决问题,揭秘字节动态配置化的 flink 任务的实现。

预期效果如下:

1.即在任务不停止的情况下可以动态的上线一个动态规则、一个 sink kafka topic,上线某个、某类埋点对应的流数据的 kafka topic

如图左边是修改配置,添加了一个拆流规则以及对应 topic,右边这个规则 topic 就开始产出数据,对应的 console consumer 就消费到了复合规则的数据。(gif 加载可能比较慢)

8

2.即在任务不停止的情况下可以动态的下线一个动态规则、一个 sink kafka topic,下线某个、某类埋点对应的流数据的 kafka topic

如图左边是修改配置,删除了一个拆流规则以及对应 topic,右边这个规则 topic 就不产出数据了,对应的 console consumer 就没有新数据可以消费了。(gif 加载可能比较慢)

9

3.总体效果如下:

4.难点剖析篇-此框‍架建设的难点、业界目前的实现

首先带大家分析下,实现这个框架,最基本的模块都需要包含什么:

  1. flink 任务:本身就是一个 Map 任务,逻辑简单
  2. 动态上下线规则配置:肯定得有一个动态配置中心去告诉 flink 任务需要新上下线一个 kafka topic
  3. 动态规则过滤引擎:flink 任务监听到规则发生动态变化之后,要热更新规则,将新的规律规则应用起来。需要一个动态代码执行引擎
  4. 动态上下线 Kafka topic:目前大多数公司用的是 flink 自带的 kafka-connector,一旦涉及到需要添加一个下游,就需要添加一个 kafka producer operator,因为涉及到多加了一个 operator,那肯定得重启任务。需要动态添加删除 producer 的能力。

5.数据建设篇-框架具体方案设计

5.1.方案设计

5.1.1.方案

先说说方案选择的结论:

  1. flink 入口任务:Map 模型使用 ProcessFunction 底层算子
  2. 动态上下线规则配置:配置中心开源的有很多,这里为了实现轻量化,实现简单,使用 zookeeper 作为动态规则配置中心。当然如果对 zk 压力大,也可以使用广播配置实现。
  3. 动态规则引擎:规则引擎很多,比如常见的可以使用 JavaScript、Groovy、jython、mvel2、freemarker 等等,太多了。考虑到性能、易用性选用 janino 将动态规则动态编译出 class。然后作为动态规则引擎使用。后面会详述选用 janino 的原因。
  4. 动态上下线 Kafka topic:去除 flink-kafka-connector,直接在 ProcessFunction 中使用原生 kafka-clients 输出数据,维护一个 producer 池。

整体方案架构图如图所示:

项目代码、在博主公众号回复【揭秘字节跳动埋点数据实时动态处理引擎】获取:

5.1.2.预期效果

5.1.2.1.上线配置

4

5.1.2.2.下线配置

5

5.2.具体实现

整个任务的实现非常简单。

本地运行,可以参考下面两篇安装 zk 和 kafka。

  • zk:https://www.jianshu.com/p/5491d16e6abd
  • kafka:https://www.jianshu.com/p/dd2578d47ff6

5.2.1.flink 任务入口逻辑

首先来看看整个任务的入口逻辑,ProcessFunction 的功能很简单:

  1. 针对数据源的每一条日志数据,遍历动态规则引擎池
  2. 只要这条数据满足某一条规则的条件,就将这条日志数据写出到规则对应的 topic 中
代码语言:javascript
复制
 env.addSource(new UserDefinedSource())
    .process(new ProcessFunction<ClientLogSource, ClientLogSink>() {
        // 动态规则配置中心
        private ZkBasedConfigCenter zkBasedConfigCenter;
        // kafka producer 管理中心
        private KafkaProducerCenter kafkaProducerCenter;

        @Override
        public void open(Configuration parameters) throws Exception {
            super.open(parameters);
            this.zkBasedConfigCenter = ZkBasedConfigCenter.getInstance();
            this.kafkaProducerCenter = KafkaProducerCenter.getInstance();
        }

        @Override
        public void processElement(ClientLogSource clientLogSource, Context context, Collector<ClientLogSink> collector)
                throws Exception {
            
            // 遍历所有的动态规则
            this.zkBasedConfigCenter.getMap().forEach(new BiConsumer<Long, DynamicProducerRule>() {
                @Override
                public void accept(Long id, DynamicProducerRule dynamicProducerRule) {
                    // 验证该条数据是否符合该条规则
                    if (dynamicProducerRule.eval(clientLogSource)) {
                        // 将符合规则的数据发向对应规则的 topic 中
                        kafkaProducerCenter.send(dynamicProducerRule.getTargetTopic(), clientLogSource.toString());
                    }

                }
            });
        }

        @Override
        public void close() throws Exception {
            super.close();
            // 关闭规则池
            this.zkBasedConfigCenter.close();
            // 关闭 producer 池
            this.kafkaProducerCenter.close();
        }
    });

env.execute();

5.2.2.动态上下线规则配置

来看 flink ProcessFunction 中的核心点,第一部分就是 ZkBasedConfigCenter。其功能包含:

  1. 任务启动时,初始化加载 zk 配置,初始化规则池,将规则池中的配置规则编译成 class 可执行规则
  2. 监听 zk 配置变更,将新增配置加入规则池,将下线配置从规则池删除
5.2.2.1.动态规则 schema 设计

动态规则包含的内容与用户需求息息相关:

举例:用户需要将在首页上报 + id > 300 用户的客户端日志都写入 topic_id_bigger_than_300_and_main_page 的 kafka topic 中。

那么针对这个 flink 任务来说就有以下三项用户的输入:

  1. 动态规则的过滤条件:即上游每一条数据过来之后检验这条数据是否满足规则条件。上面这个例子的条件就是 clientLogSource.getId() > 300 && clientLogSource.getPage().equals("首页");其中 clientLogSource 是原始日志 model
  2. 动态规则要写入的 topic 名称:这条规则过滤出来的数据要写入哪个 topic。上面这个例子的 topic 就是 topic_id_bigger_than_300_and_main_page
  3. 动态规则的唯一 id:唯一标识一个过滤规则的 id

针对上述要求设计动态规则配置的 schema 如下:

代码语言:javascript
复制
{
    "id-数值类型 string": {
  "condition-过滤条件": "1==1",
  "targetTopic-目标 topic 名称": "tuzisir1"
 }
 "1": {
  "condition": "clientLogSource.getId() > 300 && clientLogSource.getPage().equals(\"首页\")",
  "targetTopic": "topic_id_bigger_than_300_and_main_page"
 },
 "2": {
  "condition": "clientLogSource.getPage().equals(\"个人主页\")",
  "targetTopic": "topic_profile_page"
 }
}

对应动态规则 model 设计如下:

项目代码、在博主公众号回复【揭秘字节跳动埋点数据实时动态处理引擎】获取:

代码语言:javascript
复制
public class DynamicProducerRule implements Evaluable {

    // 具体过滤规则
    private String condition;

    // 具体写入 topic
    private String targetTopic;

    // 使用 janino 编译的规则过滤器
    private Evaluable evaluable;

    public void init(Long id) {
        try {
            // 使用 janino 初始化规则
            Class<Evaluable> clazz = JaninoUtils.genCodeAndGetClazz(id, targetTopic, condition);
            this.evaluable = clazz.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean eval(ClientLogSource clientLogSource) {
        return this.evaluable.eval(clientLogSource);
    }

}

重点在于 Evaluable 接口,动态生成代码就是继承了这个接口,用于执行过滤规则的基础接口。

代码动态生成下面会详细介绍。

代码语言:javascript
复制
public interface Evaluable {

    // 动态规则接口过滤方法
    boolean eval(ClientLogSource clientLogSource);

}
5.2.2.2.基于 zk 的动态配置中心

使用了 zk 作为动态配置中心,来动态监听规则配置以及更新规则池。

代码语言:javascript
复制
public class ZkBasedConfigCenter {
    
    // zk config 变化监听器
    private TreeCache treeCache;

    // zk 客户端
    private CuratorFramework zkClient;

    private ZkBasedConfigCenter() {
        try {
            open();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // !!!规则池!!!规则池!!!规则池
    private ConcurrentMap<Long, DynamicProducerRule> map = new ConcurrentHashMap<>();

    private void open() throws Exception {

        // 初始化规则
        // 初始化 zk config 监听器
        // 当有配置变更时
        // 调用 private void update(String json) 更新规则

    }

    public void close() {
        this.treeCache.close();
        this.zkClient.close();
    }

    private void update(String json) {

        Map<Long, DynamicProducerRule>
                result = getNewMap(json);

        // 1.将新增规则添加进规则池
        // 2.将下线规则从规则池删除
    }

    private Map<Long, DynamicProducerRule> getNewMap(String json) {
        // 将新规则解析,并使用 janino 进行初始化
    }
}

可以使用一个固定路径的配置,如图博主使用的是 /kafka-config 这个路径

7

5.2.3.动态规则引擎

目前字节使用的引擎是 Groovy,但是博主常用 flink sql,sql 中的代码生成是使用 janino 做的,因此就比较了 janino 和 groovy 的性能差异,janino 编译出的原生 class 性能接近原生 class,是 Groovy 的 4 倍左右。其他的引擎不考虑,要么易用性差,要么性能差。

★Notes:性能这一点真的是很重要,1:4 的差距可以说是差别很大了。如果你的场景也是大流量,非常耗费性能的场景,建议直接入手 janino!!! ”

来看看具体的 benchmark case 代码:

代码语言:javascript
复制
// ClientLogSource 是原始日志
boolean eval(flink.examples.datastream._01.bytedance.split.model.ClientLogSource clientLogSource) {
    return String.valueOf(clientLogSource.getId()).equals("1");
}

上面这段代码,在博主 mac 本地执行,每次循环执行 5kw 次,总计执行 5 次 得出的结果如下:

代码语言:javascript
复制
java:847 ms
janino:745 ms
groovy:4110 ms

java:1097 ms
janino:1170 ms
groovy:4052 ms

java:916 ms
janino:1117 ms
groovy:4311 ms

java:915 ms
janino:1112 ms
groovy:4382 ms

java:921 ms
janino:1104 ms
groovy:4321 ms

重复执行了很多次:java object : janino 编译原生 class :groovy :几乎都是 1:1:4 的耗时。所以此处我们选择性能更好的 janino。

代码语言:javascript
复制
public class JaninoUtils {

    public static Class<Evaluable> genCodeAndGetClazz(Long id, String topic, String condition) throws Exception {
        // 动态生成代码
        // 初始化 Class<Evaluable> 并返回
    }

}

5.2.4.动态上下线 Kafka topic

来看入口类中的第二个核心点,就是 KafkaProducerCenter。其功能包含:

  1. 维护所有的 producer 池
  2. 提供消息发送接口

项目代码、在博主公众号回复【揭秘字节跳动埋点数据实时动态处理引擎】获取:

代码语言:javascript
复制
public class KafkaProducerCenter {

    // kafka producer 池
    private final ConcurrentMap<String, Producer<String, String>> producerConcurrentMap
            = new ConcurrentHashMap<>();

    private Producer<String, String> getProducer(String topicName) {

        // 如果 kafka producer 池中有当前 topic 的 producer,则直接返回
        // 如果没有,则初始化一个新的 producer 然后返回

    }

    public void send(String topicName, String message) {

        final ProducerRecord<String, String> record = new ProducerRecord<>(topicName,
                "", message);
        try {
            RecordMetadata metadata = getProducer(topicName).send(record).get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void close() {
        // 关闭所有 producer 连接
    }
}

上面就是所有的代码、逻辑实现方案。其实整体看下来是非常简单的。

6. 数据保障篇-框架的保障方案

  1. 配置中心挂了怎么办?

为这个任务分配独立的队列资源,每当这个任务加载到最新配置时,都将配置在本地存储一份。当配置中心挂了的时候,还可以直接加载机器本地的配置,不至于什么都产出不了。

  1. 怎么保障用户的配置是无误的?
    • 上线前审批:有专门的埋点管理人员进行逻辑验证及管理
    • 上线前自动化测试:在埋点管理平台自动化验证逻辑正确性,保障上线到 flink 任务里的配置都是正确的
    • AOP 异常处理、报警:在环境中做 AOP 异常处理,将异常数据 dump 到专用异常 topic 中,也需要自动化把报警信息透出
    • 结果验证:针对最终的结果需要有数据准确性验证机制

7. 总结与展望篇

7.1.总结

本文主要揭秘、实现了字节跳动埋点数据实时动态处理引擎。

7.2.展望

  1. 本文主要实现了拆流的动态化,输出数据和输入数据完全相同,但是很多情况下,下游只需要其中的一些字段。因此之后还可以做到对于 sink message 字段、消息的个性化。比如可以加一个动态化的 Map 逻辑,将数据源中的 ClientLogSource 转化为任何用户想要的 Model。比如使用 Dynamic Message 或者使用代码生成去做。
  2. 目前过滤条件完全是 java 语法,之后可以扩展成为 sql 语法,提高可读性
  3. 函数、rpc 热加载
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 大数据羊说 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.序篇-先说结论
  • 2.背景篇-为啥需要这么个框架
  • 3.定义、目标篇-做这个框架的目标、预期效果是什么
  • 4.难点剖析篇-此框‍架建设的难点、业界目前的实现
  • 5.数据建设篇-框架具体方案设计
    • 5.1.方案设计
      • 5.1.1.方案
      • 5.1.2.预期效果
    • 5.2.具体实现
      • 5.2.1.flink 任务入口逻辑
      • 5.2.2.动态上下线规则配置
      • 5.2.3.动态规则引擎
      • 5.2.4.动态上下线 Kafka topic
  • 6. 数据保障篇-框架的保障方案
  • 7. 总结与展望篇
    • 7.1.总结
      • 7.2.展望
      相关产品与服务
      大数据
      全栈大数据产品,面向海量数据场景,帮助您 “智理无数,心中有数”!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档