【本地构建编译】Apache SeaTunnel2.3.5适配Web1.0.0,运行实现Mysql-CDC示例!

其实技术思路都大同小异都是分割数据,多线程执行分布式多节点执行任务,栅栏同步等待全部线程执行完成两阶段事务,read+writ都之心完成才算执行成功,否则回退,根据主键分割数据,然后下发给多节点同步处理数据,在加一个两阶段事务,保证数据read+write两边一致性,中间在做一个Job的Checkpoin(检查点),savepoint(保存点)这两个都是涉及到任务执行的情况状态等信息的持久存储,所以可以存储在hdfsoss(ali-hdfs-oss)s3等分布式存储,可以多线程任务共享数据,无非可以在把filinkspark自定义引擎啥的搞一套,在一个加个插件发现机制

前言

如果不了解背景的同学可以查看之前的文章,以便于大家学习交流!

主要看这两篇

Apache SeaTunnel本地源码构建编译运行调试


CentOs7.x安装部署 SeaTunnelWeb遇到的坑

编译

版本说明

SeaTunnel的分支选择2.3.4-release里面的版本是

<version>2.3.5-SNAPSHOT</version>

17176670514700c864feeb3283c5d27fdbaaf1f9ebec7

先把SeaTunnel 2.3.4-release的分支拉下来,拉下项目来记得切换分支到2.3.4-release分支,这一步特别关键,否则没有切换分支可能默认在dev分支,估计会有问题,web版本为1.0.0,这里需要特别注意!

release分支配置

如果你的电脑是windwos电脑可以将持久化配置成localfile,SeaTunnel源码文件有说明:localfile已经废弃,使用HDFS替代.

17176670514221d144b3a9cc1bff9b7c0aec581c9c85f

在Web应用配置中,可以选择性地添加这个包。尽管在当前的配置说明中没有提供具体的截图,需要注意的是,在运行时,相关的jar包需添加到Web应用的libs目录下以及SeaTunnel的lib目录下。

这一步骤虽然可选,但对于确保应用功能的完整性和兼容性是非常有帮助的。

1717667051431b288c34159a44f4cf77247332a3a5b60

编译打包:

mvn clean package -pl seatunnel-dist -am '-Dmaven.test.skip=true' -T 8C

Maven调优配置

settingmavenRunner中配置jvm参数如下:

-Dfile.encoding=GBK -DarchetypeCatalog=local -Xmx1024m -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m -Xss2m -Dmaven.test.skip=true -Dmaven.compile.fork=true

maven做了一些参数调优,否则maven导入编译打包会很慢,settings->maven->importing中的jvm参数可以设置为:

-Xmx1024m

当项目规模较大时,未设置合适的参数可能导致项目导入速度极慢甚至堆栈溢出。为了加快编译打包速度,建议增大Maven的最大堆设置。

此外,可以在Archetype Catalogs中添加一个本地配置,指定本地Maven仓库的位置,以避免每次编译时从远程仓库拉取依赖,这样不仅能节省时间,还能减少因网络慢带来的编译延迟。

所以一般都是将各个模块分别install到本地仓库,然后执行总体编译打包命令的时候通过这个配置优选选用本地仓库的jar,就不会去远程下载了,这种就可以加快编译打包的速度),点击Build Tools-->Maven

设置Maven的线程数据为:

Thread Count 8 -T option

设置输入一个8即可!

以上配置是为了导入maven项目加载快和编译运行快,两个项目里面都可以调优配置一下。

Web1.0.0适配

配置文件修改和新增文件

1717667051409677a6c1f14b91e0f708f544049fa0196

手动拷贝jar修改依赖

修改顶级父pom的版本号如下,该为2.3.5

17176670514090c75add952f3f3e63d2dbe7268dfb1b9

lib用于从SeaTunnel本地编译拷贝一些依赖的jar,然后替换如下:

       <dependency>             <groupId>org.apache.seatunnel</groupId>             <artifactId>seatunnel-common</artifactId>             <version>${seatunnel-framework.version}</version>             <scope>system</scope>             <systemPath>D:/other-workspace/seatunnel-web/lib/seatunnel-common-2.3.5-SNAPSHOT-2.12.15.jar</systemPath>         </dependency>

在处理多模块Maven项目时,避免在顶级父POM中直接添加依赖,因为顶级父POM通常使用来统一管理依赖,该标签不支持包含本地jar包的直接依赖。

因此,应将依赖的jar包手动拷贝到各个子模块的lib目录下,而不是顶级目录。建议使用绝对路径来引用这些jar,以简化跨多个子模块的管理。

若在编译时发现缺少依赖,应检查并确保所有必需的jar包已被正确放置并在子模块的POM文件中被引用。

17176670514349e8e31f0d64be4061fa786f9d80a8e8d

需要手动将拷贝的lib下的jar包文件手动导入项目,否则idea编译后不能识别代码,如果有类是红色的!说明没有导入依赖,所以要这种手动导入一下才可以的,这里需要特别注意!

修改Web不兼容的代码

上一步修改完依赖编译会有接口代码不兼容报错,所以需要修改Web代码

EngineDataType内部类修改:

public static class SeaTunnelDataTypeConvertor             implements DataTypeConvertor<SeaTunnelDataType<?>> {         @Override         public SeaTunnelDataType<?> toSeaTunnelType(String s, String s1) {             return DATA_TYPE_MAP.get(s.toLowerCase(Locale.ROOT)).getRawType();         }         @Override         public SeaTunnelDataType<?> toSeaTunnelType(                 String s, SeaTunnelDataType<?> seaTunnelDataType, Map<String, Object> map) {             return seaTunnelDataType;         }         @Override         public SeaTunnelDataType<?> toConnectorType(                 String s, SeaTunnelDataType<?> seaTunnelDataType, Map<String, Object> map) {             return seaTunnelDataType;         }         @Override         public String getIdentity() {             return "EngineDataTypeConvertor";         }     }

JobExecutorServiceImpl

executeJobBySeaTunnel方法: 原来: JobExecutionEnvironment jobExecutionEnv =                     seaTunnelClient.createExecutionContext(filePath, jobConfig); 现在: SeaTunnelConfig seaTunnelConfig = ConfigProvider.locateAndGetSeaTunnelConfig();             ClientJobExecutionEnvironment jobExecutionEnv =                     seaTunnelClient.createExecutionContext(filePath, jobConfig, seaTunnelConfig);

JobInstanceServiceImpl类:

public void complete(             @NonNull Integer userId, @NonNull Long jobInstanceId, @NonNull String jobEngineId)方法: 原来: if (statusList.size() == 1 && statusList.contains("FINISHED")) {             jobStatus = JobStatus.FINISHED.name();         } else if (statusList.contains("FAILED")) {             jobStatus = JobStatus.FAILED.name();         } else if (statusList.contains("CANCELED")) {             jobStatus = JobStatus.CANCELED.name();         } else if (statusList.contains("CANCELLING")) {             jobStatus = JobStatus.CANCELLING.name();         } else {             jobStatus = JobStatus.RUNNING.name();         } 现在: if (statusList.size() == 1 && statusList.contains("FINISHED")) {             jobStatus = JobStatus.FINISHED.name();         } else if (statusList.contains("FAILED")) {             jobStatus = JobStatus.FAILED.name();         } else if (statusList.contains("CANCELED")) {             jobStatus = JobStatus.CANCELED.name();         } else if (statusList.contains("CANCELING")) {             jobStatus = JobStatus.CANCELING.name();         } else {             jobStatus = JobStatus.RUNNING.name();         }

PluginDiscoveryUtil类的getConnectorFeatures方法

原来: public static Map<PluginIdentifier, ConnectorFeature> getConnectorFeatures(             PluginType pluginType) throws IOException {         Common.setStarter(true);         if (!pluginType.equals(PluginType.SOURCE)) {             throw new UnsupportedOperationException("ONLY support plugin type source");         }         Path path = new SeaTunnelSinkPluginDiscovery().getPluginDir();         List<Factory> factories;         if (path.toFile().exists()) {             List<URL> files = FileUtils.searchJarFiles(path);             factories =                     FactoryUtil.discoverFactories(new URLClassLoader(files.toArray(new URL[0])));         } else {             factories =                     FactoryUtil.discoverFactories(Thread.currentThread().getContextClassLoader());         }         Map<PluginIdentifier, ConnectorFeature> featureMap = new ConcurrentHashMap<>();         factories.forEach(                 plugin -> {                     if (TableSourceFactory.class.isAssignableFrom(plugin.getClass())) {                         TableSourceFactory tableSourceFactory = (TableSourceFactory) plugin;                         PluginIdentifier info =                                 PluginIdentifier.of(                                         "seatunnel",                                         PluginType.SOURCE.getType(),                                         plugin.factoryIdentifier());                         featureMap.put(                                 info,                                 new ConnectorFeature(                                         SupportColumnProjection.class.isAssignableFrom(                                                 tableSourceFactory.getSourceClass())));                     }                 });         return featureMap;     } 现在: public static Map<PluginIdentifier, ConnectorFeature> getConnectorFeatures(             PluginType pluginType) throws IOException {         Common.setStarter(true);         if (!pluginType.equals(PluginType.SOURCE)) {             throw new UnsupportedOperationException("ONLY support plugin type source");         }         List<Factory> factories = null;         SeaTunnelSinkPluginDiscovery seaTunnelSinkPluginDiscovery =                 new SeaTunnelSinkPluginDiscovery();         Map<PluginIdentifier, String> allSupportedPlugins =                 seaTunnelSinkPluginDiscovery.getAllSupportedPlugins(pluginType);         for (Entry<PluginIdentifier, String> entry : allSupportedPlugins.entrySet()) {             PluginIdentifier pluginIdentifier = entry.getKey();             List<PluginIdentifier> pluginIdentifiers = new ArrayList<>();             pluginIdentifiers.add(pluginIdentifier);             List<URL> files = seaTunnelSinkPluginDiscovery.getPluginJarPaths(pluginIdentifiers);             if (CollectionUtils.isNotEmpty(files)) {                 factories =                         FactoryUtil.discoverFactories(                                 new URLClassLoader(files.toArray(new URL[0])));             } else {                 factories =                         FactoryUtil.discoverFactories(                                 Thread.currentThread().getContextClassLoader());             }         }         Map<PluginIdentifier, ConnectorFeature> featureMap = new ConcurrentHashMap<>();         if (CollectionUtils.isNotEmpty(factories)) {             factories.forEach(                     plugin -> {                         if (TableSourceFactory.class.isAssignableFrom(plugin.getClass())) {                             TableSourceFactory tableSourceFactory = (TableSourceFactory) plugin;                             PluginIdentifier info =                                     PluginIdentifier.of(                                             "seatunnel",                                             PluginType.SOURCE.getType(),                                             plugin.factoryIdentifier());                             featureMap.put(                                     info,                                     new ConnectorFeature(                                             SupportColumnProjection.class.isAssignableFrom(                                                     tableSourceFactory.getSourceClass())));                         }                     });         }         return featureMap;     }

SchemaDerivationServiceImplderivationSQL

原来: TableFactoryContext context =                 new TableFactoryContext(                         Collections.singletonList(table),                         ReadonlyConfig.fromMap(config),                         Thread.currentThread().getContextClassLoader()); 现在: TableTransformFactoryContext context =                 new TableTransformFactoryContext(                         Collections.singletonList(table),                         ReadonlyConfig.fromMap(config),                         Thread.currentThread().getContextClassLoader());

SeaTunnelEngineProxyrestoreJob方法中

原来: seaTunnelClient.restoreExecutionContext(filePath, jobConfig, jobEngineId).execute(); 现在: SeaTunnelConfig seaTunnelConfig = ConfigProvider.locateAndGetSeaTunnelConfig();             seaTunnelClient                     .restoreExecutionContext(filePath, jobConfig, seaTunnelConfig, jobEngineId)                     .execute();

TableSchemaServiceImplTableSchemaServiceImpl()方法

原来: public TableSchemaServiceImpl() throws IOException {         Common.setStarter(true);         Path path = new SeaTunnelSinkPluginDiscovery().getPluginDir();         if (path.toFile().exists()) {             List<URL> files = FileUtils.searchJarFiles(path);             files.addAll(FileUtils.searchJarFiles(Common.pluginRootDir()));             factory = new DataTypeConvertorFactory(new URLClassLoader(files.toArray(new URL[0])));         } else {             factory = new DataTypeConvertorFactory();         }     } 现在: public TableSchemaServiceImpl() throws IOException {         Common.setStarter(true);         SeaTunnelSinkPluginDiscovery seaTunnelSinkPluginDiscovery =                 new SeaTunnelSinkPluginDiscovery();         Map<PluginIdentifier, String> allSupportedPlugins =                 seaTunnelSinkPluginDiscovery.getAllSupportedPlugins(PluginType.SINK);         for (Map.Entry<PluginIdentifier, String> entry : allSupportedPlugins.entrySet()) {             PluginIdentifier pluginIdentifier = entry.getKey();             List<PluginIdentifier> pluginIdentifiers = new ArrayList<>();             pluginIdentifiers.add(pluginIdentifier);             List<URL> files = seaTunnelSinkPluginDiscovery.getPluginJarPaths(pluginIdentifiers);             if (CollectionUtils.isNotEmpty(files)) {                 factory =                         new DataTypeConvertorFactory(new URLClassLoader(files.toArray(new URL[0])));             } else {                 factory = new DataTypeConvertorFactory();             }         }     }

TableSchemaServiceImplgetSeaTunnelSchema

原来: SeaTunnelDataType<?> dataType = convertor.toSeaTunnelType(field.getType()); 现在: SeaTunnelDataType<?> dataType = convertor.toSeaTunnelType(field.getType(), null);

到此web1.0.0的兼容代码已经修改完成。

Web编译打包

执行如下编译打包命令

mvn clean package -pl seatunnel-web-dist -am '-Dmaven.test.skip=true' -T 8C

运行MySQL-CDC示例

经过以上的步骤,SeaTunnel2.3.5和web1.0.0的适配已经可以正常在dist下打包成功,

配置运行SeaTunnel

环境变量和启动类配置

**环境变量:**这个是dis的target的二进制解压路径(运行家目录)

-DSEATUNNEL_HOME=D:\other-workspace\seatunnel\seatunnel-dist\target\apache-seatunnel-2.3.5-SNAPSHOT

17176670524191b5965f0c432b5c38e2825d1cf6038d7

配置运行Web

环境变量、jvm参数和启动类配置

环境变量:这个和SeaTunnel的是同一个

-DSEATUNNEL_HOME=D:\other-workspace\seatunnel\seatunnel-dist\target\apache-seatunnel-2.3.5-SNAPSHOT

jvm参数:是web-dis下的target的加压路径(运行家目录)

ST_WEB_BASEDIR_PATH=D:\other-workspace\seatunnel-web\seatunnel-web-dist\target\apache-seatunnel-web-1.0.0-SNAPSHOT

拷贝jar

SeaTunnel的家目录:

1717667052457ad0da65c9a6af84b6605a856b86adb9e

Web的家目录:

1717667052427c67ae0ad4300a8f9867b10dbf18090a9171766705243115b7d16a3dddc60eda209ac55687f39a

需要在SeaTunnel家目录的lib和web家目录的libs下放入如下jar包:

mysql-connector-java-8.0.33.jar datasource-jdbc-mysql-1.0.0-SNAPSHOT.jar connector-jdbc-2.3.5-SNAPSHOT-2.12.15.jar connector-cdc-mysql-2.3.5-SNAPSHOT-2.12.15.jar

其它的cdc也是一样的都要将所需要的jar放到这两个路径下,否则,缺少依赖运行会报错!

点击debug将两个项目启动起来,能正常启动就是ok的,SeaTunnel启动有一个hadoop的报错,不影响可以正常启动的,如果有hadoop环境就不会有hadoop的报错的。

UI编译运行注意事项

本机上安装的nodenpm的版本需要大于等于Web项目中规定的版本,否则会编译失败,如果版本过低,需要将web-dist的pom中的如下插件注释:

版本过低会编译打包会遇到如下错误:

[ERROR] Failed to execute goal com.github.eirslett:frontend-maven-plugin:1.11.3:npm (build) on project seatunnel-web-dist: Failed to  run task: 'npm run build:prod' failed. org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1) ->

解决:

 <!--<plugin>                         <groupId>com.github.eirslett</groupId>                         <artifactId>frontend-maven-plugin</artifactId>                         <version>1.11.3</version>                         <configuration>                             <workingDirectory>${project.basedir}/../seatunnel-ui</workingDirectory>                         </configuration>                         <executions>                             <execution>                                 <id>install node and npm</id>                                 <goals>                                     <goal>install-node-and-npm</goal>                                 </goals>                                 <configuration>                                     <nodeVersion>v14.17.3</nodeVersion>                                     <npmVersion>6.14.13</npmVersion>                                 </configuration>                             </execution>                             <execution>                                 <id>install</id>                                 <goals>                                     <goal>npm</goal>                                 </goals>                                 <phase>generate-resources</phase>                                 <configuration>                                     <arguments>install &#45;&#45;ignore-scripts</arguments>                                 </configuration>                             </execution>                             <execution>                                 <id>build</id>                                 <goals>                                     <goal>npm</goal>                                 </goals>                                 <configuration>                                     <arguments>run build:prod</arguments>                                 </configuration>                             </execution>                         </executions>                     </plugin>-->

然后去升级本地安装的node、npm,先卸载低版本,然后安装最新稳定高版本,最重要的是:配置node的环境变量,npm的环境变量!

MySQL-CDC的示例

添加数据源

1717667052427c67ae0ad4300a8f9867b10dbf18090a9

创建CDC任务

下面 创建了两种类型的任务:数据集成和整库同步。数据集成任务只允许同步单一数据表,而整库同步可处理多表的CDC任务。两者都必须设置为流式任务类型,批处理类型不支持,会导致错误。

数据源方面,源(source)使用的是mysql-cdc,而目标端(sink)只能选择jdbc类型的数据源。进行CDC操作前,需确认并启动MySQL 8.0的binlog监听功能,以便正确同步数据。

1717667052447cf385aa8490a1a3ccd4d870047828fad

任务执行现象和结论

点击执行按钮后去目的地表查看,没有数据,过一会后去查看,发现目的地表有数据,然后改源表中的一条数据的一个字段的值,在去目的地表查看对应的字段也变更了,说明MySQL-CDC单表CDC(先做全量后做增量和实时)的Demo是成功了。

在Web项目下会有一个profile里面会保存每次创建的任务的文件,下面是我随便找了一个用作demo文件,这个文件的内容都是web页面配置生成到这个profile下的:

env { "job.mode"=STREAMING "job.name"="SeaTunnel_Job" } source { MySQL-CDC {     format=DEFAULT     "snapshot.split.size"=8096     "snapshot.fetch.size"=1024     "incremental.parallelism"=1     "connect.timeout.ms"=30000     "connect.max-retries"=3     "connection.pool.size"=20     "chunk-key.even-distribution.factor.lower-bound"=0.05     "chunk-key.even-distribution.factor.upper-bound"=100     "sample-sharding.threshold"=1000     "inverse-sampling.rate"=1000     "startup.mode"=INITIAL     "exactly_once"="true"     "stop.mode"=NEVER     parallelism=1     "result_table_name"=Table13434473575488     "dag-parsing.mode"=MULTIPLEX     catalog {         factory=Mysql     }     database-names=[         "xxxxx"     ]     table-names=[         "xxxx.xx_order"     ]     password="xxxx"     username=root     base-url="jdbc:mysql://xxxx:3306/seatunnel?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&allowPublicKeyRetrieval=true"     server-time-zone=UTC } } transform { } sink { Jdbc {     "schema_save_mode"="CREATE_SCHEMA_WHEN_NOT_EXIST"     "data_save_mode"="APPEND_DATA"     "connection_check_timeout_sec"=30     "batch_size"=1000     "is_exactly_once"="true"     "xa_data_source_class_name"=test-cdc1     "max_commit_attempts"=3     "transaction_timeout_sec"=-1     "auto_commit"="true"     "support_upsert_by_query_primary_key_exist"="true"     "multi_table_sink_replica"=1     "source_table_name"=Table13434473575488     "generate_sink_sql"=true     catalog {         factory=MySQL         username=root         password="xxx"         base-url="jdbc:mysql://xxxx:3306/seatunnel?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&allowPublicKeyRetrieval=true"     }     database="xxx_test"     url="jdbc:mysql://xxxxx:3306/seatunnel?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&allowPublicKeyRetrieval=true"     driver="com.mysql.cj.jdbc.Driver"     password="xxxxxx"     user=root } }

运行遇到一个报错:

Caused by: java.lang.ClassCastException: cannot assign instance of java.lang.Long to field org.apache.seatunnel.api.table.catalog.Column.columnLength of type java.lang.Integer in instance of org.apache.seatunnel.api.table.catalog.PhysicalColumn

这个是因为我之前把SeaTunnel的代码拉下来默认在dev分支,两边引擎的代码不一致导致的保持,所以文章开头特别强调需要切换分支到·2.3.4-release·上,然后重新编译将SeaTunnel的jar拷贝到web项目新建的lib路径下统一管理外部jar下,重新编译web后跑起来运行两边的代码就是一致的了,web依赖SeaTunnel需要以SeaTunnel中编译的为主,否则就会有一些奇奇怪怪的问题。

同步任务实例

下面是一个历史任务执行可以查看的界面,由于我们配置的存储是localfile的,所以这个刷新这个同步任务实例或者点击里面的任务查看会报错,是因为这里的接口是去hdfs上找历史任务的jar包,本地没有hadoop环境所以会报错,特此说明,不影响我们MySQL-CDC的操作,正式环境有hadoop环境就没有这个问题了,在Linux环境不会,这个存储介质支持:hdfs、oss(阿里oss-hdfs)、s3等

1717667052504217f0bf198a3f12555f78cd43371f97d

总结

其实逻辑也简单的,就是根据主键唯一标识分割数据发给不同的节点上两阶段执行,每个节点都是执行source --> t ---> sink,连接段提交事务才算完成一整个链路的数据同步的,如果是CDC的插件,拿MySQL来举例子:

  • 会监听Binlog的日志变化

  • 读取变化的数据发给节点

  • Sink根据主键更新

  • Source的数据源是一个CDC的数据源

  • Sink端的数据源是一个JDBC的数据源的

这个跟写业务代码处理数据同步有啥区别么?

比如说同步一张有几千万数据的一个单表,

第一步:分页根据id升序查出所有的id,(或者是查一个list,分割list给多个线程执行)

第二步:一页一个线程处理数据同步

第三步:加一个栅栏同步等待所有数据同步线程处理完成,然后主线程才算执行完成!

其实技术思路都大同小异:都是分割数据,多线程执行(分布式多节点执行任务),栅栏同步等待全部线程执行完成(两阶段事务,read + writ都之心完成才算执行成功,否则回退),根据主键分割数据,然后下发给多节点同步处理数据,在加一个两阶段事务,保证数据read + write 两边一致性,中间在做一个Job的Checkpoin(检查点) ,savepoint(保存点) 这两个都是涉及到任务执行的情况状态等信息的持久存储,所以可以存储在hdfs/oss(ali-hdfs-oss)/s3等分布式存储,可以多线程任务共享数据,无非可以在把filink / spark /自定义引擎 啥的搞一套,在一个加个插件发现机制

source + t + sink

这个三个端都有自己的不同数据源的实现,可以加载发现自己的jar包,而且还能通过Web控制台可视化管理任务,这个设计思想是可以学习可借鉴的!

本文中使用的方式适配了运行MySQL-CDC单表的数据同步,如果是其他的场景,需要用这种方式去适配,涉及到的API兼容性问题需要自己处理解决,我只是提供一种思路和方法,只要熟悉项目代码,相信大家都能把项目本地编译运行起来就可以修改拓展源码,希望我的分享对你有所启发和帮助,请一键三连!

本文由 白鲸开源科技 提供发布支持!