FLink学习002——数据是怎么进来的

FLink学习002——数据是怎么进来的

1.Flink世界观

​ 在flink的世界观中一切都是由流组成的,离线数据是有界限的流,实时数据是一个没有界限的流,这就是所谓的有界流和无界流。

无界数据流无界数据流有一个开始但是没有结束,它们不会在生成时终止并提供数据,必须连续处理无界流,也就是说必须在获取后立即处理event。对于无界数据流我们无法等待所有数据都到达,因为输入是无界的,并且在任何时间点都不会完成。处理无界数据通常要求以特定顺序(例如事件发生的顺序)获取event,以便能够推断结果完整性。

有界数据流有界数据流有明确定义的开始和结束,可以在执行任何计算之前通过获取所有数据来处理有界流,处理有界流不需要有序获取,因为可以始终对有界数据集进行排序,有界流的处理也称为批处理。

在这里插入图片描述

2.WordCount

public class WordCount {

	// *************************************************************************
	// PROGRAM
	// *************************************************************************

	public static void main(String[] args) throws Exception {

		// Checking input parameters
		final MultipleParameterTool params = MultipleParameterTool.fromArgs(args);

		// set up the execution environment
		final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

		// make parameters available in the web interface
		env.getConfig().setGlobalJobParameters(params);

		// get input data
		DataStream<String> text = null;
		if (params.has("input")) {
			// union all the inputs from text files
			for (String input : params.getMultiParameterRequired("input")) {
				if (text == null) {
					text = env.readTextFile(input);
				} else {
					text = text.union(env.readTextFile(input));
				}
			}
			Preconditions.checkNotNull(text, "Input DataStream should not be null.");
		} else {
			System.out.println("Executing WordCount example with default input data set.");
			System.out.println("Use --input to specify file input.");
			// get default test text data
			text = env.fromElements(WordCountData.WORDS);
		}

		DataStream<Tuple2<String, Integer>> counts =
			// split up the lines in pairs (2-tuples) containing: (word,1)
			text.flatMap(new Tokenizer())
			// group by the tuple field "0" and sum up tuple field "1"
			.keyBy(0).sum(1);

		// emit result
		if (params.has("output")) {
			counts.writeAsText(params.get("output"));
		} else {
			System.out.println("Printing result to stdout. Use --output to specify output path.");
			counts.print();
		}
		// execute program
		env.execute("Streaming WordCount");
	}

	// *************************************************************************
	// USER FUNCTIONS
	// *************************************************************************

	/**
	 * Implements the string tokenizer that splits sentences into words as a
	 * user-defined FlatMapFunction. The function takes a line (String) and
	 * splits it into multiple pairs in the form of "(word,1)" ({@code Tuple2<String,
	 * Integer>}).
	 */
	public static final class Tokenizer implements FlatMapFunction<String, Tuple2<String, Integer>> {

		@Override
		public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
			// normalize and split the line
			String[] tokens = value.toLowerCase().split("\\W+");

			// emit the pairs
			for (String token : tokens) {
				if (token.length() > 0) {
					out.collect(new Tuple2<>(token, 1));
				}
			}
		}
	}

}

4.数据源

数据源的构建是通过StreamExecutionEnviroment这个方法实现来得到的

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

在StreamExecutionEnviroment中,使用了readFile方法读取数据,但是这种方法并不适合我们当前业务,不是实时数据处理。用一个socketTextStream用例来说明,可以看到指定了hostname和port,构建起一个接受网络数据的数据源

public DataStreamSource<String> socketTextStream(String hostname, int port) {
   return socketTextStream(hostname, port, "\n");
}

public DataStreamSource<String> socketTextStream(String hostname, int port, String delimiter) {
   return socketTextStream(hostname, port, delimiter, 0);
}

public DataStreamSource<String> socketTextStream(String hostname, int port, String delimiter, long maxRetry) {
   return addSource(new SocketTextStreamFunction(hostname, port, delimiter, maxRetry),
         "Socket Stream");
}

​ 可以看到会根据传入的hostname、port,以及默认的行分隔符”\n”,和最大尝试次数0,构造一个SocketTextStreamFunction实例,并采用默认的数据源节点名称为”Socket Stream”。
SocketTextStreamFunction的类继承图如下所示,可以看出其是SourceFunction的一个子类,而SourceFunction是Flink中数据源的基础接口。

img

也就是:SocketTextStreamFunction 实现了SourceFunction接口,而SourceFunction继承了Function和Serializable两个接口,其中Function也继承了Serializable接口。

下面是SourceFunction内部方法

img

@Public
public interface SourceFunction<T> extends Function, Serializable {
   void run(SourceContext<T> ctx) throws Exception;
   void cancel();
   @Public
   interface SourceContext<T> {
      void collect(T element);
      @PublicEvolving
      void collectWithTimestamp(T element, long timestamp);
      @PublicEvolving
      void emitWatermark(Watermark mark);
      @PublicEvolving
      void markAsTemporarilyIdle();
      Object getCheckpointLock();
      void close();
   }
}

run(SourceContex)方法:就是实现数据获取逻辑的地方,并可以通过传入的参数ctx(ctx是SourceContext类型)实现向下游节点的数据转发
cancel()方法:则是用来取消数据源的数据产生,一般在run方法中,会存在一个循环来持续产生数据,而cancel方法则可以使得该循环终止。

具体而言,我们可以研究下SocketTextStreamFunction的具体实现(也就是主要看其run方法的具体实现):

先看下类的介绍:

/**
 * A source function that reads strings from a socket. The source will read bytes from the socket
 * stream and convert them to characters, each byte individually. When the delimiter character is
 * received, the function will output the current string, and begin a new string.
 */

SocketTextStreamFuction主要是从socket读取byte数据,读取到的byte数据会被转换为字符,在接收到分隔符前,读取到的字符会被认为一个String;接收到分隔符后,也就意味着一个新的string即将到来。

下面是SocketTextStreamFunction中的几个主要成员属性:

/** Default delay between successive connection attempts. */
    private static final int DEFAULT_CONNECTION_RETRY_SLEEP = 500;

    /** Default connection timeout when connecting to the server socket (infinite). */
    private static final int CONNECTION_TIMEOUT_TIME = 0;

    private final String hostname;
    private final int port;
    private final String delimiter;
    private final long maxNumRetries;
    private final long delayBetweenRetries;

    private transient Socket currentSocket;
        
    private volatile boolean isRunning = true;

isRunning 就是上面提到的那个volatile修饰的bool标志,delimiter由构造器传入,即两个String使用什么分隔的。下面是所有data source类的核心,即run方法的实现

public void run(SourceContext<String> ctx) throws Exception {
   final StringBuilder buffer = new StringBuilder();
   long attempt = 0;  //重试次数
   /** 这里是第一层循环,只要当前处于运行状态,该循环就不会退出,会一直循环 */
   while (isRunning) {
      try (Socket socket = new Socket()) {
         /** 对指定的hostname和port,建立Socket连接,并构建一个BufferedReader,用来从Socket中读取数据 */
         currentSocket = socket;
         LOG.info("Connecting to server socket " + hostname + ':' + port);
         socket.connect(new InetSocketAddress(hostname, port), CONNECTION_TIMEOUT_TIME);
         BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
         char[] cbuf = new char[8192];
         int bytesRead;
         /** 这里是第二层循环,对运行状态进行了双重校验,同时对从Socket中读取的字节数进行判断 */
         while (isRunning && (bytesRead = reader.read(cbuf)) != -1) {
            buffer.append(cbuf, 0, bytesRead);
            int delimPos;
            /** 这里是第三层循环,就是对从Socket中读取到的数据,按行分隔符进行分割,并将每行数据作为一个整体字符串向下游转发 */
            while (buffer.length() >= delimiter.length() && (delimPos = buffer.indexOf(delimiter)) != -1) {
               String record = buffer.substring(0, delimPos);
               if (delimiter.equals("\n") && record.endsWith("\r")) {
                  record = record.substring(0, record.length() - 1);
               }
               /** 用入参ctx,进行数据的转发 */
               ctx.collect(record);
               buffer.delete(0, delimPos + delimiter.length());
            }
         }
      }
      /** 如果由于遇到EOF字符,导致从循环中退出,则根据运行状态,以及设置的最大重试尝试次数,决定是否进行 sleep and retry,或者直接退出循环 */
      if (isRunning) {
         attempt++;
         if (maxNumRetries == -1 || attempt < maxNumRetries) {
            LOG.warn("Lost connection to server socket. Retrying in " + delayBetweenRetries + " msecs...");
            Thread.sleep(delayBetweenRetries);
         }
         else {
            break;
         }
      }
   }
   /** 在最外层的循环都退出后,最后检查下缓存中是否还有数据,如果有,则向下游转发 */
   if (buffer.length() > 0) {
      ctx.collect(buffer.toString());
   }
}

cancel方法:

public void cancel() {
   isRunning = false;
   Socket theSocket = this.currentSocket;
   /** 如果当前socket不为null,则进行关闭操作 */
   if (theSocket != null) {
      IOUtils.closeSocket(theSocket);
   }
}

StreamExecutionEnvironment:addSource()方法:

public <OUT> DataStreamSource<OUT> addSource(SourceFunction<OUT> function, String sourceName) {
   return addSource(function, sourceName, null);
}

public <OUT> DataStreamSource<OUT> addSource(SourceFunction<OUT> function, String sourceName, TypeInformation<OUT> typeInfo) {
   /** 如果传入的输出数据类型信息为null,则尝试提取输出数据的类型信息 */
   if (typeInfo == null) {
      if (function instanceof ResultTypeQueryable) {
         /** 如果传入的function实现了ResultTypeQueryable接口, 则直接通过接口获取 */
         typeInfo = ((ResultTypeQueryable<OUT>) function).getProducedType();
      } else {
         try {
            /** 通过反射机制来提取类型信息 */
            typeInfo = TypeExtractor.createTypeInfo(
                  SourceFunction.class,
                  function.getClass(), 0, null, null);
         } catch (final InvalidTypesException e) {
            /** 提取失败, 则返回一个MissingTypeInfo实例 */
            typeInfo = (TypeInformation<OUT>) new MissingTypeInfo(sourceName, e);
         }
      }
   }
   /** 根据function是否是ParallelSourceFunction的子类实例来判断是否是一个并行数据源节点 */
   boolean isParallel = function instanceof ParallelSourceFunction;
   /** 闭包清理, 可减少序列化内容, 以及防止序列化出错 */
   clean(function);
   StreamSource<OUT, ?> sourceOperator;
   /** 根据function是否是StoppableFunction的子类实例, 来决定构建不同的StreamOperator */
   if (function instanceof StoppableFunction) {
      sourceOperator = new StoppableStreamSource<>(cast2StoppableSourceFunction(function));
   } else {
      sourceOperator = new StreamSource<>(function);
   }
   /** 返回一个新构建的DataStreamSource实例 */
   return new DataStreamSource<>(this, typeInfo, sourceOperator, isParallel, sourceName);
}

通过对addSource重载方法的依次调用,最后得到了一个DataStreamSource的实例。
TypeInformation是Flink的类型系统中的核心类,用作函数输入和输出的类型都需要通过TypeInformation来表示,TypeInformation可以看做是数据类型的一个工具,可以通过它获取对应数据类型的序列化器和比较器等。
StreamSource的类继承图如下所示:

img

上图可以看出StreamSource是StreamOperator接口的一个具体实现类,其构造函数的入参就是一个SourceFunction的子类实例,这里就是前面介绍过的SocketTextStreamFunciton的实例,构造过程如下:

public StreamSource(SRC sourceFunction) {
   super(sourceFunction);
   this.chainingStrategy = ChainingStrategy.HEAD;
}

public AbstractUdfStreamOperator(F userFunction) {
   this.userFunction = requireNonNull(userFunction);
   checkUdfCheckpointingPreconditions();
}

private void checkUdfCheckpointingPreconditions() {
   if (userFunction instanceof CheckpointedFunction && userFunction instanceof ListCheckpointed) {
      throw new IllegalStateException("User functions are not allowed to implement AND ListCheckpointed.");
   }
}xxxxxxxxxx public public StreamSource(SRC sourceFunction) {   super(sourceFunction);   this.chainingStrategy = ChainingStrategy.HEAD;}public AbstractUdfStreamOperator(F userFunction) {   this.userFunction = requireNonNull(userFunction);   checkUdfCheckpointingPreconditions();}private void checkUdfCheckpointingPreconditions() {   if (userFunction instanceof CheckpointedFunction && userFunction instanceof ListCheckpointed) {      throw new IllegalStateException("User functions are not allowed to implement AND ListCheckpointed.");   }}java

把传入的userFunction赋值给自己的属性变量,并对传入的userFunction做了校验工作,然后将链接策略设置为HEAD。
Flink中为了优化执行效率,会对数据处理链中的相邻节点会进行合并处理,链接策略有三种:
ALWAYS —— 尽可能的与前后节点进行链接;
NEVER —— 不与前后节点进行链接;
HEAD —— 只能与后面的节点链接,不能与前面的节点链接。
作为数据源的源头,是最顶端的节点了,所以只能采用HEAD或者NEVER,对于StreamSource,采用的是HEAD策略。
StreamOperator是Flink中流操作符的基础接口,其抽象子类AbstractStreamOperator实现了一些公共方法,用户自定义的数据处理逻辑会被封装在StreamOperator的具体实现子类中。

在sourceOperator变量被赋值后,即开始进行DataStreamSource的实例构建,并作为数据源构造调用的返回结果。

return new DataStreamSource<>(this, typeInfo, sourceOperator, isParallel, sourceName);

img

在Flink中,DataStream描述了一个具有相同数据类型的数据流,其提供了数据操作的各种API,如map、reduce等,通过这些API,可以对数据流中的数据进行各种操作,DataStreamSource的构建过程如下:

public DataStreamSource(StreamExecutionEnvironment environment,
      TypeInformation<T> outTypeInfo, StreamSource<T, ?> operator,
      boolean isParallel, String sourceName) {
   super(environment, new SourceTransformation<>(sourceName, operator, outTypeInfo, environment.getParallelism()));
   this.isParallel = isParallel;
   if (!isParallel) {
      setParallelism(1);
   }
}

protected SingleOutputStreamOperator(StreamExecutionEnvironment environment, StreamTransformation<T> transformation) {
   super(environment, transformation);
}

public DataStream(StreamExecutionEnvironment environment, StreamTransformation<T> transformation) {
   this.environment = Preconditions.checkNotNull(environment, "Execution Environment must not be null.");
   this.transformation = Preconditions.checkNotNull(transformation, "Stream Transformation must not be null.");
}

​ 可见构建过程就是初始化了DataStream中的environment和transformation这两个属性。

​ 其中transformation赋值的是SourceTranformation的一个实例,SourceTransformation是StreamTransformation的子类,而StreamTransformation则描述了创建一个DataStream的操作。对于每个DataStream,其底层都是有一个StreamTransformation的具体实例的,所以在DataStream在构造初始时会为其属性transformation设置一个具体的实例。并且DataStream的很多接口的调用都是直接调用的StreamTransformation的相应接口,如并行度、id、输出数据类型信息、资源描述等。

​ 通过上述过程,根据指定的hostname和port进行数据产生的数据源就构造完成了,获得的是一个DataStreamSource的实例,描述的是一个输出数据类型是String的数据流的源。
在上述的数据源的构建过程中,出现Function(SourceFunction)、StreamOperator、StreamTransformation、DataStream这四个接口:

Function接口:用户通过继承该接口的不同子类来实现用户自己的数据处理逻辑,如上述中实现了SourceFunction这个子类,来实现从指定hostname和port来接收数据,并转发字符串的逻辑;
StreamOperator接口:数据流操作符的基础接口,该接口的具体实现子类中,会有保存用户自定义数据处理逻辑的函数的属性,负责对userFunction的调用,以及调用时传入所需参数,比如在StreamSource这个类中,在调用SourceFunction的run方法时,会构建一个SourceContext的具体实例,作为入参,用于run方法中,进行数据的转发;

StreamTransformation接口:该接口描述了构建一个DataStream的操作,以及该操作的并行度、输出数据类型等信息,并有一个属性,用来持有StreamOperator的一个具体实例;
DataStream:描述的是一个具有相同数据类型的数据流,底层是通过具体的StreamTransformation来实现,其负责提供各种对流上的数据进行操作转换的API接口。

​ 通过上述的关系,最终用户自定义数据处理逻辑的函数,以及并行度、输出数据类型等就都包含在了DataStream中,而DataStream也就可以很好的描述一个具体的数据流了。

​ 上述四个接口的包含关系是这样的:Function –> StreamOperator –> StreamTransformation –> DataStream。

通过数据源的构造,理清Flink数据流中的几个接口的关系后,接下来在数据源上进行各种操作,达到最终的数据统计分析的目的。

发布了2 篇原创文章 · 获赞 6 · 访问量 472

猜你喜欢

转载自blog.csdn.net/mmmSMM_/article/details/104866778