部署手册-SeaTunnel一站式部署

94 阅读16分钟

SeaTunnel

SeaTunnel:易用、高性能、支持实时流式和离线批处理的海量数据集成平台

简介

1. 什么是SeaTunnel

SeaTunnel是一个非常易用、高性能、支持实时流式和离线批处理的海量数据集成平台,架构于

SeaTunnel专注于数据集成和数据同步,主要解决数据集成领域的常见问题:

  • 数据源多样:常用的数据源有数百种,版本不兼容。随着新技术的出现,出现了更多的数据源。用户很难找到能够全面快速支持这些数据源的工具。

  • 复杂同步场景:数据同步需要支持离线-全量同步、离线-增量同步、CDC、实时同步、全库同步等多种同步场景。

  • 资源需求高:现有的数据集成和数据同步工具往往需要大量的计算资源或JDBC连接资源来完成海量小表的实时同步。这在一定程度上加重了企业的负担。

  • 缺乏质量和监控:数据集成和同步过程经常会丢失或重复数据。同步过程缺乏监控,无法直观了解任务过程中数据的真实情况。

  • 技术栈复杂:企业使用的技术组件各不相同,用户需要针对不同的组件开发相应的同步程序来完成数据集成。

  • 管理维护困难:受限于不同的底层技术组件(Flink/Spark),离线同步和实时同步往往是分开开发和管理的,增加了管理和维护的难度。

2. 系统架构与工作流程

系统架构

WEBRESOURCE6b4d809a8bb2cc4bdf207b8eec8f26b6image.png

上图为 SeaTunnel 的整个工作流程,数据处理流水线由多个过滤器构成,以满足多种数据处理需求。如果用户习惯了 SQL,也可以直接使用 SQL 构建数据处理管道,更加简单高效。目前,SeaTunnel 支持的过滤器列表也在扩展中。

  • 丰富且可扩展的Connector:SeaTunnel提供了不依赖于特定执行引擎的Connector API。基于此API开发的连接器(Source、Transform、Sink)可以运行在很多不同的引擎上,比如目前支持的SeaTunnel Engine、Flink、Spark。

  • Connector插件:插件式的设计让用户可以很方便的开发自己的Connector,并集成到SeaTunnel项目中。目前,SeaTunnel 已支持 100 多个 Connector,而且数量还在激增。

  • 批流融合:基于SeaTunnel Connector API开发的Connector,完美兼容离线同步、实时同步、全量同步、增量同步等场景。大大降低了管理数据集成任务的难度。

  • 支持分布式快照算法,保证数据一致性。

  • 多引擎支持:SeaTunnel默认使用SeaTunnel Engine进行数据同步。同时,SeaTunnel也支持使用Flink或Spark作为Connector的执行引擎,以适配企业现有的技术组件。SeaTunnel 支持多个版本的 Spark 和 Flink。

  • JDBC多路复用,数据库日志多表解析:SeaTunnel支持多表或全库同步,解决了JDBC连接过多的问题;支持多表或全库日志读取和解析,解决了CDC多表同步场景需要重复读取和解析日志的问题。

  • 高吞吐低延迟:SeaTunnel支持并行读写,提供高吞吐低延迟稳定可靠的数据同步能力。

  • 完善的实时监控:SeaTunnel支持数据同步过程中每一步的详细监控信息,让用户轻松了解同步任务读写的数据量、数据大小、QPS等信息。

  • 支持两种作业开发方式:编码和画布设计:提供了作业的可视化管理、调度、运行和监控能力。

工作架构

工作架构图如下:

WEBRESOURCEa80be87d8f1f2b34c7bae1117c4792d2image.png

  • 用户配置作业信息,选择执行引擎提交作业。

  • Source Connector负责并行读取数据并将数据发送给下游Transform或直接发送给Sink,Sink将数据写入目的地。值得注意的是,无论是Source还是Transform和Sink,都可以很方便的自行开发扩展。

  • SeaTunnel 是一个 EL(T) 数据集成平台。因此,在SeaTunnel中,Transform只能用于对数据进行一些简单的转换,例如将某列的数据转换为大写或小写,更改列名,或者将一列拆分为多列。

  • SeaTunnel 使用的默认引擎是SeaTunnel Engine。如果您选择使用Flink或Spark引擎,SeaTunnel会将Connector打包成Flink或Spark程序提交给Flink或Spark运行。

  • Source Connectors SeaTunnel 支持从各种关系数据库、图形数据库、NoSQL 数据库、文档数据库和内存数据库中读取数据。HDFS等各种分布式文件系统。S3、OSS等多种云存储。同时我们也支持很多常见的SaaS服务的数据读取。

  • 转换连接器如果源和接收器之间的架构不同,您可以使用转换连接器更改从源读取的架构,使其与接收器架构相同。

  • Sink Connector SeaTunnel 支持向各种关系数据库、图数据库、NoSQL 数据库、文档数据库和内存数据库写入数据。HDFS等各种分布式文件系统。S3、OSS等多种云存储。同时我们也支持向很多常见的SaaS服务写入数据。

初始构建

1. 安装Java

需要提供jdk1.8以上的版本环境

2. 下载SeaTunel

# 执行如下命令,速度可能比较缓慢
wget -b "https://archive.apache.org/dist/incubator/seatunnel/2.3.1/apache-seatunnel-incubating-2.3.1-bin.tar.gz"
# 下载完毕后,进行解压缩
tar -zxvf apache-seatunnel-incubating-2.3.1-bin.tar.gz

3. 安装连接器

从2.2.0-beta开始,二进制包默认不提供connector依赖,所以第一次使用时,我们需要执行如下命令安装connector:(当然你也可以手动下载connector从repo.maven.apache.org/maven2/org/…下载,然后手动移动到connectors/seatunnel目录.

# 此方法太慢了,建议从本地网上直接下载来所有的包
sh bin/install-plugin.sh 2.3.1

通常你不需要所有的连接器插件,所以你可以通过配置指定你需要的插件config/plugin_config,比如你只需要connector-console插件,那么你可以修改plugin_config为

--connectors-v2--
connector-console
--end--

如果你想让示例应用程序正常工作,你需要添加以下插件

--connectors-v2--
connector-fake
connector-console
--end--

注意点:需要在环境变量中配置seatunnel的全局环境变量。

您可以在${SEATUNNEL_HOME}/connectors/plugins-mapping.properties下找到所有支持的连接器和相应的 plugin_config 配置名称。

提示:

如果想通过手动下载connector的方式安装connector插件,需要特别注意以下几点

connectors目录包含以下子目录,如果不存在,需要手动创建

seatunnel

如果想手动安装V2 connector插件,只需要下载自己需要的V2 connector插件,放到seatunnel目录下即可。

下载完毕好的connector插件,需要复制到lib文件夹下。

4. 快速启动作业

添加作业配置文件以定义

编辑config/v2.batch.config.template,决定了seatunnel启动后数据输入、处理、输出的方式和逻辑。下面是一个配置文件的例子,和上面提到的例子应用是一样的。

env {
  execution.parallelism = 1
  job.mode = "BATCH"
}

source {
    FakeSource {
      result_table_name = "fake"
      row.num = 16
      schema = {
        fields {
          name = "string"
          age = "int"
        }
      }
    }
}

sink {
  Console {}
}
运行SeaTunnel

可以通过以下命令启动应用程序

cd "apache-seatunnel-incubating-${version}"
./bin/seatunnel.sh --config ./config/v2.batch.config.template -e local

查看输出:运行命令时,您可以在控制台中看到它的输出。您可以认为这是命令运行成功与否的标志。

需要注意的是:当第一次启动的官方的例子的时候,出现错误,找不到hadoop相关的包,追踪原因,发现lib下缺失包需要下载。

转存失败,建议直接上传图片文件

5. 构建Seatunnel服务引擎

在本地模式下启动成后,检测认证seatunnel运行正常,但是只能提交一次任务执行,不符合当前使用场景,根据相关官方文档的参考,需要启动setunnel服务。

# 使用命令
# -d 后台运行
sh /bin/seatunnel-cluster.sh -d

参考日志

WEBRESOURCE9d673c31d426433472a37aa543794ce9image.png

出现以上信息后,可以认为服务启动成功,也可以使用jps能够显示出seatunnel Server的进程。

基础概念

配置文件

在SeaTunnel中,最重要的是Config文件,通过它用户可以自定义自己的数据 同步要求,以最大限度地发挥SeaTunnel的潜力。

Config文件的主要格式是

一个完整的SeaTunnel配置文件应包含四个配置文件,分别是:

env{} source{} transform{} sink{}

env {
  job.mode = "BATCH" # 设置配置可选参数
}
-- 定义了数据源信息,可以同时定义多个数据源,每一个数据源有自己的特定参数定义如何获取
source { 
  FakeSource {
    result_table_name = "fake" -- 注冊為其他组件可识别的数据集
    -- 插件内置参数
    row.num = 100
    schema = {
      fields {
        name = "string"
        age = "int"
        card = "int"
      }
    }
  }
}

transform {
  Filter {
    source_table_name = "fake" -- 使用哪个数据集
    result_table_name = "fake1"
    fields = [name, card]
  }
}

sink {
  Clickhouse {
    host = "clickhouse:8123"
    database = "default"
    table = "seatunnel_console"
    fields = ["name", "card"]
    username = "default"
    password = ""
    source_table_name = "fake1"
  }
}

我们使用

result_table_name

source_table_name

env块

env块中可以直接写支持的配置项信息,比如并行度、检查点间隔时间等。

env {
    job.mode="BATCH"  #作业的运行模式,BATCH=离线批同步,STREAMING=实时同步
    job.name="SeaTunnel_Job"
    checkpoint.interval=10000 #每10000ms进行一次checkpoint
}

具体的详情细节可以观看ConfigKeyName类中的定义项。

WEBRESOURCE746d273e350ca96fa7dc8ddfe35d48cdimage.png

Row数据结构

Row是seaTunnel中数据传递的核心数据结构。

WEBRESOURCE95eeec07db9428b686616e9615b4e4d01706514360685.png

因为DataStream可以很方便地和Table进行互转,所以将Row当作核心数据结构可以让转换插件同时具有使用代码(命令式)和sql(声明式)处理数据的能力。

Source块

source块是用来声明数据源的。source块中可以声明多个连接器。

所有的source插件中都可以声明result_table_name。如果你声明了result_table_name。SeaTunnel会将source插件输出的DataStream转换为Table并注册在Table环境中。当你指定了result_table_name,那么你还可以指定field_name,在注册时,给Table重设字段名。

TransForm块

transform{}块中可以声明多个转换插件。所有的转换插件都可以使用source_table_name,和result_table_name。同样,如果我们声明了result_table_name,那么我们就能声明field_name。

具体的使用说明见下文或官方文档

sink块

Sink块里可以声明多个sink插件,每个sink插件都可以指定source_table_name。不过因为不同Sink插件的配置差异较大,所以在实现时建议参考官方文档。

工作流程

WEBRESOURCE5e4154e05b98d54eebf22ebce6c859ffimage.png

WEBRESOURCE6794f9591666845957ff107ad8a18b8aimage.png

seaTunnel-web

简介

由于SeaTunnel Web使用

初始构建

准备工作

1. 准备安装包

从apache官方中下载seatunnel.incubator.apache.org/download

WEBRESOURCE49c7308edb5df9ea484fadbebf9029d3image.png

WEBRESOURCE2086cc5cfc5352696a5274a5bd19c160image.png

2. 修改配置文件

将script/seatunnel_server_env.sh的相关配置改为你对应的配置信息。

WEBRESOURCE352cc5cfbe06db73eb9db4a8ea4b163aimage.png

由于默认的变量名称容易与系统环境变量产生冲突,加上前缀STWEB_。该完毕之后,需要更改init.sql中的用到此变量的名称。

需要注意的是:我这边采用的是手动导入的方式,当前服务器并未配置mysql客户端,命令无法执行。

将seatunnel_server_mysql.sql导出,到所在的数据库客户端进行数据库初始化操作。

3. 修改后端配置

打开conf/application.yml修改端口号和数据源信息

WEBRESOURCE08b4d1925c5f1c8397d07b6609ad03a6image.png

4. 配置插件信息

复制seatunnel的配置文件,

cp work/soft/seatunnel/apache-seatunnel-2.3.3/config/hazelcast-client.yaml conf/
cp work/soft/seatunnel/apache-seatunnel-2.3.3/connnector/plugin-mapping.properties conf/

5. 安装适应jar包

  1. 需要下载mysql的元数据驱动包

  2. 需要下载相关数据源的datasource-xx.jar

可以前往maven的中央仓库下载repo1.maven.org/maven2/org/…,不过速度超级慢。可以选择阿里的国内镜像。

不过如果想实现自动下载的话,可以拉下源码然后运行。

WEBRESOURCEbdafa71456f892d882ede80bbac454a3image.png

不过可以直接构建seatunnel与web的源码,从本地maven仓库中获取到jar包。

将相关的datasource的包导入到libs中,

需要注意的是:还需要将元数据与数据源的jar包复制到work/soft/seatunnel/apache-seatunnel-2.3.3/lib,否则任务将会不执行。

启动任务

进入到安装目录下,执行./bin/

注意:一定是在安装目录下,否则启动项目后,会出现页面404情况。

登录进入到192.168.57.128:8801页面

WEBRESOURCE32f7b50dc22142bfb92fadc897a39152image.png 点击数据源信息,点击创建

WEBRESOURCEcc28b99eac2cf66262cc87def8084f45image.png

会出现以下我们之前放好的插件信息:

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件

WEBRESOURCE7c755d1df77b8f93dc6dda4b2ff83957image.png 情况一:如果没有出现:可能是libs文件夹中无配置好,重新放置新的datasource插件到文件夹中重新启动。

创建完毕数据源信息后,点击任务,创建同步任务

WEBRESOURCEe67dd9a8ad2d89d045e03bed6d4f7170image.png

情况二:如果下拉框中出现无数据,可以重点关注日志,我这边的情况是因为我并未控制seatunnel_home的环境变量。

WEBRESOURCE203e17ead752a2a5611a0a064e4fa1421706436202405.png

启动任务

因为测试,将role表复制了一份空表,将role表离线同步到备份表中。

WEBRESOURCEfae2e59ff71e2d2dd747e3de909897f81706436260335.png

开始使用

1. 大数据量本地传输

一万条场景

使用场景:源头表导入一万条数据,插入到另外的数据库中备份表

准备数据源:

USE mydb;
SET @i = 1;
WHILE @i <= 10000 DO
    INSERT INTO mytable (name, age, addressVALUES ('John Doe', 25,'123 Main st');
    SET @i = @i + 1;
END WHILE:

任务执行情况:

十万条场景

2. 流式传输数据自动同步

3. 内置转化插件

copy插件
env {
  execution.parallelism = 2
  job.mode = "BATCH"
}
source {
    Jdbc {
        url = "jdbc:mysql://127.0.0.1:3306/test"
        driver = "com.mysql.cj.jdbc.Driver"
        connection_check_timeout_sec = 100
        user = "user"
        password = "password"
        result_table_name = "base_region_01"
        query = "select * from base_region limit 4"
    }
}
 
transform {
  Copy {
    source_table_name = "base_region_01"
    result_table_name = "base_region_02"
    -- 映射的字段名称
    fields {
      id = id
      region_name = region_name
      region_name2 = region_name
    }
  }
}
 
sink {
  jdbc {
    url = "jdbc:mysql://127.0.0.1:3306/dw"
    driver = "com.mysql.cj.jdbc.Driver"
    user = "user"
    password = "password"
    source_table_name = "base_region_02"
    query = "insert into base_region(id,region_name,region_name2) values(?,?,?)"
  }
}
Filter插件
env {
  execution.parallelism = 2
  job.mode = "BATCH"
}
source {
    Jdbc {
        url = "jdbc:mysql://127.0.0.1:3306/test"
        driver = "com.mysql.cj.jdbc.Driver"
        connection_check_timeout_sec = 100
        user = "user"
        password = "password"
        result_table_name = "t_user_01"
        query = "select * from t_user"
    }
}
 
transform {
  Filter {
    source_table_name = "t_user_01"
    result_table_name = "t_user_02"
    fields = [id, name]
  }
}
 
sink {
  jdbc {
        url = "jdbc:mysql://127.0.0.1:3306/test"
        driver = "com.mysql.cj.jdbc.Driver"
        connection_check_timeout_sec = 100
        user = "user"
        password = "password"
        source_table_name = "t_user_02"
        query = "insert into ods_t_user(id,name) values(?,?)"
  }
}
FilterSelector插件:
transform {
  FieldSelector {
    fields = ["id", "name", "age"] -- 只选择数据源信息中这些字段进行后续处理
  }
}
FieldMapper插件:
transform {
  FieldMapper {
    mappings {
      source_field = "source_value"
      target_field = "target_value"
    }
  }
}
DataFilter插件
transform {
  DataFilter {
    condition = "age >= 18" // 只需要年龄大于18的数据
  }
}
TypeConverter插件
transform {
  TypeConverter {
    field_conversion {
      name {
        from = "string"
        to = "integer"
      }
      age {
        from = "string"
        to = "integer"
      }
    }
  }
}
Replace插件
env {
  execution.parallelism = 2
  job.mode = "BATCH"
}
source {
    Jdbc {
        url = "jdbc:mysql://127.0.0.1:3306/test"
        driver = "com.mysql.cj.jdbc.Driver"
        connection_check_timeout_sec = 100
        user = "user"
        password = "password"
        result_table_name = "t_user_01"
        query = "select * from t_user"
    }
}
 
transform {
  Replace {
    source_table_name = "t_user_01"
    result_table_name = "t_user_02"
    replace_field = "name"
    pattern = "%"
    replacement = ""
  }
}
 
sink {
  jdbc {
        url = "jdbc:mysql://127.0.0.1:3306/test"
        driver = "com.mysql.cj.jdbc.Driver"
        connection_check_timeout_sec = 100
        user = "user"
        password = "password"
        source_table_name = "t_user_02"
        query = "insert into ods_t_user(id,name,birth,gender) values(?,?,?,?)"
  }
}
Split插件
env {
  execution.parallelism = 2
  job.mode = "BATCH"
}
source {
    Jdbc {
        url = "jdbc:mysql://127.0.0.1:3306/test"
        driver = "com.mysql.cj.jdbc.Driver"
        connection_check_timeout_sec = 100
        user = "user"
        password = "password"
        result_table_name = "t_user_01"
        query = "select * from t_user"
    }
}
 
transform {
  Split {
    source_table_name = "t_user_01"
    result_table_name = "t_user_02"
    separator = "-"
    split_field = "birth"
    output_fields  = [birth_y, birth_m, birth_d]
  }
}
 
sink {
  jdbc {
        url = "jdbc:mysql://127.0.0.1:3306/test"
        driver = "com.mysql.cj.jdbc.Driver"
        user = "user"
        password = "password"
        source_table_name = "t_user_02"
        query = "insert into ods_t_user_y_m_d(id,name,birth,gender,birth_y,birth_m,birth_d) values(?,?,?,?,?,?,?)"
  }
}
FilterRowKind插件
 
env {
  job.mode = "BATCH"
}
 
source {
  FakeSource {
    result_table_name = "fake"
    row.num = 100
    schema = {
      fields {
        id = "int"
        name = "string"
        age = "int"
      }
    }
  }
}
 
transform {
  FilterRowKind {
    source_table_name = "fake"
    result_table_name = "fake1"
    exclude_kinds = ["INSERT"] -- 只保留数据标记为 insert的行
  }
}
 
sink {
  Console {
    source_table_name = "fake1"
  }
}
SQL引擎

可以通过手写sql语句的方式,进行数据转换,可支持自定义函数。

使用sql插件来检测字段,仅保留用户名与地址字段,其他字段将被丢弃,用户信息表是前一个插件配置的结果表名称。

sql {
    sql = "select username, address from user_info",
}

使用自定义函数-Use UDF

sql {
    sql = "select substring(telephone, 0, 10) from user_info",
}

使用自定义聚合函数-Use UDAF

sql {
    sql = "select avg(age) from user_info",
    table_name = "user_info"
}

4. 自定义转化插件

以copy的插件为例:

代码结构:

WEBRESOURCE2ba6f74ade70a1fbce8c53210d74310bimage.png copyTransformConfig类

// 保存了插件的配置项信息
@Getter
@Setter
public class CopyTransformConfig implements Serializable {
    @Deprecated
    public static final Option<String> SRC_FIELD =
            Options.key("src_field")
                    .stringType()
                    .noDefaultValue()
                    .withDescription("Src field you want to copy");

    @Deprecated
    public static final Option<String> DEST_FIELD =
            Options.key("dest_field")
                    .stringType()
                    .noDefaultValue()
                    .withDescription("Copy Src field to Dest field");

    // 配置fields局域信息
    public static final Option<Map<String, String>> FIELDS =
            Options.key("fields")
                    .mapType()
                    .noDefaultValue() // 无默认信息
                    // 描述信息
                    .withDescription(
                            "Specify the field copy relationship between input and output");

    private LinkedHashMap<String, String> fields;

    public static CopyTransformConfig of(ReadonlyConfig config) {
        LinkedHashMap<String, String> fields = new LinkedHashMap<>();
        Optional<Map<String, String>> optional = config.getOptional(FIELDS);
        if (optional.isPresent()) {
            fields.putAll(config.get(FIELDS));
        } else {
            fields.put(config.get(DEST_FIELD), config.get(SRC_FIELD));
        }

        CopyTransformConfig copyTransformConfig = new CopyTransformConfig();
        copyTransformConfig.setFields(fields);
        return copyTransformConfig;
    }
}

CopyFieldTransformFactory类

@AutoService(Factory.class)
public class CopyFieldTransformFactory implements TableTransformFactory {
    @Override
    // 定义身份标识
    public String factoryIdentifier() {
        return CopyFieldTransform.PLUGIN_NAME;
    }

    @Override
    public OptionRule optionRule() {
        return OptionRule.builder()
            // bundled捆绑选项:必须同时存在或者都不存在
                .bundled(CopyTransformConfig.SRC_FIELD, CopyTransformConfig.DEST_FIELD)
                .bundled(CopyTransformConfig.FIELDS)
                .build();
    }

    @Override
    // 创建插件
    public TableTransform createTransform(TableTransformFactoryContext context) {
        CopyTransformConfig copyTransformConfig = CopyTransformConfig.of(context.getOptions());
        CatalogTable catalogTable = context.getCatalogTables().get(0);
        return () -> new CopyFieldTransform(copyTransformConfig, catalogTable);
    }
}    

CopyFieldTransform类:

// 具体的转化类:主要用于对source数据的处理
public class CopyFieldTransform extends MultipleFieldOutputTransform {
    public static final String PLUGIN_NAME = "Copy";

    private final CopyTransformConfig config;
    private List<String> fieldNames;
    private List<Integer> fieldOriginalIndexes;
    private List<SeaTunnelDataType<?>> fieldTypes;

    public CopyFieldTransform(CopyTransformConfig copyTransformConfig, CatalogTable catalogTable) {
        super(catalogTable);
        this.config = copyTransformConfig;
        SeaTunnelRowType seaTunnelRowType = catalogTable.getTableSchema().toPhysicalRowDataType();
        initOutputFields(seaTunnelRowType, config.getFields());
    }

    @Override
    public String getPluginName() {
        return PLUGIN_NAME;
    }
    // 将输入的字段域初始化进入到类型中
    private void initOutputFields(
            SeaTunnelRowType inputRowType, LinkedHashMap<String, String> fields) {
        List<String> fieldNames = new ArrayList<>();
        List<Integer> fieldOriginalIndexes = new ArrayList<>();
        List<SeaTunnelDataType<?>> fieldsType = new ArrayList<>();
        for (Map.Entry<String, String> field : fields.entrySet()) {
            String srcField = field.getValue();
            int srcFieldIndex;
            try {
                srcFieldIndex = inputRowType.indexOf(srcField);
            } catch (IllegalArgumentException e) {
                throw TransformCommonError.cannotFindInputFieldError(getPluginName(), srcField);
            }
            fieldNames.add(field.getKey());
            fieldOriginalIndexes.add(srcFieldIndex);
            fieldsType.add(inputRowType.getFieldType(srcFieldIndex));
        }
        this.fieldNames = fieldNames;
        this.fieldOriginalIndexes = fieldOriginalIndexes;
        this.fieldTypes = fieldsType;
    }

    @Override
    protected Column[] getOutputColumns() {
        if (inputCatalogTable == null) {
            Column[] columns = new Column[fieldNames.size()];
            for (int i = 0; i < fieldNames.size(); i++) {
                columns[i] =
                        PhysicalColumn.of(fieldNames.get(i), fieldTypes.get(i), 200, true, "", "");
            }
            return columns;
        }

        Map<String, Column> catalogTableColumns =
                inputCatalogTable.getTableSchema().getColumns().stream()
                        .collect(Collectors.toMap(column -> column.getName(), column -> column));

        List<Column> columns = new ArrayList<>();
        for (Map.Entry<String, String> copyField : config.getFields().entrySet()) {
            Column srcColumn = catalogTableColumns.get(copyField.getValue());
            PhysicalColumn destColumn =
                    PhysicalColumn.of(
                            copyField.getKey(),
                            srcColumn.getDataType(),
                            srcColumn.getColumnLength(),
                            srcColumn.isNullable(),
                            srcColumn.getDefaultValue(),
                            srcColumn.getComment());
            columns.add(destColumn);
        }
        return columns.toArray(new Column[0]);
    }

    @Override
    protected Object[] getOutputFieldValues(SeaTunnelRowAccessor inputRow) {
        Object[] fieldValues = new Object[fieldNames.size()];
        for (int i = 0; i < fieldOriginalIndexes.size(); i++) {
            fieldValues[i] =
                    clone(
                            fieldNames.get(i),
                            fieldTypes.get(i),
                            inputRow.getField(fieldOriginalIndexes.get(i)));
        }
        return fieldValues;
    }
    // 核心复制方法
    private Object clone(String field, SeaTunnelDataType<?> dataType, Object value) {
        if (value == null) {
            return null;
        }
        switch (dataType.getSqlType()) {
            case BOOLEAN:
            case STRING:
            case TINYINT:
            case SMALLINT:
            case INT:
            case BIGINT:
            case FLOAT:
            case DOUBLE:
            case DECIMAL:
            case DATE:
            case TIME:
            case TIMESTAMP:
                return value;
            case BYTES:
                byte[] bytes = (byte[]) value;
                byte[] newBytes = new byte[bytes.length];
                System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
                return newBytes;
            case ARRAY:
                ArrayType arrayType = (ArrayType) dataType;
                Object[] array = (Object[]) value;
                Object newArray =
                        Array.newInstance(arrayType.getElementType().getTypeClass(), array.length);
                for (int i = 0; i < array.length; i++) {
                    Array.set(newArray, i, clone(field, arrayType.getElementType(), array[i]));
                }
                return newArray;
            case MAP:
                MapType mapType = (MapType) dataType;
                Map map = (Map) value;
                Map<Object, Object> newMap = new HashMap<>();
                for (Object key : map.keySet()) {
                    newMap.put(
                            clone(field, mapType.getKeyType(), key),
                            clone(field, mapType.getValueType(), map.get(key)));
                }
                return newMap;
            case ROW:
                SeaTunnelRowType rowType = (SeaTunnelRowType) dataType;
                SeaTunnelRow row = (SeaTunnelRow) value;

                Object[] newFields = new Object[rowType.getTotalFields()];
                for (int i = 0; i < rowType.getTotalFields(); i++) {
                    newFields[i] =
                            clone(
                                    rowType.getFieldName(i),
                                    rowType.getFieldType(i),
                                    row.getField(i));
                }
                SeaTunnelRow newRow = new SeaTunnelRow(newFields);
                newRow.setRowKind(row.getRowKind());
                newRow.setTableId(row.getTableId());
                return newRow;
            case NULL:
                return null;
            default:
                throw CommonError.unsupportedDataType(
                        getPluginName(), dataType.getSqlType().toString(), field);
        }
    }
}

入口解析

服务端

默认使用seatunnel默认引擎,通过源码中找到初始入口。

WEBRESOURCE3750b3e3c80cdcd3fe88469bf667602e1708414550437.jpg

核心启动类

WEBRESOURCEa900f868e86239d7b8a76a31d6b85dd8image.png

WEBRESOURCE5335045a81c1be90312e8c8e091bef18image.png

new SeaTunnelNodeContext(seaTunnelConfig),这里会返回一个SeaTunnelNodeContext类,这个类是继承自Hazelcast这个组件的DefaultNodeContext类。在Hazelcast启动的过程中,会去调用DefaultNodeContext类的实现类的createNodeExtension()方法,在这里其实也就是SeaTunnelNodeContext类的createNodeExtension()方法。

WEBRESOURCE3b38c67c03ff68c1f9652f48926304781708421510133.png

WEBRESOURCEf451407fc543430e17bf3146f11c0f16image.png

WEBRESOURCE9abb47c9f63ded4dee06a71d9a25363aimage.png

实际运行中,hazelcast会调用方法中init方法。可以观测到任务执行器开始启动。

WEBRESOURCE591d516ab9bff227a2714a51f68c5ff8image.png

官方介绍服务类:

**TaskExecutionService **

TaskExecutionService 是一个执行任务的服务,将在每个节点上运行一个实例。它从 JobMaster 接收 TaskGroup 并在其中运行 Task。并维护TaskID->TaskContext,对Task的具体操作都封装在TaskContext中。而Task内部持有OperationService,也就是说Task可以通过OperationService远程调用其他Task或JobMaster进行通信。

**CoordinatorService **

CoordinatorService是一个充当协调器的服务,它主要负责处理客户端提交的命令以及切换master后任务的恢复。客户端在提交任务时会找到master节点并将任务提交到CoordinatorService服务上,CoordinatorService会缓存任务信息并等待任务执行结束。当任务结束后再对任务进行归档处理。

SlotService

SlotService是slot管理服务,用于管理集群的可用Slot资源。SlotService运行在所有节点上并定期向master上报资源信息。

WEBRESOURCE83a43fe7c926b5c23f387e2e266c756fimage.png

这里新建了一个cooperativeTaskWorker类,这个类对象会被提交到executorService去执行,代码就是下一行的executorService.submit(cooperativeTaskWorker)这个代码。

然后当cooperativeTaskWorker被提交到executorService上的时候,其实是会运行cooperativeTaskWorker这个类的run方法的。

客户端

WEBRESOURCE12acefd78c755bbd5bf7f883f1df830dimage.png