Flink/Scala: explicación detallada de las funciones de transformación comunes de DataStream Transformations

I. Introducción

Este artículo presenta la forma de datos principal de Flink: DataStream, que es una función de conversión común para la transmisión de datos.A través de la transformación, un DataStream se puede convertir en un nuevo DataStream.

Consejos:

La siguiente demostración usa la siguiente clase de caso como tipo de datos y genera 10 elementos cada s a través de la función SourceFromCycle personalizada. Preste especial atención a la adición del parámetro isWait a la función Fuente, que controla si la Fuente genera datos con un retraso de 3 segundos. El entorno global es 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. Transformación común

1.Mapa - Flujo de datos → Flujo de datos

Procese un solo elemento y devuelva un solo elemento. mapDemo devuelve el num y num+1 para cada elemento y, finalmente, el sumidero se guarda en el archivo.

  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.Filtro - Flujo de datos → Flujo de datos

Calcula una función booleana para cada elemento y mantiene los elementos para los que la función devuelve verdadero. filterDemo solo guarda datos que comienzan con 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 - Flujo de datos → Flujo de datos

Genera 0, uno o más elementos a partir de un elemento. FlatMapDemo devuelve una tupla de self y self+1. 0 devuelve 0, 1, 1 devuelve 1, 2... y así sucesivamente.

  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 - Flujo de datos → Flujo con llave 

Divida la secuencia en particiones desconectadas, los registros con la misma clave se asignarán a la misma partición, keyBy() se implementa internamente a través de la partición Hash y las claves se pueden especificar de diferentes maneras. Los números se dividen utilizando el primer bit de cada número a continuación, y puede ver que los números debajo del TaskId correspondiente tienen el mismo primer bit. Sugerencias: Obtenga TaskId a través de RuntimeContext.

  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.Reducir - KeyedStream → DataStream

Amortizaciones "sucesivas" en flujos de datos clave. Combina el elemento actual con el valor simplificado más cercano y emite el nuevo valor. La siguiente agregación reducida acumula los resultados numéricos del mismo comienzo. Dado que es una acumulación dinámica, los números de salida son irregulares.

  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.Ventana - KeyedStream → WindowedStream

Windows se puede definir en KeyedStreams ya particionados. Windows agrupa los datos de cada clave en función de alguna característica (por ejemplo, los datos que llegaron en los últimos 5 segundos). Dependiendo del tipo, hay ventanas deslizantes y ventanas móviles. La siguiente demostración usa el primer dígito para particionar y genera una ventana con un intervalo de 5 s. La ventana contiene todos los datos dentro de 5 s.

  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 - Flujo de datos → Flujo de todas las ventanas

Windows se puede definir en flujos de datos regulares. Windows agrupa todos los eventos de transmisión en función de ciertas características (por ejemplo, datos que llegaron en los últimos 5 segundos). Window y WindowAll agregan datos dentro de un tiempo específico. La diferencia es que Window agrega los datos de cada partición, es decir, agrega los datos de la misma clave, por lo que se generan ventanas HashNum, mientras que WindowAll agrega todos los datos dentro del tiempo especificado. , independientemente de la clave, por lo que su paralelismo es solo 1. Como WindowAll no distingue entre particiones, se puede ver que la ventana obtenida por windowAll contiene muchos datos, mientras que la ventana obtenida por window tiene menos datos, pero ambas tienen la misma clave, es decir, el mismo primer número.

  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

Aplica la función reduce a la ventana y devuelve el valor reducido. El siguiente método acumula todos los números en windowAll y lo devuelve como un flujo de datos, cada número es la suma de todos los números en 5s.

  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. Unión - Flujo de datos → Flujo de datos

Combina dos o más flujos de datos, creando un nuevo flujo que contiene todos los elementos de todos los flujos. Nota: si fusiona un flujo de datos consigo mismo, obtendrá cada elemento dos veces en el flujo resultante. Dado que la unión unifica el propio flujo de datos, cada elemento se puede obtener dos veces.

  def unionDemo(env: StreamExecutionEnvironment): Unit = {
    val dataStream = env.addSource(new SourceFromCycle())
    env.setParallelism(5)
    dataStream.union(dataStream).print()
  }

10.Únete - Flujo de datos, Flujo de datos → Flujo de datos

Concatena dos flujos de datos en la clave dada y la ventana común. Conecte y procese los datos en los dos flujos de acuerdo con la clave especificada. La siguiente demostración concatena y suma los mismos números.

  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

Une dos elementos en dos flujos de clave con una clave común dentro de un intervalo de tiempo dado, donde los elementos satisfacen:

e1.timestamp + lowerBound <= e2.timestamp <= e1.timestamp + upperBound

En resumen, dos datos con la misma clave solo se conectarán dentro de los límites superior e inferior del tiempo especificado. Si se excede el tiempo de espera, incluso si la clave es la misma, no se agregará. 

  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()
  }

La demostración anterior establece el tiempo en -1 s -> 1 s, es decir, la tolerancia de tiempo de 2 s antes y después. Primero, ejecute el siguiente ejemplo, donde el parámetro isWait es falso, es decir, el flujo de datos no se retrasa y la salida es normal.

    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()
    })

Los datos se pueden vincular normalmente. 

Lo siguiente establece el parámetro isWait de la primera secuencia en verdadero:

    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()
    })

Dado que la generación de datos cuando isWait = true se retrasará 3 s, lo que supera la tolerancia de 2 s, los datos no se pueden vincular y no se emiten datos.

12. WindowCoGroup - Flujo de datos, Flujo de datos → Flujo de datos

Coagrupa dos flujos de datos en una clave determinada y una ventana común. Los datos de la misma clave se forman en una ventana, y los datos correspondientes a la clave de los dos flujos se procesan simultáneamente de acuerdo con la clave. La siguiente demostración procesa todos los números con el mismo primer número dentro de los 5 segundos en los dos flujos.

  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.Conectar - Flujo de datos, Flujo de datos → Flujo conectado

"Conectar" dos flujos de datos que mantienen su tipo. Las conexiones permiten el estado compartido entre dos flujos. Connect tiene dos usos, uno es fusionar dos flujos de datos, lo cual es algo diferente de union. Union fusiona flujos de datos del mismo tipo, es decir, Stream1 y Stream2 deben ser DataStream[T], y Connect puede fusionar diferentes tipos de datos streams. , el número singular debe procesarse por separado y finalmente recibir el mismo tipo de datos T, por ejemplo, Stream1 es tipo A, Stream2 es tipo B, después del procesamiento, ambos devuelven T, puede usar Connect. El segundo uso es BroadcastStream, que se usa como una variable de transmisión para ser compartida por otra transmisión. Puede  consultar Flink/Scala - Ejemplo de patrón de estado de transmisión de DataStream para obtener una explicación detallada . La siguiente demostración procesa los datos de los dos flujos por separado a través de dos métodos de proceso.

  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 - Flujo conectado → Flujo de datos

Similar a map y flatMap en flujos de datos concatenados. La siguiente demostración procesa y agrega dos flujos de datos por separado.

  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 - Flujo conectado → Flujo de datos

El uso básico es el mismo que el anterior, se pueden devolver 0 o más datos, por lo que aquí hay un recopilador para la función flatMap, que puede generar múltiples datos, mientras que CoMap usa directamente la función de mapa para formar una relación correspondiente 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.Iterar - Flujo de datos → Flujo iterativo → Flujo conectado

Crea un bucle de "retroalimentación" en la transmisión al redirigir la salida de un operador al operador anterior. Esto es especialmente útil para definir algoritmos que actualizan continuamente los modelos. El siguiente código comienza con una secuencia y aplica el cuerpo iterativo continuamente. Los elementos mayores que 0 se envían de vuelta al canal de retroalimentación y el resto se reenvía en sentido descendente. Flujo de bucle de retroalimentación, este método de procesamiento divide los datos en el flujo en flujo de retroalimentación y flujo de entrega. Los primeros datos continuarán siendo procesados ​​e iterados en un bucle, mientras que los últimos se enviarán al sumidero para finalizar su ciclo, que es se usa principalmente para la iteración del modelo, como los escenarios que carecen de muestras positivas pueden usar comentarios de muestras positivas varias veces, mientras que las muestras negativas se descartan periódicamente. La siguiente demostración mantiene números pares y genera números impares. Por primera vez, tanto los números pares como los impares tienen la oportunidad de participar, pero cuando se trata de la salida, solo se emiten los datos (número impar) porque los datos (número par) continúan realizando la iteración de retroalimentación.

  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. Resumen

La transformación básica de Flink Stream es más o menos la misma. La API oficial del método anterior solo brinda un ejemplo simple de una demostración simple que no se puede ejecutar, por lo que coincide con el número. Algunas API y ProcessFunction pueden tener algunas diferencias en la escritura debido a diferentes versiones de Flink, pero la idea general y la idea no cambiarán, esta es la forma de ordenar los puntos de conocimiento de Flink Source -> Transformation -> Sink.

FlinkDataSet Source ->  Flink / Scala - DataSet de DataSource para obtener un resumen de datos

FlinkDataStream Source ->  Flink / Scala - DataStream de DataSource para obtener un resumen de datos

Transformación FlinkDataSet ->  Flink / Scala - Transformaciones de conjuntos de datos Explicación detallada de las funciones de transformación comunes 

Transformación FlinkDataStream ->  Flink / Scala - Transformaciones DataStream Funciones de transformación comunes

Flink DataSet Sink ->  Flink / Scala - Explicación detallada de los datos de salida del DataSet Sink

 Flink DataStream Sink ->  Flink / Scala - Explicación detallada de los datos de salida del DataStream Sink

Supongo que te gusta

Origin blog.csdn.net/BIT_666/article/details/123990253
Recomendado
Clasificación