Directorio de artículos
1. Semántica de tiempo en Flink
En el procesamiento de transmisión de Flink, están involucrados diferentes conceptos de tiempo, como se muestra en la siguiente figura:
-
Tiempo de ingestión : es el momento en que los datos ingresan a Flink.
-
Tiempo de procesamiento : Es el tiempo del sistema local de cada operador que realiza operaciones basadas en el tiempo. Está relacionado con la máquina. El atributo de tiempo predeterminado es ProcessingTime.
Por ejemplo, la hora en que un registro ingresa a Flink es 2020-09-03 10:00:00.123
, y la hora del sistema cuando llega a la ventana es 2020-09-03 10:00:01.234
. El contenido del registro es el siguiente:
2020-09-03 09:59:58.624 INFO Fail over to rm2
Para las empresas, las estadísticas 1 min
fallan en el número de inicios de sesión, ¿a qué hora es la más significativa? —— eventTime, porque necesitamos hacer estadísticas basadas en el tiempo de generación del registro.
2. La introducción de EventTime
En el procesamiento de transmisión por secuencias de Flink, la mayoría de las empresas utilizarán eventTime. Generalmente, solo cuando no se pueda utilizar eventTime, se verán obligados a utilizar ProcessingTime o IngestionTime.
Si desea utilizar EventTime, debe introducir el atributo de tiempo de EventTime, de la siguiente manera:
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
3. Marca de agua
3.1 Conceptos básicos
Sabemos que el procesamiento de flujo tiene un proceso y un tiempo desde la generación del evento, hasta que fluye a través de la fuente y luego al operador, aunque en la mayoría de los casos, los datos que fluyen al operador vienen en el orden de tiempo en que se genera el evento. Sin embargo, no descarta la ocurrencia de desorden debido a la red, distribución y otras razones El llamado desorden significa que el orden de los eventos recibidos por Flink no está estrictamente en el orden del EventTime del evento.
- La marca de agua es un mecanismo para medir el progreso del tiempo del evento.
- La marca de agua se usa para manejar eventos fuera de orden , y el manejo correcto de eventos fuera de orden generalmente se realiza combinando el mecanismo de marca de agua con la ventana.
- La marca de agua en el flujo de datos se utiliza para indicar que los datos cuya marca de tiempo es menor que la marca de agua ya han llegado, por lo que la ejecución de la ventana también se activa mediante la marca de agua.
- La marca de agua puede entenderse como un mecanismo de activación retardada. Podemos establecer el tiempo de retardo t de la marca de agua. Cada vez que el sistema verificará el tiempo máximo de evento máximo en los datos que han llegado, y luego determinará que todos los datos cuyo tiempo de evento sea menor que el tiempo máximo de evento-t han llegado, si los hay El tiempo de parada de la ventana es igual a maxEventTime-t, luego esta ventana se activa para ejecutarse.
El marcador de agua del flujo ordenado se muestra en la siguiente figura: (La marca de agua se establece en 0):
La figura anterior nos permite establecer el tiempo máximo de retraso de llegada para los 2 s, por lo que la marca de tiempo para el evento correspondiente 7s Watermark es 5s, el evento Watermark de marca de tiempo 12s es 10s, si lo 窗口1
estamos 1s~5s
, 窗口2
es 6s ~ 10s, entonces 时间戳为7s
La hora de llegada de la marca de agua del evento desencadenante ocurre 窗口1
, la 时间戳为12s
marca de agua cuando llega el evento solo se activa 窗口2
.
La marca de agua es el "tiempo de cierre de la ventana" que activó la ventana anterior. Una vez que la puerta está cerrada, todos los datos dentro del rango de la ventana en función de la hora actual se incluirán en la ventana.
Mientras no se alcance el nivel del agua, no importa cuánto avance el tiempo real, la ventana no se activará.
3.2 Introducción de WaterMark
La introducción de Watermark es muy simple. Para datos fuera de orden, los métodos de cita más comunes son los siguientes:
stream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(2)){
override def extractTimestamp(element: SensorReading) = {
element.timestamp * 1000
}
})
El uso de Event Time debe especificar la marca de tiempo en la fuente de datos. De lo contrario, el programa no puede conocer la hora del evento del evento (si los datos en la fuente de datos no tienen una marca de tiempo, solo puede usar el Tiempo de procesamiento).
Vemos que en el ejemplo anterior se crea una clase que parece un poco complicada, esta clase realmente implementa la interfaz para asignar marcas de tiempo. Flink expone la interfaz TimestampAssigner para que la implementemos, de modo que podamos personalizar cómo extraer la marca de tiempo de los datos del evento.
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val readings:DataStream[SensorReading]=env
.addSource(newSensorSource)
.assignTimestampsAndWatermarks(newMyAssigner())
Hay dos tipos de MyAssigner:
-
AssignerWithPeriodicWatermarks
-
Asignador con marcas de agua puntuadas
Las dos interfaces anteriores se heredan de TimestampAssigner
.
3.2.1 Generar marca de agua periódicamente
Generar marcas de agua periódicamente: el sistema insertará marcas de agua periódicamente en la corriente (¡la marca de agua también es un evento especial!). El período predeterminado es 200 milisegundos. Puede utilizar el ExecutionConfig.setAutoWatermarkInterval()
método para configurar.
env.getConfig.setAutoWatermarkInterval(5000)
La lógica de generar una marca de agua: cada 5 segundos, Flink llamará AssignerWithPeriodicWatermarks
al getCurrentWatermark()
método. Si el método devuelve una marca de tiempo con una marca de tiempo mayor que la marca de agua anterior, la nueva marca de agua se insertará en la secuencia. Esta comprobación asegura que el nivel del agua aumenta de forma monótona. Si la marca de tiempo devuelta por el método es menor o igual que la marca de tiempo del nivel de agua anterior, no se generará una nueva marca de agua.
Por ejemplo, personalice una extracción de marca de tiempo periódica:
package com.flink.scala
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.watermark.Watermark
/**
* 自定义周期性的时间戳抽取类
*/
class PeriodicAssigner extends AssignerWithPeriodicWatermarks[SensorReading] {
val bound: Long = 60 *1000 //延时 1 分钟
var maxTs: Long = Long.MinValue // 观察到的最大时间戳
override def getCurrentWatermark: Watermark = {
new Watermark(maxTs - bound)
}
override def extractTimestamp(t: SensorReading, l: Long): Long = {
maxTs = maxTs.max(t.timestamp)
t.timestamp
}
}
Un caso especial simple es que si sabemos de antemano que la marca de tiempo del flujo de datos aumenta monótonamente, es decir, no hay desorden, entonces podemos usarlo assignAscendingTimestamps
. Este método utilizará directamente la marca de tiempo de los datos para generar la marca de agua.
val withTimestampsAndWatermarks = stream.assignAscendingTimestamps(e=>e.timestamp)
Para los flujos de datos fuera de orden, si podemos estimar aproximadamente el tiempo de retardo máximo de los eventos en el flujo de datos, podemos usar el siguiente código:
val withTimestampsAndWatermarks = stream.assignTimestampsAndWatermarks(newSensorTimeAssigner)
class SensorTimeAssigner extends
BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)){
//抽取时间戳
override def extractTimestamp(r:SensorReading):Long=r.timestamp
}
3.2.2 Generar marca de agua de forma intermitente
La generación intermitente de marcas de agua es diferente de la generación periódica. Este método no es de tiempo fijo, pero puede filtrar y procesar cada dato según sea necesario. Simplemente vaya al código para dar un ejemplo, solo insertamos una marca de agua en el flujo de datos del sensor de sensor_1:
package com.flink.scala
import org.apache.flink.streaming.api.functions.AssignerWithPunctuatedWatermarks
import org.apache.flink.streaming.api.watermark.Watermark
/**
* 间断式生成 Watermark
*/
class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[SensorReading] {
val bound:Long=60*1000
override def checkAndGetNextWatermark(t: SensorReading, extractedTS: Long): Watermark = {
if ("sensor_1".equals(t.id)) {
new Watermark(extractedTS - bound)
} else {
null
}
}
override def extractTimestamp(t: SensorReading, l: Long): Long = {
t.timestamp
}
}
4. Uso de EvnetTime en la ventana
4.1 Ventana móvil (TumblingEventTimeWindows)
package com.flink.scala
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.{
DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import scala.collection.mutable
object TumblingEventTimeWindowsTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val stream: DataStream[String] = env.socketTextStream("127.0.0.1",8080)
val textWithTsDstream: DataStream[(String, Long, Int)] = stream.map(text => {
val arr: Array[String] = text.split(" ")
(arr(0), arr(1).toLong, 1)
})
val textWithEventTimeDstream: DataStream[(String, Long, Int)] = textWithTsDstream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)]() {
override def extractTimestamp(t: (String, Long, Int)) = t._2
})
val textKeyStream = textWithEventTimeDstream.keyBy(0)
textKeyStream.print("textKey:")
val windowStream = textKeyStream.window(TumblingEventTimeWindows.of(Time.seconds(2)))
val groupDstream: DataStream[mutable.HashSet[Long]] =
windowStream.fold(new mutable.HashSet[Long]()) {
case(set, (key, ts, count))=>
set += ts
}
groupDstream.print("window::::").setParallelism(1)
env.execute()
}
}
El resultado se calcula de acuerdo con la ventana de tiempo de Event Time, independientemente del tiempo del sistema (incluida la velocidad de entrada).
4.2 Ventana deslizante (SlidingEventTimeWindows)
package com.flink.scala
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.{
DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.windowing.assigners.{
SlidingEventTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time
import scala.collection.mutable
object SlidingEventTimeWindowsTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val stream: DataStream[String] = env.socketTextStream("127.0.0.1",8080)
val textWithTsDstream: DataStream[(String, Long, Int)] = stream.map(text => {
val arr: Array[String] = text.split(" ")
(arr(0), arr(1).toLong, 1)
})
val textWithEventTimeDstream: DataStream[(String, Long, Int)] = textWithTsDstream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)]() {
override def extractTimestamp(t: (String, Long, Int)) = t._2
})
val textKeyStream = textWithEventTimeDstream.keyBy(0)
textKeyStream.print("textKey:")
val windowStream = textKeyStream.window(SlidingEventTimeWindows.of(Time.seconds(2),Time.milliseconds(500)))
val groupDstream: DataStream[mutable.HashSet[Long]] =
windowStream.fold(new mutable.HashSet[Long]()) {
case(set, (key, ts, count))=>
set += ts
}
groupDstream.print("window::::").setParallelism(1)
env.execute()
}
}
4.3 Ventana de sesión (EventTimeSessionWindows)
La ejecución se activará si la diferencia de tiempo entre el EventTime de dos datos consecutivos excede el intervalo de tiempo especificado. Si agrega Marca de agua, se retrasará cuando se cumpla el activador de ventana. Cuando se alcanza el nivel de retardo, se realiza el disparo de ventana.
package com.flink.scala
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.{
DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.windowing.assigners.{
EventTimeSessionWindows, SlidingEventTimeWindows, TumblingEventTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time
object EventTimeSessionWindowsTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val stream: DataStream[String] = env.socketTextStream("127.0.0.1",8080)
val textWithTsDstream: DataStream[(String, Long, Int)] = stream.map(text => {
val arr: Array[String] = text.split(" ")
(arr(0), arr(1).toLong, 1)
})
val textWithEventTimeDstream: DataStream[(String, Long, Int)] = textWithTsDstream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)]() {
override def extractTimestamp(t: (String, Long, Int)) = t._2
})
val textKeyStream = textWithEventTimeDstream.keyBy(0)
textKeyStream.print("textKey:")
val windowStream = textKeyStream.window(EventTimeSessionWindows.withGap(Time.milliseconds(500)))
windowStream.reduce((text1,text2) =>
(text1._1, 0L, text1._3 + text2._3)
).map(_._3).print("windows:::").setParallelism(1)
env.execute()
}
}