SeaTunnel 2.1.3 任务执行流程源码解析

前言:

        最近因为公司业务需求,调研了一下SeaTunnel的工作原理,现在记录下来,也分享给大家共同学习。

一、SeaTunnel是啥,有什么用

        Apache SeaTunnel 是下一代高性能、分布式、海量数据集成框架。通过我们努力让Spark、Flink的使用更简单、更高效,将行业的优质经验和我们对Spar、Flinkk的使用固化到产品SeaTunnel中,显着降低学习成本,加速分布式数据处理能力的部署在生产环境中。

        更多的不再叙述,毕竟来看源码的人肯定知道这是个啥,放上官网:Apache SeaTunnel | Apache SeaTunnel

二、基于源码展示SeaTunnel工作原理

        我们以一个官方提供的flink-example来从源码展示SeaTunnel的执行流程。

        首先我们通过使用bin/start-seatunnel-flink.sh脚本来提交Flink任务,我们来看脚本中比较关键的两行内容:

SeaTunnel通过脚本去执行了seatunnel-core-flink.jar并且入口类为org.apache.seatunnel.core.flink.FlinkStarter,我们接下来移步源码来看这个FlinkStarter类。

public class FlinkStarter implements Starter {

    private static final String APP_NAME = SeatunnelFlink.class.getName();
    private static final String APP_JAR_NAME = "seatunnel-core-flink.jar";

    /**
     * SeaTunnel parameters, used by SeaTunnel application. e.g. `-c config.conf`
     */
    private final FlinkCommandArgs flinkCommandArgs;

    /**
     * SeaTunnel flink job jar.
     */
    private final String appJar;

    FlinkStarter(String[] args) {
        this.flinkCommandArgs = CommandLineUtils.parseCommandArgs(args, FlinkJobType.JAR);
        // set the deployment mode, used to get the job jar path.
        Common.setDeployMode(flinkCommandArgs.getDeployMode());
        Common.setStarter(true);
        this.appJar = Common.appLibDir().resolve(APP_JAR_NAME).toString();
    }

    @SuppressWarnings("checkstyle:RegexpSingleline")
    public static void main(String[] args) {
        FlinkStarter flinkStarter = new FlinkStarter(args);
        System.out.println(String.join(" ", flinkStarter.buildCommands()));
    }

    /**
     * 构建Flink执行命令。入口类为SeatunnelFlink
     * @return
     */
    @Override
    public List<String> buildCommands() {
        return CommandLineUtils.buildFlinkCommand(flinkCommandArgs, APP_NAME, appJar);
    }

}

在FlinkStarter类中,Seatunnel使用buildCommands()构建Flink提交任务命令,下面是buildCommands的实现:

   public static List<String> buildFlinkCommand(FlinkCommandArgs flinkCommandArgs, String className, String jarPath) {
        List<String> command = new ArrayList<>();
        command.add("${FLINK_HOME}/bin/flink");
        command.add(flinkCommandArgs.getRunMode().getMode());
        command.addAll(flinkCommandArgs.getFlinkParams());
        command.add("-c");
        command.add(className);
        command.add(jarPath);
        command.add("--config");
        command.add(flinkCommandArgs.getConfigFile());
        if (flinkCommandArgs.isCheckConfig()) {
            command.add("--check");
        }
        // set System properties
        flinkCommandArgs.getVariables().stream()
          .filter(Objects::nonNull)
          .map(String::trim)
          .forEach(variable -> command.add("-D" + variable));
        return command;

    }

        到这里,大概能推断出来SeaTunnel的执行逻辑:将自己作为一个Jar提交给Flink,在Flink执行时,以SeaTunnel为底座,根据用户编写的conf来装填对应的Source、Transform、Sink插件,最终拼好任务后提交给Flink去构建StreamGraph和JobGraph。我们继续往下看,来验证一下我们的想法。

       在上面的FlinkStarter类中,SeaTunnel将要执行的jar的main设置为SeatunnelFlink.class,那么我们去看看这个类。

public class SeatunnelFlink {

    public static void main(String[] args) throws CommandException {
        // 解析conf中的参数
        FlinkCommandArgs flinkCommandArgs = CommandLineUtils.parseCommandArgs(args, FlinkJobType.JAR);
        // 校验解析出的参数是否合法
        Command<FlinkCommandArgs> flinkCommand = new FlinkCommandBuilder()
            .buildCommand(flinkCommandArgs);
        // 执行
        Seatunnel.run(flinkCommand);
    }

}

        这个类也很简单,主要做了两件事,分别是校验参数,以及执行任务,我们再去看run方法:

    public static <T extends CommandArgs> void run(Command<T> command) throws CommandException {
        try {
            command.execute();
        } catch (ConfigRuntimeException e) {
            showConfigError(e);
            throw e;
        } catch (Exception e) {
            showFatalError(e);
            throw e;
        }
    }

同样很简单,我们去看execute方法

我们可以看到,execute方法有5个实现,第一个 FlinkApiConfValidateCommand用来校验配置参数是否合法,第二个FlinkApiTaskExecuteCommand是真正构建Flink作业的类,我们点进去再看

@Override
    public void execute() throws CommandExecuteException {
        // 配置计算引擎类型 Flink or Spark
        EngineType engine = flinkCommandArgs.getEngineType();
        Path configFile = FileUtils.getConfigPath(flinkCommandArgs);
        // 解析conf文件中的配置
        Config config = new ConfigBuilder(configFile).getConfig();
        // 基于上面解析的conf和计算引擎类型,构建作业入口,Flink中的env
        FlinkExecutionContext executionContext = new FlinkExecutionContext(config, engine);
        // 解析Source、Transform、Sink
        List<BaseSource<FlinkEnvironment>> sources = executionContext.getSources();
        List<BaseTransform<FlinkEnvironment>> transforms = executionContext.getTransforms();
        List<BaseSink<FlinkEnvironment>> sinks = executionContext.getSinks();

        // 检查上面解析出的插件类型        
        checkPluginType(executionContext.getJobMode(), sources, transforms, sinks);
        baseCheckConfig(sinks, transforms, sinks);
        showAsciiLogo();

        try (Execution<BaseSource<FlinkEnvironment>,
                BaseTransform<FlinkEnvironment>,
                BaseSink<FlinkEnvironment>,
                FlinkEnvironment> execution = new ExecutionFactory<>(executionContext).createExecution()) {
            // 插件的生命周期方法,插件注册后将调用该方法
            prepare(executionContext.getEnvironment(), sources, transforms, sinks);
            // 开始执行
            execution.start(sources, transforms, sinks);
            close(sources, transforms, sinks);
        } catch (Exception e) {
            throw new CommandExecuteException("Execute Flink task error", e);
        }
    }

这个方法中的主要工作为解析插件信息以及创建当前选择的计算引擎的程序入口,对应于Flink中的env,我们继续看start方法,我们选择流式处理的FlinkStreamExecution实现:

  @Override
    public void start(List<FlinkStreamSource> sources, List<FlinkStreamTransform> transforms, List<FlinkStreamSink> sinks) throws Exception {
        List<DataStream<Row>> data = new ArrayList<>();

        // 遍历用户选择的的Source插件
        for (FlinkStreamSource source : sources) {
            // 通过addSource.returns(RowTypeInfo)获取带有Schema的DataStream<Row>
            DataStream<Row> dataStream = source.getData(flinkEnvironment);
            data.add(dataStream);
            // 将当前带有Schema的DataStream<Row>注册为FlinkTable
            registerResultTable(source, dataStream);
        }

        DataStream<Row> input = data.get(0);

        // 遍历用户选择的Transform插件
        for (FlinkStreamTransform transform : transforms) {
            // 通过schema创建FlinkTable,再用FlinkTable生成带Schema的DataStream<Row>
            DataStream<Row> stream = fromSourceTable(transform.getConfig()).orElse(input);
            // 执行Transform组件逻辑
            input = transform.processStream(flinkEnvironment, stream);
            // 将处理后的数据注册为表
            registerResultTable(transform, input);
            transform.registerFunction(flinkEnvironment);
        }

        for (FlinkStreamSink sink : sinks) {
            // 拿到数据写如外部系统
            DataStream<Row> stream = fromSourceTable(sink.getConfig()).orElse(input);
            sink.outputStream(flinkEnvironment, stream);
        }
        try {
            LOGGER.info("Flink Execution Plan:{}", flinkEnvironment.getStreamExecutionEnvironment().getExecutionPlan());
            // 开始交由Flink执行
            flinkEnvironment.getStreamExecutionEnvironment().execute(flinkEnvironment.getJobName());
        } catch (Exception e) {
            LOGGER.warn("Flink with job name [{}] execute failed", flinkEnvironment.getJobName());
            throw e;
        }
    }

        到此任务构建完成,后续工作交由FLink去完成。我们来看上面这段代码中的一些重要方法,首先是如何从Source组建中拿到DataSource<Row>,我们以Kafka为例:

    @Override
    public DataStream<Row> getData(FlinkEnvironment env) {
        StreamTableEnvironment tableEnvironment = env.getStreamTableEnvironment();
        tableEnvironment
                .connect(getKafkaConnect())
                .withFormat(setFormat())
                .withSchema(getSchema())
                .inAppendMode()
                .createTemporaryTable(tableName);
        Table table = tableEnvironment.scan(tableName);
        return TableUtil.tableToDataStream(tableEnvironment, table, true);
    }

        KafkaSource 通过用户在conf文件中配置的Schema信息,将从Kafka读进来的流注册为FlinkTable,在将Table转换为带有Schema属性的DataStream<Row>。

        我们再来看Transform组件是如何读数据和写出处理完成后的数据的,首先是根据SourceTableName读根据指定父级Source传递进来的数据:


    private Optional<DataStream<Row>> fromSourceTable(Config pluginConfig) {
        // 如果配置了source_table_name
        if (pluginConfig.hasPath(SOURCE_TABLE_NAME)) {
            StreamTableEnvironment tableEnvironment = flinkEnvironment.getStreamTableEnvironment();
            // 通过Schema创建FlinkTable
            Table table = tableEnvironment.scan(pluginConfig.getString(SOURCE_TABLE_NAME));
            // 将FlinkTable转换为带Schema的DataStream<Row>
            return Optional.ofNullable(TableUtil.tableToDataStream(tableEnvironment, table, true));
        }
        return Optional.empty();
    }

然后根据ResultTableName将处理完的数据注册为Flink动态表:


    private void registerResultTable(Plugin<FlinkEnvironment> plugin, DataStream<Row> dataStream) {
        Config config = plugin.getConfig();
        // 如果配置了 result_table_name
        if (config.hasPath(RESULT_TABLE_NAME)) {
            String name = config.getString(RESULT_TABLE_NAME);
            StreamTableEnvironment tableEnvironment = flinkEnvironment.getStreamTableEnvironment();
            // 判断环境中是否存在 此表
            if (!TableUtil.tableExists(tableEnvironment, name)) {
                if (config.hasPath("field_name")) {
                    String fieldName = config.getString("field_name");
                    // 将表注册如Flink StreamTableEnvironment中
                    tableEnvironment.registerDataStream(name, dataStream, fieldName);
                } else {
                    tableEnvironment.registerDataStream(name, dataStream);
                }
            }
        }
    }

再来看Sink组件是如何将数据写入外部存储系统的,这里还是以Kafka为例,直接调用了env.addSink方法将流写出:

    @Override
    public void outputStream(FlinkEnvironment env, DataStream<Row> dataStream) {
        FlinkKafkaProducer<Row> rowFlinkKafkaProducer = new FlinkKafkaProducer<>(
                topic,
                JsonRowSerializationSchema.builder().withTypeInfo(dataStream.getType()).build(),
                kafkaParams,
                null,
                getSemanticEnum(semantic),
                FlinkKafkaProducer.DEFAULT_KAFKA_PRODUCERS_POOL_SIZE);
        dataStream.addSink(rowFlinkKafkaProducer);
    }

总结:

将执行逻辑画了个图,下面把图方上帮助理解:

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
为了安装Apache Seatunnel,您可以按照以下步骤进行操作: 1. 首先,您需要下载Apache Seatunnel的二进制文件。您可以在中找到下载链接。 2. 下载完成后,使用以下命令解压文件: ``` sudo tar -zxvf apache-seatunnel-incubating-2.1.3-bin.tar.gz ``` 3. 接下来,您需要配置环境变量。打开`/etc/profile`文件: ``` sudo vim /etc/profile ``` 4. 在文件末尾添加以下内容,并保存文件: ``` export SEATUNNEL_HOME=/usr/local/apache-seatunnel-incubating-2.1.3 export PATH=$PATH:${SEATUNNEL_HOME}/bin ``` 5. 让环境变量生效: ``` source /etc/profile ``` 现在,您已成功安装了Apache Seatunnel。您可以使用`start-seatunnel-spark.sh`命令提交Spark任务,或使用`start-seatunnel-flink.sh`命令提交Flink任务。 : https://join.slack.com/t/apacheseatunnel/shared_invite/zt-1kcxzyrxz-lKcF3BAyzHEmpcc4OSaCjQ : ``` #下载解压 sudo wget "apache-seatunnel-incubating-2.1.3-bin.tar.gz" sudo tar -zxvf apache-seatunnel-incubating-2.1.3-bin.tar.gz #配置环境变量 sudo vim /etc/profile export SEATUNNEL_HOME=/usr/local/apache-seatunnel-incubating-2.1.3 export PATH=$PATH:${SEATUNNEL_HOME}/bin #环境变量生效 source /etc/profile ``` : 提交Spark任务用`start-seatunnel-spark.sh`,提交Flink任务则用`start-seatunnel-flink.sh`。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [一文搞定 Apache SeaTunnel 2.3.1 全流程部署使用](https://blog.csdn.net/weixin_54625990/article/details/131099059)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [SeaTunnel安装及测试](https://blog.csdn.net/dwjlyl/article/details/128250253)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EdwardsWang丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值