FLink學習之數據是怎麼進來的

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數據流中的幾個接口的關係後,接下來在數據源上進行各種操作,達到最終的數據統計分析的目的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章