Flink 源码解析1--StreamGraph的生成

1. 先来简单看一下入门的WordCount程序

1、首先数据源会产生随机的数字数据流(0-10内的数字)形式,然后通过flink的transformation将数据进行单词计数,再print输出

// wordcount 计数demo代码
public class WordCountDemo {
    public static void main(String[] args) throws Exception {

        // conf设置,避免调试的时候心跳超时
        Configuration configuration = new Configuration();
        configuration.setLong(HeartbeatManagerOptions.HEARTBEAT_INTERVAL, 10 * 60 * 1000);
        configuration.setLong(HeartbeatManagerOptions.HEARTBEAT_TIMEOUT, 20 * 60 * 1000);
        configuration.setString(AkkaOptions.ASK_TIMEOUT, "600 s");
        configuration.setString(AkkaOptions.WATCH_HEARTBEAT_INTERVAL, "600 s");
        configuration.setString(AkkaOptions.WATCH_HEARTBEAT_PAUSE, "1000 s");
        configuration.setString(AkkaOptions.TCP_TIMEOUT, "600 s");
        configuration.setString(AkkaOptions.TRANSPORT_HEARTBEAT_INTERVAL, "600 s");
        configuration.setString(AkkaOptions.LOOKUP_TIMEOUT, "600 s");
        configuration.setString(AkkaOptions.CLIENT_TIMEOUT, "600 s");
        configuration.setLong(WebOptions.TIMEOUT, 10 * 60 * 1000L);

        // local环境初始化
        StreamExecutionEnvironment streamExecutionEnvironment = StreamExecutionEnvironment.createLocalEnvironment(1, configuration);

        // 添加自定义的数据源(每秒随机产生一个范围在(0-10)的数字)
        DataStream<Integer> dataStream = streamExecutionEnvironment.addSource(new RandomSource()).returns(Integer.class);
        dataStream.map(new MapFunction<Integer, Tuple2<String, Integer>>() { // 生成 k-v对
            @Override
            public Tuple2<String, Integer> map(Integer value) throws Exception {
                return new Tuple2<>("i" + value, 1);
            }
        }).keyBy(0).timeWindow(Time.seconds(5)).reduce(new ReduceFunction<Tuple2<String, Integer>>() { // 每5s按照key进行统计计数
            @Override
            public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
                value2.f1 = value1.f1 + value2.f1;
                return value2;
            }
        }).filter(new FilterFunction<Tuple2<String, Integer>>() {
            @Override
            public boolean filter(Tuple2<String, Integer> value) throws Exception {
                return value.f0.compareTo("i5") != 0;
            }
        }).print();
        streamExecutionEnvironment.execute("word count run");
    }
}

// 数据源产生器
public class RandomSource extends RichSourceFunction<Integer> {
    private Random random;
    private volatile Boolean running = false;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        random = new Random();
        running = true;
    }

    @Override
    public void close() throws Exception {
        super.close();
        random = null;
    }

    @Override
    public void run(SourceContext<Integer> sourceContext) throws Exception {
        while (running) {
            Integer out = random.nextInt(10);
            sourceContext.collect(out);
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        running = false;
    }
}

在数据源产生器中,主要关注运行的run方法,其会源源不断的产生随机的数据并形成dataStream供后续flink transformation的操作

1、StreamExecutionEnvironment简单说明

        以dataStream.map(MapFunction(...))作为flink算子操作分析的入口,查看StreamExecutionEnvironment源码可以知道流式计算环境中几个重要的数据结构;

StreamExecutionEnvironment中包含:

  • ExecutionConfig config = new ExecutionConfig(); // 程序执行时的配置;主要包括默认并行度,序列化器,执行模式(Batch or Pipelined)等等;
  • CheckpointConfig checkpointCfg = new CheckpointConfig(); // ckp检查点相关的配置;主要包括检查点模式(EXACTLY_ONCE or AT_LEAST_ONCE),检查点时间间隔,检查点在程序退出后是否删除等等;
  • StateBackend defaultStateBackend; // 流式计算中设置存储中间状态、检查点、保存点的存储介质(MemoryStateBackend,FsStateBackend,RocksDBStateBackend等);
  • TimeCharacteristic timeCharacteristic; // 流式计算中的处理时间设置
  • List<Tuple2<String, DistributedCache.DistributedCacheEntry>> cacheFile; // 需要分布式缓存的文件
  • List<StreamTransformation<?>> transformations = new ArrayList<>(); // 存储用户定义的flink transformation的算子操作

其下有四个实现的子类,主要是 本地执行环境、远程执行环境、CliContext交互式执行环境、Plan-web端执行环境;

在调用了execute()方法后,其具体执行由子类实现,子类实现大致逻辑:获取

  • 生成StreamGraph。代表程序的拓扑结构,是从用户代码定义的transformation算子直接生成的图。
  • 生成JobGraph。这个图是要交给flink去生成task的图。
  • 生成一系列配置
  • 将JobGraph和配置交给flink集群去运行。如果不是本地运行的话,还会把jar文件通过网络发给其他节点。
  • 以本地模式运行的话,可以看到启动过程,如启动性能度量、web模块、JobManager、ResourceManager、taskManager等等
  • 启动任务。值得一提的是在启动任务之前,先启动了一个用户类加载器,这个类加载器可以用来做一些在运行时动态加载类的工作。

2、flink算子(Operator)的注册、声明

       在程序中以env.addSource()和dataStream.map(...)来跟踪分析flink transformation算子的注册、声明streamExecutionEnvironment.addSource(new RandomSource()).returns(Integer.class); // 添加自定义的输入源跟踪源代码可以知道

public <OUT> DataStreamSource<OUT> addSource(SourceFunction<OUT> function, String sourceName, TypeInformation<OUT> typeInfo) {
  if (typeInfo == null) { // 根据数据源方法反射解析数据的输出类型
    if (function instanceof ResultTypeQueryable) {
      typeInfo = ((ResultTypeQueryable<OUT>) function).getProducedType();
    } else {
      try {
        typeInfo = TypeExtractor.createTypeInfo(
            SourceFunction.class,
            function.getClass(), 0, null, null);
      } catch (final InvalidTypesException e) {
        typeInfo = (TypeInformation<OUT>) new MissingTypeInfo(sourceName, e);
      }
    }
  }
  boolean isParallel = function instanceof ParallelSourceFunction; // 是否并行

  clean(function);
  StreamSource<OUT, ?> sourceOperator;
  if (function instanceof StoppableFunction) {
    sourceOperator = new StoppableStreamSource<>(cast2StoppableSourceFunction(function));
  } else {
    sourceOperator = new StreamSource<>(function); // 构建包装StreamSource数据产生源
  }
  return new DataStreamSource<>(this, typeInfo, sourceOperator, isParallel, sourceName); // 构建返回 DataStreamSource 数据源
}

        DataStreamSource数据产生源中,使用SourceTransformation类封装了StreamSource类,其包含用户自定义的SourceFunction;后续flink transformation算子转换操作的数据源头DataStream具体子类就是DataStreamSource;接下来进行.map()算子操作的分析:

public <R> SingleOutputStreamOperator<R> map(MapFunction<T, R> mapper) {

   TypeInformation<R> outType = TypeExtractor.getMapReturnTypes(clean(mapper), getType(),
         Utils.getCallLocationName(), true); 

   return transform("Map", outType, new StreamMap<>(clean(mapper))); 
}


public <R> SingleOutputStreamOperator<R> transform(String operatorName, TypeInformation<R> outTypeInfo, OneInputStreamOperator<T, R> operator) {

   // read the output type of the input Transform to coax out errors about MissingTypeInfo
   transformation.getOutputType(); // 获取当前算子的输出

   OneInputTransformation<T, R> resultTransform = new OneInputTransformation<>(
         this.transformation, // 当前dataStream转换算子
         operatorName,
         operator, // 进行变换的转换操作
         outTypeInfo,
         environment.getParallelism()); // 将当前

   @SuppressWarnings({ "unchecked", "rawtypes" })
   SingleOutputStreamOperator<R> returnStream = new SingleOutputStreamOperator(environment, resultTransform);

   getExecutionEnvironment().addOperator(resultTransform);

   return returnStream;
}

        首先会根据当前DataStream也就是初始的DataStreamSource的输出类型以及当前的转换函数MapFunction去确定当前Map transformation算子转换之后的输出类型; 然后将当前Map transformation算子使用OneInputTransformation类封装了StreamMap类,其包含用户自定义的MapFunction,并将其添加到streamExecutionEnvironment中的transformations算子列表中;并将SingleOutputStreamOperator作为map算子作用后输出的DataStream;

        在算子中,每一个StreamTransformation代表了生成 DataStream 的操作,每一个 DataStream 的底层都有对应的一个 StreamTransformation。在 DataStream 上面通过 map 等算子不断进行转换,就得到了由 StreamTransformation 构成的图。当需要执行的时候,底层的这个图就会被转换成 StreamGraph。StreamTransformation 在运行时并不一定对应着一个物理转换操作,有一些操作只是逻辑层面上的,比如 split/select/partitioning 等。

        每一个 StreamTransformation 都有一个关联的 Id,这个 Id 是全局递增的。除此以外,还有 uid, slotSharingGroup, parallelism 等信息。

        StreamTransformation有很多具体的子类,如SourceTransformation、OneInputStreamTransformation、TwoInputTransformation、SideOutputTransformation、SinkTransformation等等,这些分别对应了DataStream 上的不同转换操作。

其映射过程如下所示:

最终可以根据程序有以下DataStream以及transformation算子转换形式:

        可以看到WindowStream负责生成对应的时间窗口、窗口触发器等,在flink中,其会将KeyStream、windowStream作为其下游聚合数据的来源,也就是说key-window-reduce的上下游transformation作为统一的一个transformation进行添加,其余的DataStream都会对应自己的StreamTransformation,这些StreamTransformation都会添加到streamExecutionEnvironment中的transformations算子列表(source和window相关的被下游算子持有引用):

  1. OneInputTransformation{id=2, name='Map', outputType=Java Tuple2<String, Integer>, parallelism=1};
    表示Map操作的OneInputTransformation对象,其input属性指向的是数据源的转换SourceTransformation;
  2. OneInputTransformation{id=4, name='Window(TumblingProcessingTimeWindows(5000), ProcessingTimeTrigger, ReduceFunction$2, PassThroughWindowFunction)', outputType=Java Tuple2<String, Integer>, parallelism=1};
    表示reduce操作的OneInputTransformation对象,其input属性指向的是表示keyBy的转换PartitionTransformation,而PartitionTransformation的input属性指向的是Map转换的OneInputTransformation;
  3. OneInputTransformation{id=5, name='Filter', outputType=Java Tuple2<String, Integer>, parallelism=1};
    表示filter操作的OneInputTransformation对象,其input属性指向的是reduce转化的OneInputTransformation对象;
  4. SinkTransformation{id=6, name='Print to Std. Out', outputType=GenericType<java.lang.Object>, parallelism=1};
    表示sink操作对应的SinkTransformation对象,其input属性指向的是fliter转化的OneInputTransformation对象;

3、StreamGraph的生成

        在transformations算子被注册、声明后,env.executor()会调用StreamGraph streamGraph = getStreamGraph(); 其会调用基类的streamGraph生成方法;StreamGraphGenerator.generate(env, transformations);其中的transformations就是记录我们在流式转换的transform方法中放进来的转换算子的list列表;在遍历List<StreamTransformation>生成StreamGraph的时候,会递归调用StreamGraphGenerator#transform方法。对于每一个StreamTransformation,确保当前其上游已经完成转换。     

       StreamTransformations会被转换为StreamGraph中的节点StreamNode,并为上下游节点添加连接两个StreamNode节点的数据流边StreamEdge。其中涉及到StreamGraph生成的代码主要为两大块:

private Collection<Integer> transform(StreamTransformation<?> transform) { // 针对单个transformation进行转化

   if (alreadyTransformed.containsKey(transform)) { // 由于是递归调用的,可能已经完成了转换,避免多次重复转化
      return alreadyTransformed.get(transform);
   }

   LOG.debug("Transforming " + transform);

   if (transform.getMaxParallelism() <= 0) {

      // if the max parallelism hasn't been set, then first use the job wide max parallelism
      // from theExecutionConfig.
      int globalMaxParallelismFromConfig = env.getConfig().getMaxParallelism();
      if (globalMaxParallelismFromConfig > 0) {
         transform.setMaxParallelism(globalMaxParallelismFromConfig);
      }
   }

   // call at least once to trigger exceptions about MissingTypeInfo
   transform.getOutputType();

   // 这里对操作符的类型进行判断,并调用相应转换操作的图处理逻辑
   // 主要核心功能是递归的将该节点和节点的上游节点加入图
   Collection<Integer> transformedIds;
   if (transform instanceof OneInputTransformation<?, ?>) {
      transformedIds = transformOneInputTransform((OneInputTransformation<?, ?>) transform);
   } else if (transform instanceof TwoInputTransformation<?, ?, ?>) {
      transformedIds = transformTwoInputTransform((TwoInputTransformation<?, ?, ?>) transform);
   } else if (transform instanceof SourceTransformation<?>) {
      transformedIds = transformSource((SourceTransformation<?>) transform);
   } else if (transform instanceof SinkTransformation<?>) {
      transformedIds = transformSink((SinkTransformation<?>) transform);
   } else if (transform instanceof UnionTransformation<?>) {
      transformedIds = transformUnion((UnionTransformation<?>) transform);
   } else if (transform instanceof SplitTransformation<?>) {
      transformedIds = transformSplit((SplitTransformation<?>) transform);
   } else if (transform instanceof SelectTransformation<?>) {
      transformedIds = transformSelect((SelectTransformation<?>) transform);
   } else if (transform instanceof FeedbackTransformation<?>) {
      transformedIds = transformFeedback((FeedbackTransformation<?>) transform);
   } else if (transform instanceof CoFeedbackTransformation<?>) {
      transformedIds = transformCoFeedback((CoFeedbackTransformation<?>) transform);
   } else if (transform instanceof PartitionTransformation<?>) {
      transformedIds = transformPartition((PartitionTransformation<?>) transform);
   } else if (transform instanceof SideOutputTransformation<?>) {
      transformedIds = transformSideOutput((SideOutputTransformation<?>) transform);
   } else {
      throw new IllegalStateException("Unknown transformation: " + transform);
   }

   // need this check because the iterate transformation adds itself before
   // transforming the feedback edges
   if (!alreadyTransformed.containsKey(transform)) {
      alreadyTransformed.put(transform, transformedIds);
   }
   // ..................... 进行一些资源和配置上的设置
   return transformedIds;
}

以最典型的transformOneInputTransform为例进行转化StreamNode节点的操作分析:

private <IN, OUT> Collection<Integer> transformOneInputTransform(OneInputTransformation<IN, OUT> transform) {

   Collection<Integer> inputIds = transform(transform.getInput()); // 首先先确保上游节点都完成转换

   // the recursive call might have already transformed this
   if (alreadyTransformed.containsKey(transform)) {
      return alreadyTransformed.get(transform);
   }

   String slotSharingGroup = determineSlotSharingGroup(transform.getSlotSharingGroup(), inputIds); // 确定资源共享组,用户如果没有指定,默认是default
   
   streamGraph.addOperator(transform.getId(),  // 向StreamGraph中添加Operator, 这一步会生成对应的StreamNode
         slotSharingGroup,
         transform.getCoLocationGroupKey(),
         transform.getOperator(),
         transform.getInputType(),
         transform.getOutputType(),
         transform.getName());

   if (transform.getStateKeySelector() != null) {
      TypeSerializer<?> keySerializer = transform.getStateKeyType().createSerializer(env.getConfig());
      streamGraph.setOneInputStateKey(transform.getId(), transform.getStateKeySelector(), keySerializer);
   }

   streamGraph.setParallelism(transform.getId(), transform.getParallelism());
   streamGraph.setMaxParallelism(transform.getId(), transform.getMaxParallelism());

   for (Integer inputId: inputIds) { // 依次连接到上游节点, 创建并生成对应的StreamEdge
      streamGraph.addEdge(inputId, transform.getId(), 0);
   }

   return Collections.singleton(transform.getId());
}

        具体的StreamNode节点生成过程,在生成StreamNode中,保存了对应的 StreamOperator (从 StreamTransformation 得到),并且还引入了变量 jobVertexClass 来表示该节点在 TaskManager 中运行时的实际任务类型;AbstractInvokable是所有可以在TaskManager中运行的任务的抽象基础类,包括流式任务和批任务。StreamTask是所有流式任务的基础类,其具体的子类包括SourceStreamTask, OneInputStreamTask, TwoInputStreamTask等等。

protected StreamNode addNode(Integer vertexID,
   String slotSharingGroup,
   @Nullable String coLocationGroup,
   Class<? extends AbstractInvokable> vertexClass,
   StreamOperator<?> operatorObject,
   String operatorName) {

   if (streamNodes.containsKey(vertexID)) {
      throw new RuntimeException("Duplicate vertexID " + vertexID);
   }

   StreamNode vertex = new StreamNode(environment,   // 创建StreamNode,这里保存了StreamOperator和vertexClass的信息
      vertexID,
      slotSharingGroup,
      coLocationGroup,
      operatorObject,
      operatorName,
      new ArrayList<OutputSelector<?>>(),
      vertexClass);

   streamNodes.put(vertexID, vertex);

   return vertex;
}

        前面提到,在每一个物理节点的转换上,会调用 StreamGraph#addEdge 在输入节点和当前节点之间建立边的连接;对于一些不包含物理转换操作的 StreamTransformation,如 Partitioning, split/select, union,并不会生成 StreamNode,而是生成一个带有特定属性的虚拟节点。当添加一条有虚拟节点指向下游节点的边时,会找到虚拟节点上游的物理节点,在两个物理节点之间添加边,并把虚拟转换操作的属性附着上去。

private void addEdgeInternal(Integer upStreamVertexID,
		Integer downStreamVertexID,
		int typeNumber,
		StreamPartitioner<?> partitioner,
		List<String> outputNames,
		OutputTag outputTag) {

	//先判断是不是虚拟节点上的边,如果是,则找到虚拟节点上游对应的物理节点
	//在两个物理节点之间添加边,并把对应的 StreamPartitioner,或者 OutputTag 等补充信息添加到StreamEdge中
	if (virtualSideOutputNodes.containsKey(upStreamVertexID)) {
		......
	} else if (virtualPartitionNodes.containsKey(upStreamVertexID)) {
		int virtualId = upStreamVertexID;
		upStreamVertexID = virtualPartitionNodes.get(virtualId).f0;
		if (partitioner == null) {
			partitioner = virtualPartitionNodes.get(virtualId).f1;
		}
		addEdgeInternal(upStreamVertexID, downStreamVertexID, typeNumber, partitioner, outputNames, outputTag);
	} else {
		//两个物理节点
		StreamNode upstreamNode = getStreamNode(upStreamVertexID);
		StreamNode downstreamNode = getStreamNode(downStreamVertexID);

		// If no partitioner was specified and the parallelism of upstream and downstream
		// operator matches use forward partitioning, use rebalance otherwise.
		if (partitioner == null && upstreamNode.getParallelism() == downstreamNode.getParallelism()) {
			partitioner = new ForwardPartitioner<Object>();
		} else if (partitioner == null) {
			partitioner = new RebalancePartitioner<Object>();
		}

		if (partitioner instanceof ForwardPartitioner) {
			if (upstreamNode.getParallelism() != downstreamNode.getParallelism()) {
				throw new UnsupportedOperationException("Forward partitioning does not allow " +
						"change of parallelism. Upstream operation: " + upstreamNode + " parallelism: " + upstreamNode.getParallelism() +
						", downstream operation: " + downstreamNode + " parallelism: " + downstreamNode.getParallelism() +
						" You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global.");
			}
		}

		//创建 StreamEdge,保留了StreamPartitioner等属性
		StreamEdge edge = new StreamEdge(upstreamNode, downstreamNode, typeNumber, outputNames, partitioner, outputTag);

		//分别将StreamEdge添加到上游节点和下游节点
		getStreamNode(edge.getSourceId()).addOutEdge(edge);
		getStreamNode(edge.getTargetId()).addInEdge(edge);
	}
}
//StreamGraphGenerator#transformPartition
private <T> Collection<Integer> transformPartition(PartitionTransformation<T> partition) {
	StreamTransformation<T> input = partition.getInput();
	List<Integer> resultIds = new ArrayList<>();

	//递归地转换上游节点 (若上游已转换则直接返回转换ids)
	Collection<Integer> transformedIds = transform(input);

	for (Integer transformedId: transformedIds) {
		int virtualId = StreamTransformation.getNewNodeId();
		//添加虚拟的 Partition 节点
		streamGraph.addVirtualPartitionNode(transformedId, virtualId, partition.getPartitioner());
		resultIds.add(virtualId);
	}

	return resultIds;
}

// StreamGraph#addVirtualPartitionNode
public void addVirtualPartitionNode(Integer originalId, Integer virtualId, StreamPartitioner<?> partitioner) {

	if (virtualPartitionNodes.containsKey(virtualId)) {
		throw new IllegalStateException("Already has virtual partition node with id " + virtualId);
	}

	//添加一个虚拟节点,后续添加边的时候会连接到实际的物理节点
	virtualPartitionNodes.put(virtualId,
		new Tuple2<Integer, StreamPartitioner<?>>(originalId, partitioner));
}

针对上一节分析的WordCountDemo代码进行转化操作具体分析:1、针对第一个Map操作进行具体生成StreamNode节点的分析如下:

1、OneInputTransformation{id=2, name='Map', outputType=Java Tuple2<String, Integer>, parallelism=1};
      表示Map操作的OneInputTransformation对象,其input属性指向的是数据源的转换SourceTransformation;

  • 其会先第一步对其Map#OneInputTransformation的上游输入input#SourceTransformation进行生成StreamNode节点处理;其会进一步调用streamGraph.addSource(),添加Source所对应的StreamNode节点,其会包括Source源#StreamOperator和可执行任务类型jobVertexClass#SourceStreamTask;
  • 后面则会对Map#OneInputTransformation当前算子进行处理,生成StreamNode节点;其会包括StreamMap#MapFunction和可执行任务类型jobVertexClass#OneInputStreamTask;
  • 之后便会对当前算子Map以及其上游输入Source算子进行StreamEdge的数据连接边的添加;最终形成Node#Source-- StreamEdge--Node#Map 这样的形式;

StreamNode#Source(Custom Source-1;inEdges=null, outEdges=StreamEdge#(sourceVertex=StreamNode#Source, targetVertex=StreamNode#Map))

               StreamEdge#(Source: Custom Source-1 -> Map-2, typeNumber=0, selectedNames=[], outputPartitioner=FORWARD, outputTag=null)

StreamNode#Map(Map-2;inEdges=StreamEdge#(sourceVertex=StreamNode#Source, targetVertex=StreamNode#Map), outEdges=null))

 

所有针对上诉转化代码,有最终的StreamGraph 图拓扑结构形成如下:
如下transformations算子列表(source和window相关的被下游算子持有引用):

1、OneInputTransformation{id=2, name='Map', outputType=Java Tuple2<String, Integer>, parallelism=1};
      表示Map操作的OneInputTransformation对象,其input属性指向的是数据源的转换SourceTransformation(StreamNode#1);

2、OneInputTransformation{id=4, name='Window(TumblingProcessingTimeWindows(5000), ProcessingTimeTrigger, ReduceFunction$2, PassThroughWindowFunction)', outputType=Java Tuple2<String, Integer>, parallelism=1};
      表示reduce操作的OneInputTransformation对象,其input属性指向的是表示keyBy的转换PartitionTransformation,而PartitionTransformation的input属性指向的是Map转换的OneInputTransformation;

3、OneInputTransformation{id=5, name='Filter', outputType=Java Tuple2<String, Integer>, parallelism=1};
      表示filter操作的OneInputTransformation对象,其input属性指向的是reduce转化的OneInputTransformation对象;

4、SinkTransformation{id=6, name='Print to Std. Out', outputType=GenericType<java.lang.Object>, parallelism=1};
      表示sink操作对应的SinkTransformation对象,其input属性指向的是fliter转化的OneInputTransformation对象;

 

1、StreamNode#1(SourceTransformation) --> StreamEdge --> StreamNode#2(MapTransformation)

2、StreamNode#2(MapTransformation) --> (先指向PartitionTransformation所在的虚拟节点StreamNode#3) --> StreamNode#4(WindowTransformation);最终会使用StreamEdge 替换虚拟节点来连接两个实体StreamNode,其StreamEdge上包含虚拟节点所有的转换属性;

      StreamNode#2(MapTransformation) --> StreamEdge --> StreamNode#4(WindowTransformation);

3、StreamNode#4(WindowTransformation) --> StreamEdge --> StreamNode#5(FilterTransformation);

4、StreamNode#5(FilterTransformation) --> StreamEdge --> StreamNode#6(SinkTransformation);

针对上述转化过程,其对数据流的相关转换操作,都可以通过streamExecutionEnvironment.getStreamGraph().getStreamingPlanAsJSON()来获取到StreamGraph实例的json字符串;将获取到的json字符串放如到 http://flink.apache.org/visualizer/ ,可以转化为图形表示,如下:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值