【5】示例逐行解析 Flink 的运行过程(一)

112 阅读5分钟

示例代码

看下面一段代码,后面我们从此代码开始解析。

package com.wanli;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.util.Collector;

import java.util.Random;

public class SimpleTestMain {
  public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(2); // 设置全局并行度为1,这样确保只有一个线程在运行

    DataStreamSource<TestEvent> dataStreamSource = env.addSource(new TestSource());
    dataStreamSource
        .process(new MyTestFuntion1())
        .process(new MyTestFuntion2())
        .print();

    System.out.println("ThreadId=" + Thread.currentThread().getId());
    env.execute("Test Job");
  }

  // 自定义第一个 Function
  public static class MyTestFuntion1 extends ProcessFunction<TestEvent, TestEvent> {
    public MyTestFuntion1() {
      System.out.println("ObjectHash=" + this.hashCode());
    }

    @Override
    public void processElement(TestEvent value, ProcessFunction<TestEvent, TestEvent>.Context ctx,
        Collector<TestEvent> out) throws Exception {
      value.setName(value.getName() + "-T" + Thread.currentThread().getId() + "-O" + this.hashCode());
      out.collect(value);
    }
  }


  // 自定义第二个 Function
  public static class MyTestFuntion2 extends ProcessFunction<TestEvent, TestEvent> {
    public MyTestFuntion2() {
      System.out.println("ObjectHash=" + this.hashCode());
    }

    @Override
    public void processElement(TestEvent value, ProcessFunction<TestEvent, TestEvent>.Context ctx,
        Collector<TestEvent> out) throws Exception {
      value.setName(value.getName() + "-T" + Thread.currentThread().getId() + "-O" + this.hashCode());
      out.collect(value);
    }
  }


  // 定义事件
  @Data
  public static final class TestEvent {
    private final long time = System.currentTimeMillis();
    private String name;
    private int value;

    @Override
    public String toString() {
      return "name=" + name + ", value=" + value + ", time=" + time;
    }
  }


  // 自定义数据源
  public static final class TestSource implements SourceFunction<TestEvent> {
    private static final String[] NAMES = {"Zhangsan", "Lisi", "Wangwu", "Liuliu"};
    private static final Random RANDOM = new Random(System.currentTimeMillis());
    private boolean closed = false;

    @Override
    public void run(SourceContext<TestEvent> ctx) throws Exception {
      while (!closed) {
        Thread.sleep(1000L);
        TestEvent event = new TestEvent();
        event.setName(NAMES[RANDOM.nextInt(NAMES.length)]);
        event.setValue(RANDOM.nextInt(1000));
        ctx.collect(event);
      }
      log.info("source closed.");
    }

    @Override
    public void cancel() {
      closed = true;
    }
  }
}

代码解释:

  • 在自定义数据源中,每秒随机返回一个 TestEvent 类型的事件。
  • 为了避免线程干扰,我们一开始使用:env.setParallelism(1) 来设置全局并行度为1,保证只有一个线程在运行。
  • MyTestFunction1 和 MyTestFunction2 里面分别添加了各自的线程 ID 和对象 ID,最后一起输出。

当执行 env.execute("Test Job")

将断点打在 env.execute() 这一行并执行 debug,此时我们看到关键信息如下:

image.png

可以看到,env 里面有三个 transformations,分别对应第一个 Function 的处理、第二个 Function 的处理和 print 的处理。

展开这三个 transformation,可以看到它们各自的 input 来源 transformation:

image.png

可以很清楚的看到,第一个 transformation 是第二个的输入,同理,第二个 transformation 是第三个的输入。

那第一个 transformation 的输入是谁呢?我们展开 dataStreamSource 对象:

image.png

可以看到,对象编号为 1532 的正好是 dataStreamSource 的 transformation,即输入数据源。

如此,从输入开始,就将 transformation 串联起来作为一个 pipeline 了。

对象的变化

为了更好看清对象的变化,我们将并行度设置为 2:

env.setParallelism(2);

运行几秒钟,结果如下:

ObjectHash=633070006
ObjectHash=2114650936
ThreadId=1
1> name=Liuliu-T68-O2140958286-T68-O1954221341, value=968, time=1721645957561
2> name=Zhangsan-T67-O551159643-T67-O2105574944, value=316, time=1721645958747
1> name=Zhangsan-T68-O2140958286-T68-O1954221341, value=559, time=1721645959754
2> name=Lisi-T67-O551159643-T67-O2105574944, value=812, time=1721645960760

从运行结果我们可以看到:

  • main 方法中的线程 ID 为 1;
  • 在 main 方法中创建的 MyTestFunction1MyTestFunction2 的对象 hash 值分别为 633070006 和 2114650936
  • 我们有两个并行度,前面的输出序号分别为 1>2>
  • 两个并行度对应了两个线程,线程号分别对应 67 和 68,说明两个 Function 发生了算子链合并;
  • 线程 67 和 68 中的 MyTestFunction1MyTestFunction2 的对象 hash 值是独立的,在线程里面不变化的,即两个线程分别有各自的 Function 对象;
  • 两个线程中的对象与 main 方法中的对象不是同一个;
  • 在两个线程中,没有调用 MyTestFunction1MyTestFunction2 的构造方法。

从以上的现象,我们得出结论:

  • main 线程我们前面知道,是在 flink client 或者 flink JobManager 中执行的,而 Function 的线程是在 TaskManager 中执行的,是不同的线程;
  • 一个并行度对应一个单线程;
  • 在 main 方法中创建的 Function 对象,是通过序列化和反序列化的方式复制到 TaskManager 的,且不会调用构造方法。

我们禁用算子链合并再运行一次:

// 在 env.setParallelism(2); 下面加上
env.disableOperatorChaining();

运行结果:

ObjectHash=633070006
ObjectHash=2114650936
ThreadId=1
2> name=Lisi-T72-O1137715813-T73-O2014470780, value=273, time=1721806002785
1> name=Liuliu-T74-O1514615514-T75-O497397811, value=578, time=1721806003993
2> name=Lisi-T72-O1137715813-T73-O2014470780, value=820, time=1721806004998
1> name=Wangwu-T74-O1514615514-T75-O497397811, value=39, time=1721806006004

可以看到,相同序号的不同 Function 对应的线程 ID 不一样,此时是生成了两个 Task。