Flink与Spring Boot的集成

  • flink 1.10.0
  • spring boot 2.2.2.RELEASE

这方面的资料很少,网上有的方法不完整。基本思路是把spring容器的初始化放在sink的open方法中执行。

要么只使用spring framework组件,甚至使用xml这样的方式配置bean(使用ClassPathXmlApplicationContext );要么直接在open中启动了SpringApplication。有可能在单机的flink上能运行,但是on yarn的时候不行了。

其实想要达到的目的很简单:

  • 使用spring的IoC容器管理组件
  • 注解配置和自动配置
  • 使用springboot的外部化配置(application.yml)
  • 需要可以运行在yarn集群中

但是里面的坑非常多,除了需要了解一些flink的任务提交部署原理,需要对spring framework, spring boot, maven,hadoop yarn有一些了解。有些地方需要深入了解,否则莫名其妙入坑,半天爬不出来。所以需要记录一下,已方便后来者别再浪费时间。Flink的官方文档真的是很简短。

Spring容器的集成点

需要算子(Operator)具体Function(Source、Sink)的初始化中,因为这些算子会被序列化到分布式计算节点中执行。所以通常的main只是任务提交的入口,并不是最终算子执行初始化入口。

所以,在Source和Sink的open方法中初始化容器。由于通常Source都由比较固定的组件,比如kafka集成了FlinkKafkaConsumer,所以这部分没有过多的需要编写处理逻辑,从而没有引入spring容器。但是,初始化这部分组件有配置参数传递的需求。

依赖jar包注意点

引入flink或者hadoop等等这种运行时会提供的jar包时,记得把作用域置scope设为provided

典型的flink依赖包引入如下:

<dependency>
  <groupId>org.apache.flinkgroupId>
  <artifactId>flink-javaartifactId>
  <version>1.10.0version>
  <scope>providedscope>
dependency>
<dependency>
  <groupId>org.apache.flinkgroupId>
  <artifactId>flink-streaming-java_2.11artifactId>
  <version>1.10.0version>
  <scope>providedscope>
dependency>

如果是kafka连接器这样的依赖,非flink核心依赖,则是需要打包时打进去的,使用默认的scope就行。

<dependency>
    <groupId>org.apache.flinkgroupId>
    <artifactId>flink-connector-kafka-0.10_2.11artifactId>
    <version>1.10.0version>
dependency>

这里是一个自定义Sink的例子,在open中初始化spring的容器。

@Slf4j
public class MySink extends RichSinkFunction<String> {
     

    private AnnotationConfigApplicationContext ctx;

    public MySink(){
     
        log.info("MySink new");
    }

    @Override
    public void open(Configuration parameters) throws Exception {
     
        this.ctx = new AnnotationConfigApplicationContext(Config.class);
        log.info("MySink open");
      
        // 这里获取了配置的数据源
        DataSource ds = ctx.getBean(DataSource.class);
        log.info("----------test info------------{}",ds);
    }

    @Override
    public void invoke(String value, Context context) throws Exception {
     
      	//
        log.info(value);
    }

    @Override
    public void close() throws Exception {
     
       // 关闭容器
        ctx.close();
        log.info("MySink close");
    }
}

Flink的入口类,这里的是标准的Flink初始化步骤。

@Slf4j
public class DemoApplication {
     

    public static void main(String[] args) throws Exception {
     
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 有限的流
				// DataStream stream = env.fromElements("1", "2", "3");
				// 这里模拟一个持续发送数据的源
        DataStream<String> stream = env.addSource(new SourceFunction<String>() {
     

            @Override
            public void run(SourceContext<String> sourceContext) throws Exception {
     
                long c = 0;
                while(true) {
     
                    sourceContext.collect("test"+ c++);
                    Thread.sleep(3000);
                }
            }

            @Override
            public void cancel() {
     }
        });

        stream.addSink(new MySink());
        env.execute("spring flink demo");
    }
}

maven打包注意点

项目继承spring的parent后,需要覆盖打包插件配置:

这个配置源于flink官网的打包模板,并做了集成Spring必要的修改。

<plugin>
  <groupId>org.apache.maven.pluginsgroupId>
  <artifactId>maven-shade-pluginartifactId>
  <version>3.1.1version>
  <executions>
    <execution>
      <phase>packagephase>
      <goals>
        <goal>shadegoal>
      goals>
      <configuration>
        <artifactSet>
          <excludes>
            <exclude>com.google.code.findbugs:jsr305exclude>
            <exclude>org.slf4j:*exclude>
            <exclude>log4j:*exclude>
          excludes>
        artifactSet>
        <filters>
          <filter>
            
            <artifact>*:*artifact>
            <excludes>
              <exclude>META-INF/*.SFexclude>
              <exclude>META-INF/*.DSAexclude>
              <exclude>META-INF/*.RSAexclude>
            excludes>
          filter>
        filters>
        <transformers>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.handlersresource>
          transformer>
          <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
            <resource>META-INF/spring.factoriesresource>
          transformer>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.schemasresource>
          transformer>
          <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
          <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>xx.demo.DemoApplicationmainClass>
          transformer>
        transformers>
      configuration>
    execution>
  executions>
  <dependencies>
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-maven-pluginartifactId>
      <version>2.2.2.RELEASEversion>
    dependency>
  dependencies>
plugin>

注意包冲突

典型的包冲突一般不会和发生在与flink依赖上,flink使用shade把常用依赖包打到自己的命令空间下。

比如你可以找到这样的jar包:

  • flink-shaded-asm-5.0.4-6.0.jar
  • flink-shaded-guava-18.0-6.0.jar
  • flink-shaded-jackson-2.7.9-6.0.jar

冲突往往发生在与第三方库的依赖使用上,比如hadoop,它的依赖非常多,冲突的概率就很大。

如果你的flink程序是需要提交到hadoop的yarn集群运行的话,你会遇到snake yml解析器的版本冲突问题。

spring 5.x 使用 snakeyaml-1.25.jar ,而hadoop 3.1.x的yarn lib中则使用了snakeyaml-1.16.jar。

这个导致你在flink client中执行时(就是main方法中执行),如果想使用spring的yml配置解析加载功能无法正确执行。但是它不会影响到提交到yarn中的job中的运行。在sink中open方法中执行spring的容器初始化,程序是可以正常工作的。

目前方案的缺陷与解决思路

上述方法存在的缺陷是,无法拿到main执行期间的Environment,这样无法使用Spring Boot的环境参数、命令参数覆盖配置。这一特性缺失,使应用的启动的灵活性大大降低了。只能手工从main把需要的参数传递到实际的Sink实例或者Source上。

应该有办法可以把SpringApplication的启动环境配置序列化保存,后移到flink的task启动的时候反序列化,然后传递给SpringApplication,这样就可以完美实现Spring Boot与Flink的完美集成了。有空看看SpringApplication启动的源码应该能找到办法。

你可能感兴趣的:(spring,boot,flink)