I.はじめに
この記事では、ストリーミングデータの一般的な変換関数であるFlink:DataStreamの主なデータ形式を紹介します。変換により、DataStreamを新しいDataStreamに変換できます。
チップ:
次のデモでは、データ型として次のケースクラスを使用し、カスタムSourceFromCycle関数を介して毎秒10個の要素を生成します。ソース関数にisWaitパラメーターを追加することに特に注意してください。この関数は、ソースが3秒の遅延でデータを生成するかどうかを制御します。グローバル環境はStreamExecutionEnvironmentです。
case class Data(num: Int)
// 每s生成一批数据
class SourceFromCycle(isWait: Boolean = false) extends RichSourceFunction[Data] {
private var isRunning = true
var start = 0
override def run(ctx: SourceFunction.SourceContext[Data]): Unit = {
if (isWait) {
TimeUnit.SECONDS.sleep(3)
}
while (isRunning) {
(start until (start + 100)).foreach(num => {
ctx.collect(Data(num))
if (num % 10 == 0) {
TimeUnit.SECONDS.sleep(1)
}
})
start += 100
}
}
override def cancel(): Unit = {
isRunning = false
}
}
val env = StreamExecutionEnvironment.getExecutionEnvironment
2.一般的な変換
1.マップ-DataStream→DataStream
単一の要素を処理し、単一の要素を返します。mapDemoは、各要素のnumとnum + 1を返し、最後にシンクがファイルに保存されます。
def mapDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(1)
val transformationStream = dataStream.map(data => {
(data.num, data.num + 1)
})
val output = "./output/"
transformationStream.writeAsText(output, WriteMode.OVERWRITE)
}
2.Filter-DataStream→DataStream
各要素のブール関数を計算し、関数がtrueを返す要素を保持します。filterDemoは、1から始まるデータのみを保持します。
def filterDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(1)
dataStream.filter(data => {
data.num.toString.startsWith("1")
}).print()
}
3.FlatMap-DataStream→DataStream
1つの要素から0、1つ、またはそれ以上の要素を生成します。FlatMapDemoは、selfとself+1のタプルを返します。0は0を返し、1、1は1、2...などを返します。
def flatMapDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(1)
dataStream.flatMap(data => {
val info = ArrayBuffer[Data]()
info.append(data)
info.append(Data(data.num + 1))
info.iterator
}).print()
}
4.KeyBy-DataStream→KeyedStream
ストリームを切断されたパーティションに分割します。同じキーを持つレコードは同じパーティションに割り当てられ、keyBy()はハッシュパーティション化によって内部的に実装され、キーはさまざまな方法で指定できます。番号は、以下の各番号の最初のビットを使用して分割され、対応するTaskIdの下の番号が同じ最初のビットを持っていることがわかります。ヒント:RuntimeContextを介してTaskIdを取得します。
def keyByDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream.keyBy(data => {
data.num.toString.slice(0, 1)
}).process(new ProcessFunction[Data, (Int, Int)] {
override def processElement(i: Data, context: ProcessFunction[Data, (Int, Int)]#Context, collector: Collector[(Int, Int)]): Unit = {
val taskId = getRuntimeContext.getIndexOfThisSubtask
collector.collect((taskId, i.num))
}
}).print()
}
5.Reduce-KeyedStream→DataStream
キー付きデータストリームの「ローリング」評価減。現在の要素を最も近い簡略化された値と組み合わせて、新しい値を出力します。次のreduce集計は、同じ開始の数値結果を累積します。動的な累積であるため、出力数は不規則です。
def reduceDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream.keyBy(data => {
data.num.toString.slice(0, 1)
}).reduce(new ReduceFunction[Data] {
override def reduce(o1: Data, o2: Data): Data = {
Data(o1.num + o2.num)
}
}).print()
}
6.Window-KeyedStream→WindowedStream
Windowsは、すでにパーティション化されたKeyedStreamsで定義できます。Windowsは、いくつかの特性(たとえば、過去5秒以内に到着したデータ)に基づいて、各キーのデータをグループ化します。タイプに応じて、スライディングウィンドウとローリングウィンドウがあります。次のデモでは、最初の桁を使用してパーティションを作成し、5秒間隔でウィンドウを生成します。ウィンドウには5秒以内のすべてのデータが含まれます。
def windowDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream.keyBy(data => {
data.num.toString.slice(0, 1)
}).window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.process(new ProcessWindowFunction[Data, String, String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[Data], out: Collector[String]): Unit = {
val log = key + "\t" + elements.toArray.mkString(",")
out.collect(log)
}
})
.print()
}
7.WindowAll-DataStream→AllWindowedStream
Windowsは、通常のデータストリームで定義できます。Windowsは、特定の特性(たとえば、過去5秒以内に到着したデータ)に基づいてすべてのストリーミングイベントをグループ化します。WindowとWindowAllはどちらも指定された時間内にデータを集約します。違いは、Windowが各パーティションのデータを集約する、つまり同じキーのデータを集約するため、HashNumウィンドウが生成されるのに対し、WindowAllは指定された時間内にすべてのデータを集約することです。 、キーに関係なく、その並列処理は1つだけです。WindowAllはパーティションを区別しないため、windowAllによって取得されたウィンドウには多くのデータが含まれ、windowによって取得されたウィンドウには少ないデータが含まれますが、両方とも同じキー、つまり同じ最初の番号を持っていることがわかります。
def windowAllDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream.keyBy(data => {
data.num.toString.slice(0, 1)
}).windowAll(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.process(new ProcessAllWindowFunction[Data, String, TimeWindow] {
override def process(context: Context, elements: Iterable[Data], out: Collector[String]): Unit = {
val log = elements.toArray.map(_.num).mkString(",")
out.collect(log)
}
})
.print()
}
8.WindowReduce-WindowedStream→DataStream
関数reduce関数をウィンドウに適用し、縮小された値を返します。次のメソッドは、windowAll内のすべての数値を累積し、それをDataStreamとして返します。各数値は、5秒のすべての数値の合計です。
def windowReduceDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream.keyBy(data => {
data.num.toString.slice(0, 1)
}).windowAll(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.reduce(new ReduceFunction[Data] {
override def reduce(o1: Data, o2: Data): Data = {
Data(o1.num + o2.num)
}
}).print()
}
9.Union-DataStream→DataStream
2つ以上のデータストリームをマージし、すべてのストリームのすべての要素を含む新しいストリームを作成します。注:データストリームをそれ自体とマージすると、結果のストリームで各要素が2回取得されます。ユニオンはデータストリーム自体を統合するため、各要素を2回取得できます。
def unionDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream.union(dataStream).print()
}
10.参加-DataStream、DataStream→DataStream
指定されたキーと共通ウィンドウに2つのデータストリームを連結します。指定されたキーに従って、2つのストリームのデータを接続して処理します。次のデモは、同じ数値を連結して合計します。
def joinDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream
.join(dataStream)
.where(x => x.num).equalTo(x => x.num)
.window(TumblingProcessingTimeWindows.of(Time.seconds(3)))
.apply(new JoinFunction[Data, Data, String] {
override def join(in1: Data, in2: Data): String = {
val out = in1.num.toString + " + " + in2.num.toString + " = " + (in1.num + in2.num).toString
out
}
}).print()
}
11.InnerJoin-KeyedStream、KeyedStream→DataStream
指定された時間間隔内で、2つのキーストリーム内の2つの要素を共通のキーで結合します。ここで、要素は次の条件を満たす必要があります。
e1.timestamp + lowerBound <= e2.timestamp <= e1.timestamp + upperBound
つまり、同じキーを持つ2つのデータは、指定された時間の上限と下限の範囲内でのみ接続されます。タイムアウトを超えると、キーが同じであっても、集計されません。
def windowInnerJoinDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle()).assignTimestampsAndWatermarks(new AscendingTimestampExtractor[Data] {
override def extractAscendingTimestamp(t: Data): Long = System.currentTimeMillis()
})
val dataStreamOther = env.addSource(new SourceFromCycle()).assignTimestampsAndWatermarks(new AscendingTimestampExtractor[Data] {
override def extractAscendingTimestamp(t: Data): Long = System.currentTimeMillis()
})
env.setParallelism(5)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
dataStream.keyBy(_.num).intervalJoin(dataStreamOther.keyBy(_.num))
.between(Time.seconds(-1), Time.milliseconds(1))
.upperBoundExclusive()
.lowerBoundExclusive()
.process((in1: Data, in2: Data, context: ProcessJoinFunction[Data, Data, String]#Context, collector: Collector[String]) => {
val out = in1.num.toString + " + " + in2.num.toString + " = " + (in1.num + in2.num).toString
collector.collect(out)
})
.print()
}
上記のデモでは、時間を-1s-> 1s、つまり前後の2sの時間許容値に設定しています。最初に、次の例を実行します。ここで、isWaitパラメーターはfalseであり、データフローは遅延されません。出力は正常です。
val dataStream = env.addSource(new SourceFromCycle(isWait = false)).assignTimestampsAndWatermarks(new AscendingTimestampExtractor[Data] {
override def extractAscendingTimestamp(t: Data): Long = System.currentTimeMillis()
})
val dataStreamOther = env.addSource(new SourceFromCycle()).assignTimestampsAndWatermarks(new AscendingTimestampExtractor[Data] {
override def extractAscendingTimestamp(t: Data): Long = System.currentTimeMillis()
})
データは正常にリンクできます。
以下は、最初のストリームのisWaitパラメーターをtrueに設定します。
val dataStream = env.addSource(new SourceFromCycle(isWait = true)).assignTimestampsAndWatermarks(new AscendingTimestampExtractor[Data] {
override def extractAscendingTimestamp(t: Data): Long = System.currentTimeMillis()
})
val dataStreamOther = env.addSource(new SourceFromCycle()).assignTimestampsAndWatermarks(new AscendingTimestampExtractor[Data] {
override def extractAscendingTimestamp(t: Data): Long = System.currentTimeMillis()
})
isWait = trueの場合、データの生成が3秒間遅延し、2秒間の許容範囲を超えるため、データをリンクできず、データは出力されません。
12.WindowCoGroup-DataStream、DataStream→DataStream
特定のキーと共通ウィンドウで2つのデータストリームをグループ化します。同じキーのデータがウィンドウになり、2つのストリームのキーに対応するデータがキーに従って同時に処理されます。次のデモでは、2つのストリームの5秒以内に同じ最初の番号を持つすべての番号を処理します。
def windowCoGroup(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
dataStream
.coGroup(dataStream)
.where(_.num.toString.head)
.equalTo(_.num.toString.head)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.apply {
(t1: Iterator[Data], t2: Iterator[Data], out: Collector[String]) =>
val t1Output = t1.toArray.mkString(",")
val t2Output = t2.toArray.mkString(",")
val output = t1Output + "\t" + t2Output
out.collect(output)
}.print()
}
13.Connect-DataStream、DataStream→ConnectedStream
タイプを維持する2つのデータストリームを「接続」します。接続により、2つのストリーム間で状態を共有できます。Connectには2つの用途があります。1つは2つのデータストリームをマージすることです。これはunionとは多少異なります。Unionは同じタイプのデータストリームをマージします。つまり、Stream1とStream2はDataStream [T]である必要があり、Connectは異なるタイプのデータをマージできます。ストリーム。、単一の番号を個別に処理し、最後に同じデータ型Tをシンクする必要があります。たとえば、Stream1はタイプA、Stream2はタイプBです。処理後、両方ともTを返し、Connectを使用できます。2番目の使用法はBroadcastStreamで、これは別のストリームで共有されるブロードキャスト変数として使用されます。 詳細な説明については、Flink /Scala-DataStreamブロードキャスト状態パターンの例を参照してください。次のデモでは、2つのプロセスメソッドを使用して2つのストリームのデータを個別に処理します。
def connectDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
env.setParallelism(5)
val connectStream = dataStream.connect(dataStream)
connectStream.process(new CoProcessFunction[Data, Data, String] {
override def processElement1(in1: Data, context: CoProcessFunction[Data, Data, String]#Context, collector: Collector[String]): Unit = {
collector.collect("[Stream1]-" + in1.num.toString)
}
override def processElement2(in2: Data, context: CoProcessFunction[Data, Data, String]#Context, collector: Collector[String]): Unit = {
collector.collect("[Stream2]-" + in2.num.toString)
}
}).print()
}
14.CoMap-ConnectedStream→DataStream
連結されたデータストリームのmapおよびflatMapに似ています。次のデモでは、2つのストリームデータを別々に処理および集約します。
def windowCoMapDemo(env: StreamExecutionEnvironment): Unit = {
env.setParallelism(5)
val dataStream = env.addSource(new SourceFromCycle())
dataStream.connect(dataStream).map(new CoMapFunction[Data, Data, String] {
override def map1(in1: Data): String = "[1]:" + in1.num
override def map2(in2: Data): String = "[2]:" + in2.num
}).print()
}
15.CoFlatMap-ConnectedStream→DataStream
基本的な使い方は上記と同じで、0個以上のデータを返すことができるので、複数のデータを生成できるflatMap関数のコレクターですが、CoMapはmap関数を直接使用して1-1の対応関係を形成します。
def windowCoFlatMapDemo(env: StreamExecutionEnvironment): Unit = {
env.setParallelism(5)
val dataStream = env.addSource(new SourceFromCycle())
dataStream.connect(dataStream).flatMap(new CoFlatMapFunction[Data, Data, String] {
override def flatMap1(in1: Data, collector: Collector[String]): Unit = {
collector.collect("[1]-" + in1.num)
collector.collect("[1 pow2]-" + in1.num * in1.num)
}
override def flatMap2(in2: Data, collector: Collector[String]): Unit = {
collector.collect("[2]-" + in2.num)
collector.collect("[2 pow2]-" + in2.num * in2.num)
}
}).print()
}
16.Iterate-DataStream→IterativeStream→ConnectedStream
1つのオペレーターの出力を前のオペレーターにリダイレクトすることにより、ストリームに「フィードバック」ループを作成します。これは、モデルを継続的に更新するアルゴリズムを定義する場合に特に役立ちます。以下のコードはストリームで始まり、反復本体を継続的に適用します。0より大きい要素はフィードバックチャネルに送り返され、残りはダウンストリームに転送されます。フィードバックループフロー、この処理方法は、フロー内のデータをフィードバックフローと配信フローに分割します。前者のデータは引き続き処理され、ループで繰り返されますが、後者はシンクに出力されてサイクルを終了します。正のサンプルがないシナリオでは、正のサンプルフィードバックを複数回使用できますが、負のサンプルは定期的に破棄されるなど、主にモデルの反復に使用されます。次のデモは偶数を保持し、奇数を出力します。初めて奇数と偶数の両方が参加する機会がありますが、出力に関しては、データ(偶数)がフィードバックの反復を継続するため、データ(奇数)のみが出力されます。
def windowIterateDemo(env: StreamExecutionEnvironment): Unit = {
val dataStream = env.addSource(new SourceFromCycle())
dataStream.iterate {
iteration => {
val iterationBody = iteration.map(data => {
println(data.num + " " + "appear!")
data
})
// 反馈流,持续参与迭代 && 输出流,离开迭代
(iterationBody.filter(_.num % 2 == 0).setParallelism(1), iterationBody.filter(_.num % 2 != 0).setParallelism(1))
}
}.print()
}
3.まとめ
Flink Streamの基本的な変換はほぼ同じです。上記のメソッドの公式APIは、実行できない単純なデモの簡単な例を示しているだけなので、数と一致します。一部のAPIとProcessFunctionは、次の理由で書き込みにいくつかの違いがある場合があります。 Flinkのバージョンは異なりますが、全体的なアイデアとアイデアは変わりません。FlinkSource->Transformation->Sinkの知識ポイントを整理する方法は次のとおりです。
FlinkDataSet Source-> Flink/Scala-データの概要を取得するためのデータソースのDataSet
FlinkDataStream Source-> Flink/Scala-データサマリーを取得するためのデータソースのDataStream
FlinkDataSet変換-> Flink/Scala-DataSet変換一般的な変換関数の詳細な説明
FlinkDataStream変換-> Flink/Scala-DataStream変換一般的な変換関数
Flink DataSet Sink-> Flink /Scala-DataSetSink出力データの詳細な説明
Flink DataStream Sink-> Flink /Scala-DataStreamSink出力データの詳細な説明