SeaTunnel 日志框架集成改造,确保 100+ 连接器日志稳定输出

SeaTunnel 通过统一的连接器 API 集成了众多的数据源与目标(当前支持 100+ 种数据源与目标),并且最终连接器可以运行到多种执行引擎上(当前支持 Spark/Flink/SeaTunnel-Zeta),来帮助用户快速完成数据集成同步的需求。

封面_20230414_145614138

作者 | 王海林,Apache SeaTunnel PPMC

01




背景

SeaTunnel 通过统一的连接器 API 集成了众多的数据源与目标(当前支持 100+ 种数据源与目标),并且最终连接器可以运行到多种执行引擎上(当前支持 Spark/Flink/SeaTunnel-Zeta),来帮助用户快速完成数据集成同步的需求。
这意味着 SeaTunnel 是一个 jar 依赖众多的开放式插件系统,并且具有多种运行时环境,如下是当前所有模块及运行环境与日志框架的依赖情况梳理:

  • 执行引擎
    • Spark2.x 使用 slf4j + log4j1
    • Spark3.x 使用 slf4j + log4j2
    • Flink 使用 slf4j + log4j2
    • SeaTunnel-Zeta 使用 slf4j + log4j1
  • SeaTunnel Starter(提交作业的引导器)
    • jar 包跟随作业一起提交
    • jar 包跟随作业一起提交
    • Spark-Starter 使用 slf4j + log4j1
    • Flink-Starter 使用 slf4j + log4j1
    • SeaTunnel-Starter(SeaTunnel-Zeta) 使用 slf4j + log4j1
  • Connector 连接器
    • 这里是各种相互冲突的日志桥接包同时引入触发 StackOverflowError 的根源
    • 这里是连接器出现日志冲突的根源
    • 各个 Connertor 可能直接或间接依赖:slf4j、log4j1、log4j2、logback、commons-logging 等
    • 各个 Connector 依赖的第三方包也可能会依赖部分日志桥接包:slf4j-log4j12、log4j-over-slf4j、log4j-to-slf4j、slf4j-jdk14、jul-to-slf4j、slf4j-jcl、jcl-over-slf4j 等
  • Examples 运行示例
    • 结合上述执行引擎 + Connector 组合出的日志依赖
  • E2E 测试
    • 结合上述执行引擎 + Connector 组合出的日志依赖

可以看到,在日志 API、实现框架、桥接包等依赖上均存在冲突点位,这会导致非常多的 Java 日志框架冲突问题:

  • 连接器之间(数据源与目标)使用不同的日志框架
  • 连接器与连接器之间的依赖包使用不同的日志框架
  • 执行引擎之间使用不同的日志框架
  • 执行引擎多版本之间使用不同的日志框架
  • 执行引擎与连接器使用不同的日志框架
  • SeatTunnel Modules 与连接器使用不同的日志框架
  • 以上各种情况相互叠加组合

在这样的情况下,要保证连接器运行在各个引擎的各个版本上都能完整输出日志是非常困难的,况且随着项目继续发展,连接器和执行引擎还在不断变化和扩充中。

02




目标

在作业执行过程中,日志是非常重要的问题观测和排查方法,我们迫切需要解决上述问题,以期望达到:

  • 任何连接器组合、运行在任何执行引擎的任何版本上,都能完整输出所有日志(包括各种依赖包中的日志)
  • 即使项目发展变化,连接器增加并更改,也能保持日志正确稳定的输出(只解决一次问题)

并且更进一步,我们还期望在自有的执行引擎 SeaTunnel-Zeta 中提供更好用的日志功能:

  • 动态更改日志级别
  • 串联任务执行过程的全部日志

因此我们对日志框架进行了一系列的集成改造。

03




整体设计

在 Java 日志体系中 slf4j 是当前的事实标准,它提供一套日志 API 供使用者调用发送日志,并且在运行时动态绑定到具体的日志实现框架上输出日志,同时还提供一组适配转接到各个日志框架的桥接包,总的来说它非常灵活且易于切换并且广泛被使用。
结合 Maven 的依赖管理插件,可以通过预定义相关依赖规则一次解决依赖问题,最终 SeaTunnel 选择 slf4j + slf4j bridges + maven plugins + log4j2 作为解决方案。
slf4j ecosystem

2_20230414_145614135

slf4j bridger
2_20230414_145614135


01

解决连接器之间的日志冲突


Connector 将其他所有日志相关的依赖(包含所有日志框架的 API、实现包、桥接包)都设置为 Maven-Scope(Provided),使得打包后 Connector 与其他 Connector 或各个版本的执行引擎之间不产生任何日志框架方面的冲突。

02

解决执行引擎之间的日志冲突


SeaTunnel 为每个执行引擎提供了引导器模块位于 seatunnel-core/**-starter,这里是解决不同引擎日志框架差异的最佳位置。我们根据不同执行引擎的自身内置的日志框架不同 ,提供不同的日志桥接包在提交任务时跟随 Connector 一起提交到引擎中去,使得 Connector 及其依赖包中使用其他日志框架输出的日志全部通过桥接包转接到 slf4j-api 上,之后就由每个引擎内置的日志框架承接 slf4j-api 去做日志输出。

03

Maven管理日志依赖


上述解决日志依赖冲突的方法需要逐个模块去定义 Maven-Scope(Runtime) ,维护起来非常麻烦,SeaTunnel 具有大量的连接器并且还在不断扩充中,这显然不能一次性解决问题。
maven dependencyManagement
使用 maven dependencyManagement 在项目根路径 pom.xml 中定义所有日志相关包的缺省 Scope,对所有继承的子模块生效。
maven-shade-plugin
并且还需要使用 maven pluginManagement 在项目根路径 pom.xml 中定义 maven-shade-plugin 规则,在 Starter、Connector 等模块打包阶段 exclude 日志相关 jar 包,当然子模块可以更新需求重新定义(例如 Starter 根据引擎决定 include 的桥接包)。
maven-surefire-plugin
上述方案只能解决 maven 打包之后的依赖问题,但对于工程中运行 mvn test 则无效,因为测试阶段还未打包。所以还需要再使用 maven pluginManagement 在项目根路径 pom.xml 中定义 maven-surefire-plugin 规则,在 mvn test 单元测试阶段 exclude 日志相关 jar 包。

04

SeaTunnel Zeta日志增强


升级到 log4j2
除了解决日志框架冲突之外,我们还期望更好的使用日志框架来解决观测性问题,基于日志框架的性能考虑将 log4j1 迁移到 log4j2。
动态修改日志级别
SeaTunnel-Zeta 基于 hazelcast 来启动分布式服务,我们通过集成 hazelcast logging 并扩展 hazelcast web 接口,提供运行时动态修改日志级别的功能。
任务类加载器排除日志类
SeaTunnel-Zeta 中提供了任务级别的 ChildFirstClassLoader,还需要更改此类加载规则使日志相关的包优先从 Parent ClassLoader 加载,以保证每次提交任务运行时 Connector 代码中的日志正确的链接到 SeaTunnel-Zeta 配置的日志输出中。
提供任务级别的日志跟踪
为了在 SeaTunnel-Zeta 中提供更好的日志使用体验,方便用户查询任务执行过程中输出的日志,我们计划集成 slf4j MDC 在作业调度执行过程中各个阶段去注入元信息,来串联同一个任务提交执行的过程中所有输出的日志(当然还需要考虑线程切换时拷贝 MDC 信息)。
定义 mdc 字段:

  • ZT-JID:表示作业(Job)的 ID
  • ZT-TID:表示任务(Task)的 ID
  • ZT-PID:表示任务管道(Pipeline)的 ID

然后在 log4j2 输出日志时配置如下 pattern 即可在每一行日志都带上任务相关信息:

[%X{ZT-JID, ZT-TID, ZT-PID}] [%p] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c:%L - %m%n

有了上述样式的日志元信息,就可以 grep 过滤相关 ID 的日志或接入第三方日志系统做结构化查询。

04




实施方法

01

定义日志包 dependencyManagement
在项目根路径 pom.xml 定义 dependencyManagement & dependencies,被所有子模块继承此 Maven Scope 设置

<dependencyManagement>
    <!-- ***************** slf4j & provider & bridges start ***************** -->
    <!-- Declare slf4j-api -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <!-- Declare slf4j-api provider: log4j2.x -->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>${log4j2.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>${log4j2.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>${log4j2.version}</version>
    </dependency>
    <!-- Declare log4j2 asynchronous loggers provider: disruptor -->
    <dependency>
      <groupId>com.lmax</groupId>
      <artifactId>disruptor</artifactId>
      <version>${log4j2-disruptor.version}</version>
    </dependency>
    <!-- Include the logging bridges -->
    <!-- commons-logging bridge to slf4j -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <!-- jdk-logging bridge to slf4j -->
    <!-- low performance, see: https://www.slf4j.org/legacy.html#jul-to-slf4j
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jul-to-slf4j</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    -->

    <!-- log4j1.x bridge to log4j2.x -->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-1.2-api</artifactId>
      <version>${log4j2.version}</version>
    </dependency>
    <!-- Exclude the logging bridges via provided scope -->
    <!-- log4j1.x bridge to slf4j
         Use of the SLF4J adapter (log4j-over-slf4j) together with the SLF4J bridge (slf4j-log4j12) should never be attempted as it will cause events to endlessly be routed between SLF4J and Log4j 1
     -->

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- slf4j binding to log4j1.x -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- log4j2.x binding to slf4j.
         Use of the SLF4J adapter (log4j-to-slf4j-2.x.jar) together with the SLF4J bridge (log4j-slf4j-impl-2.x.jar) should never be attempted as it will cause events to endlessly be routed between SLF4J and Log4j 2
    -->

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-to-slf4j</artifactId>
      <version>${log4j2.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- slf4j binding to jdk-logging -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jdk14</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- slf4j binding to commons-logging -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jcl</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- slf4j binding to nop -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-nop</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- slf4j binding to simple -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- Exclude other logging provider via provided scope -->
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>${commons-logging.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>${log4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>${logback.version}</version>
      <scope>provided</scope>
    </dependency>
    <!-- ***************** slf4j & provider & bridges end ***************** -->
</dependencyManagement>
<dependencies>
    <!-- ***************** slf4j & provider & bridges start ***************** -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-1.2-api</artifactId>
    </dependency>
    <!-- ***************** slf4j & provider & bridges end ***************** -->
</dependencies>


02

义maven-shade-plugin插件规则

在项目根路径 pom.xml 定义 maven-shade-plugin 规则,所有子模块做 shade 包时继承使用

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>${maven-shade-plugin.version}</version>
    <configuration>
        <artifactSet>
            <excludes>
                <exclude>org.slf4j:*</exclude>
                <exclude>ch.qos.logback:*</exclude>
                <exclude>log4j:*</exclude>
                <exclude>org.apache.logging.log4j:*</exclude>
                <exclude>commons-logging:*</exclude>
            </excludes>
        </artifactSet>
    </configuration>
</plugin>


03

定义 maven-surefire-plugin 插件规则

在项目根路径 pom.xml 定义 maven-surefire-plugin 规则用于各个模块单元测试使用

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${maven-surefire-plugin.version}</version>
    <configuration>
        <classpathDependencyExcludes>
            <!--
                The logger provider & bridges declared under 'provided' scope should be explicitly excluded from testing as below.
            -->

            <classpathDependencyExclude>org.slf4j:slf4j-jdk14</classpathDependencyExclude>
            <classpathDependencyExclude>org.slf4j:slf4j-jcl</classpathDependencyExclude>
            <classpathDependencyExclude>org.slf4j:slf4j-nop</classpathDependencyExclude>
            <classpathDependencyExclude>org.slf4j:slf4j-simple</classpathDependencyExclude>
            <classpathDependencyExclude>org.slf4j:slf4j-reload4j</classpathDependencyExclude>
            <classpathDependencyExclude>org.slf4j:slf4j-log4j12</classpathDependencyExclude>
            <classpathDependencyExclude>org.slf4j:log4j-over-slf4j</classpathDependencyExclude>
            <classpathDependencyExclude>commons-logging:commons-logging</classpathDependencyExclude>
            <classpathDependencyExclude>log4j:log4j</classpathDependencyExclude>
            <classpathDependencyExclude>ch.qos.logback:logback-classic</classpathDependencyExclude>
            <classpathDependencyExclude>ch.qos.logback:logback-core</classpathDependencyExclude>
            <classpathDependencyExclude>org.apache.logging.log4j:log4j-to-slf4j</classpathDependencyExclude>
        </classpathDependencyExcludes>
    </configuration>
</plugin>


04

Starter 模块打包桥接包

Spark Starter

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <configuration>
        <artifactSet>
            <excludes>
                <!--
                    Spark(2.x) server lib already include:
                        slf4j-api
                        log4j
                        slf4j-log4j12
                        jul-to-slf4j
                        jcl-over-slf4j

                    Spark(3.x) server lib already include:
                        slf4j-api
                        log4j-api
                        log4j-core
                        log4j-slf4j-impl
                        log4j-1.2-api
                        jul-to-slf4j
                        jcl-over-slf4j
                -->

                <exclude>org.slf4j:slf4j-api</exclude>
                <exclude>org.slf4j:slf4j-jdk14</exclude>
                <exclude>org.slf4j:slf4j-jcl</exclude>
                <exclude>org.slf4j:slf4j-nop</exclude>
                <exclude>org.slf4j:slf4j-simple</exclude>
                <exclude>org.slf4j:slf4j-reload4j</exclude>
                <exclude>org.slf4j:slf4j-log4j12</exclude>
                <exclude>org.slf4j:jcl-over-slf4j</exclude>
                <exclude>org.slf4j:jul-to-slf4j</exclude>
                <!-- spark2.x use slf4j + log4j1.x -->
                <exclude>org.slf4j:log4j-over-slf4j</exclude>
                <exclude>log4j:*</exclude>
                <exclude>commons-logging:*</exclude>
                <exclude>ch.qos.logback:*</exclude>
                <exclude>org.apache.logging.log4j:log4j-api</exclude>
                <exclude>org.apache.logging.log4j:log4j-core</exclude>
                <exclude>org.apache.logging.log4j:log4j-slf4j-impl</exclude>
                <!-- spark3.x use slf4j + log4j2.x -->
                <exclude>org.apache.logging.log4j:log4j-to-slf4j</exclude>
            </excludes>
        </artifactSet>
    </configuration>
</plugin>

Flink Starter

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <configuration>
        <artifactSet>
            <excludes>
                <!--
                    not excluded:
                        jcl-over-slf4j(commons-logging to slf4j bridge)

                    Flink server lib already include:
                        slf4j-api
                        log4j-api
                        log4j-core
                        log4j-slf4j-impl
                        log4j-1.2-api
                -->

                <exclude>org.slf4j:slf4j-api</exclude>
                <exclude>org.slf4j:slf4j-jdk14</exclude>
                <exclude>org.slf4j:slf4j-jcl</exclude>
                <exclude>org.slf4j:slf4j-nop</exclude>
                <exclude>org.slf4j:slf4j-simple</exclude>
                <exclude>org.slf4j:slf4j-reload4j</exclude>
                <exclude>org.slf4j:slf4j-log4j12</exclude>
                <exclude>org.slf4j:log4j-over-slf4j</exclude>
                <exclude>log4j:*</exclude>
                <exclude>commons-logging:*</exclude>
                <exclude>ch.qos.logback:*</exclude>
                <exclude>org.apache.logging.log4j:log4j-api</exclude>
                <exclude>org.apache.logging.log4j:log4j-core</exclude>
                <exclude>org.apache.logging.log4j:log4j-slf4j-impl</exclude>
                <exclude>org.apache.logging.log4j:log4j-1.2-api</exclude>
                <exclude>org.apache.logging.log4j:log4j-to-slf4j</exclude>
            </excludes>
        </artifactSet>
    </configuration>
</plugin>

SeaTunnel Starter(SeaTunnel-Zeta)

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <configuration>
        <artifactSet>
            <excludes>
                <!--
                    not excluded:
                        slf4j-api
                        log4j2-api
                        log4j2-core
                        log4j-slf4j-impl
                        log4j-1.2-api(log4j1.x to log4j2.x bridge)
                        jcl-over-slf4j(commons-logging to slf4j bridge)
                -->

                <exclude>org.slf4j:slf4j-jdk14</exclude>
                <exclude>org.slf4j:slf4j-jcl</exclude>
                <exclude>org.slf4j:slf4j-nop</exclude>
                <exclude>org.slf4j:slf4j-simple</exclude>
                <exclude>org.slf4j:slf4j-reload4j</exclude>
                <exclude>org.slf4j:slf4j-log4j12</exclude>
                <exclude>org.slf4j:log4j-over-slf4j</exclude>
                <exclude>log4j:*</exclude>
                <exclude>commons-logging:*</exclude>
                <exclude>ch.qos.logback:*</exclude>
                <exclude>org.apache.logging.log4j:log4j-to-slf4j</exclude>
            </excludes>
        </artifactSet>
    </configuration>
</plugin>


05




完整的功能issue&PR

  • https://github.com/apache/incubator-seatunnel/issues/2725
  • https://github.com/apache/incubator-seatunnel/pull/3025
  • https://github.com/apache/incubator-seatunnel/pull/2722


06




参考

  • https://www.slf4j.org/manual.html#swapping
  • https://www.slf4j.org/legacy.html
  • https://logging.apache.org/log4j/2.x/manual/migration.html