Flink / Scala - Explication détaillée des fonctions de transformation courantes des transformations DataStream

Introduction

Cet article présente la principale forme de données de Flink : DataStream, qui est une fonction de conversion courante pour le streaming de données. Grâce à la transformation, un DataStream peut être converti en un nouveau DataStream.

Des astuces:

La démonstration suivante utilise la classe de cas suivante comme type de données et génère 10 éléments toutes les s via la fonction SourceFromCycle personnalisée. Portez une attention particulière à l'ajout du paramètre isWait à la fonction Source, qui contrôle si la source génère des données avec un délai de 3 s. L'environnement global est 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. Transformation commune

1.Carte - Flux de données → Flux de données

Traiter un seul élément et renvoyer un seul élément. mapDemo renvoie le num et num + 1 pour chaque élément, et enfin le récepteur est enregistré dans le fichier.

  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.Filtre - Flux de données → Flux de données

Calcule une fonction booléenne pour chaque élément et conserve les éléments pour lesquels la fonction renvoie true. filterDemo ne conserve que les données commençant par 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 - Flux de données → Flux de données

Génère 0, un ou plusieurs éléments à partir d'un élément. FlatMapDemo renvoie un tuple de self et self+1. 0 renvoie 0, 1, 1 renvoie 1, 2 ... et ainsi de suite.

  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 

Divisez le flux en partitions déconnectées, les enregistrements avec la même clé seront affectés à la même partition, keyBy() est implémenté en interne via le partitionnement Hash et les clés peuvent être spécifiées de différentes manières. Les nombres sont partitionnés en utilisant le premier bit de chaque nombre ci-dessous, et vous pouvez voir que les nombres sous le TaskId correspondant ont le même premier bit. Conseils : Obtenez le TaskId via 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.Réduire - KeyedStream → DataStream

Réductions de valeur « glissantes » sur les flux de données à clé. Combine l'élément courant avec la valeur simplifiée la plus proche et émet la nouvelle valeur. L'agrégation de réduction suivante accumule les résultats numériques du même début. Puisqu'il s'agit d'une accumulation dynamique, les nombres de sortie sont irréguliers.

  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 peut être défini sur des KeyedStreams déjà partitionnés. Windows regroupe les données dans chaque clé en fonction de certaines caractéristiques (par exemple, les données arrivées au cours des 5 dernières secondes). Selon le type, il existe des fenêtres glissantes et des fenêtres roulantes. La démo suivante utilise le premier chiffre pour partitionner et génère une fenêtre avec un intervalle de 5 s. La fenêtre contient toutes les données dans les 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 - DataStream → AllWindowedStream

Windows peut être défini sur des flux de données réguliers. Windows regroupe tous les événements de streaming en fonction de certaines caractéristiques (par exemple, les données arrivées au cours des 5 dernières secondes). Window et WindowAll agrègent les données dans un délai spécifié. La différence est que Window agrège les données de chaque partition, c'est-à-dire qu'il agrège les données de la même clé, de sorte que les fenêtres HashNum sont générées, tandis que WindowAll agrège toutes les données dans le délai spécifié. , quelle que soit la clé. Son parallélisme n'est donc que de 1. Étant donné que WindowAll ne fait pas de distinction entre les partitions, on peut voir que la fenêtre obtenue par windowAll contient beaucoup de données, tandis que la fenêtre obtenue par window contient moins de données, mais les deux ont la même clé, c'est-à-dire le même premier numéro.

  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 - Flux fenêtré → Flux de données

Applique la fonction réduire la fonction à la fenêtre et renvoie la valeur réduite. La méthode suivante accumule tous les nombres dans windowAll et le renvoie sous forme de DataStream, chaque nombre est la somme de tous les nombres 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.Union - Flux de données → Flux de données

Fusionne deux ou plusieurs flux de données, créant un nouveau flux contenant tous les éléments de tous les flux. Remarque : si vous fusionnez un flux de données avec lui-même, vous obtiendrez chaque élément deux fois dans le flux résultant. Puisque l'union unifie le flux de données lui-même, chaque élément peut être obtenu deux fois.

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

10.Joindre - Flux de données, Flux de données → Flux de données

Concatène deux flux de données sur la clé donnée et la fenêtre commune. Connectez et traitez les données dans les deux flux en fonction de la clé spécifiée. La démo suivante concatène et additionne les mêmes nombres.

  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

Joint deux éléments dans deux flux de clés avec une clé commune dans un intervalle de temps donné, où les éléments satisfont :

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

En bref, deux données avec la même clé ne seront connectées que dans les limites supérieure et inférieure du temps spécifié. Si le délai est dépassé, même si la clé est la même, elle ne sera pas agrégée. 

  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 démonstration ci-dessus définit le temps sur -1s -> 1s, c'est-à-dire la tolérance de temps de 2s avant et après. Tout d'abord, exécutez l'exemple suivant, où le paramètre isWait est faux, c'est-à-dire que le flux de données n'est pas retardé, et la sortie est normale.

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

Les données peuvent être liées normalement. 

Ce qui suit définit le paramètre isWait du premier flux sur 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()
    })

Étant donné que la génération de données lorsque isWait = true sera retardée de 3 s, ce qui dépasse la tolérance de 2 s, les données ne peuvent pas être liées et aucune donnée n'est sortie.

12.WindowCoGroup - Flux de données, Flux de données → Flux de données

Co-regroupe deux flux de données sur une clé donnée et une fenêtre commune. Les données de la même clé sont formées dans une fenêtre, et les données correspondant à la clé des deux flux sont traitées simultanément en fonction de la clé. La démonstration suivante traite tous les nombres avec le même premier nombre dans les 5 secondes dans les deux flux.

  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.Connecter - Flux de données, Flux de données → Flux connecté

"Connectez" deux flux de données qui conservent leur type. Les connexions permettent un état partagé entre deux flux. Connect a deux utilisations, l'une consiste à fusionner deux flux de données, ce qui est quelque peu différent de union. Union fusionne des flux de données du même type, c'est-à-dire que Stream1 et Stream2 doivent être DataStream[T], et Connect peut fusionner différents types de données flux. , le nombre singulier doit être traité séparément et finalement couler le même type de données T, par exemple, Stream1 est de type A, Stream2 est de type B, après traitement, les deux renvoient T, vous pouvez utiliser Connect. La deuxième utilisation est BroadcastStream, qui est utilisée comme variable de diffusion à partager par un autre flux. Vous pouvez vous référer à l'  exemple de modèle d'état de diffusion Flink / Scala - DataStream pour une explication détaillée . La démonstration suivante traite les données des deux flux séparément via deux méthodes Process.

  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 → Flux de données

Semblable à map et flatMap sur des flux de données concaténés. La démonstration suivante traite et agrège deux flux de données séparément.

  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 → Flux de données

L'utilisation de base est la même que ci-dessus, 0 ou plusieurs données peuvent être renvoyées. Voici donc un collecteur pour la fonction flatMap, qui peut générer plusieurs données, tandis que CoMap utilise directement la fonction map pour former une relation correspondante 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.Itérer - Flux de données → Flux itératif → Flux connecté

Crée une boucle de "rétroaction" dans le flux en redirigeant la sortie d'un opérateur vers l'opérateur précédent. Ceci est particulièrement utile pour définir des algorithmes qui mettent continuellement à jour les modèles. Le code ci-dessous commence par un flux et applique le corps itératif en continu. Les éléments supérieurs à 0 sont renvoyés au canal de rétroaction et les autres sont transmis en aval. Flux de boucle de rétroaction, cette méthode de traitement divise les données du flux en flux de rétroaction et flux de livraison. Les premières données continueront d'être traitées et itérées dans une boucle, tandis que les secondes seront transmises au récepteur pour terminer son cycle, qui est principalement utilisé pour l'itération du modèle, comme les scénarios dépourvus d'échantillons positifs peuvent utiliser plusieurs fois un retour d'échantillon positif, tandis que les échantillons négatifs sont périodiquement rejetés. La démo suivante conserve les nombres pairs et produit les nombres impairs. Pour la première fois, les nombres impairs et pairs ont une chance de participer, mais en ce qui concerne la sortie, seules les données (nombre impair) sont sorties car les données (nombre pair) continuent d'effectuer une itération de rétroaction.

  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. Résumé

La transformation de base de Flink Stream est à peu près la même. L'API officielle de la méthode ci-dessus ne donne qu'un exemple simple d'une démonstration simple qui ne peut pas être exécutée, elle correspond donc au nombre. Certaines API et ProcessFunction peuvent avoir des différences d'écriture en raison de différentes versions de Flink, mais l'idée générale et l'idée ne changeront pas, voici comment trier les points de connaissance de Flink Source -> Transformation -> Sink.

FlinkDataSet Source ->  Flink / Scala - DataSet de DataSource pour obtenir un résumé des données

FlinkDataStream Source ->  Flink / Scala - DataStream de DataSource pour obtenir un résumé des données

Transformation FlinkDataSet ->  Flink / Scala - Transformations DataSet Explication détaillée des fonctions de transformation courantes 

Transformation FlinkDataStream ->  Flink / Scala - Transformations DataStream Fonctions de transformation courantes

Flink DataSet Sink ->  Flink / Scala - Explication détaillée des données de sortie du DataSet Sink

 Flink DataStream Sink ->  Flink / Scala - Explication détaillée des données de sortie du DataStream Sink

Je suppose que tu aimes

Origine blog.csdn.net/BIT_666/article/details/123990253
conseillé
Classement