Ejemplo para explicar el modelo de programación del programa de procesamiento de flujo Flink

Resumen: Antes de sumergirnos en el desarrollo del programa de procesamiento de datos en tiempo real de Flink, analicemos un ejemplo simple para comprender el proceso de creación de una aplicación de transmisión con estado utilizando la API DataStream de Flink.

Este artículo se comparte desde Huawei Cloud Community " Ejemplo de Flink: modelo de programación del programa de procesamiento de flujo de Flink ", autor: TiAmoZhang.

Antes de sumergirnos en el desarrollo del programa de procesamiento de datos en tiempo real de Flink, analicemos un ejemplo simple para comprender el proceso de creación de una aplicación de transmisión con estado utilizando la API DataStream de Flink.

01. Tipo de datos de transmisión

Flink maneja los tipos de datos y la serialización de una manera única, con su propio descriptor de tipo, extracción de tipo genérico y marco de serialización de tipo. Basado en los lenguajes Java y Scala, Flink implementa su propio sistema de tipos, que admite muchos tipos, incluidos

  1. tipo básico.
  2. tipo de matriz.
  3. tipo compuesto.
  4. Tipo auxiliar.
  5. Tipo genérico.

El sistema de tipo Flink detallado se muestra en la Figura 1.

■ Figura 1 Sistema tipo Flink

La API DataStream de Flink para Java y Scala requiere que el contenido de los datos de transmisión sea serializable. Flink tiene serializadores incorporados para los siguientes tipos de datos:

  1. Tipos de datos básicos: String, Long, Integer, Boolean, Array.
  2. Tipos de datos compuestos: Tuple, POJO, clase de caso Scala.

Para otros tipos, Flink devuelve Kryo. También es posible utilizar otros serializadores en Flink. Avro en particular está bien respaldado.

1. El tipo de datos de flujo utilizado por la API Java DataStream

Para la API de Java, Flink define sus propios tipos Tuple1 a Tuple25 para representar tipos de tupla, el código es el siguiente:

Tuple2<String, Integer> person = new Tuple2<>("王老五", 35);
//索引基于0
String name = person.f0;
Integer age = person.f1;

En Java, un POJO (Objeto Java simple y antiguo) es una clase Java que:

  1. Hay un constructor predeterminado sin argumentos.
  2. Todos los campos son públicos o tienen un getter y setter predeterminados.

Por ejemplo, para definir una clase POJO llamada Persona, el código es el siguiente:

//定义一个Person POJO类public class Person{    public String name;    public Integer age;
 public Person() {};
 public Person(String name, Integer age) { this.name = name; this.age = age; };}
//创建一个实例Person person = new Person("王老五", 35);

2. Tipos de datos de transmisión utilizados por la API de Scala DataStream

Para tuplas, solo use el tipo Tuple propio de Scala, el código es el siguiente:

val person = ("王老五", 35)
//索引基于1val name = person._1val age = person._2

Para el tipo de objeto, clase de caso de uso (equivalente a JavaBean en Java), el código es el siguiente:

case class Person(name: String, age:Int)
val person = Person("王老五", 35)

3. Sistema tipo Flink

Para cualquier tipo POJO creado, parece que es un Java Bean común. En Java, Class puede usarse para describir el tipo, pero en el motor Flink, se describe como PojoTypeInfo, y PojoTypeInfo es una subclase de TypeInformation.

TypeInformation es la clase principal del sistema de tipos de Flink. Flink usa TypeInformation para describir todos los tipos de datos admitidos por Flink, al igual que el tipo de clase en Java. Cada tipo de datos admitido por Flink corresponde a una subclase de TypeInformation. Por ejemplo, el tipo POJO corresponde a PojoTypeInfo, la matriz de tipos de datos básicos corresponde a BasicArrayTypeInfo, el tipo de mapa corresponde a MapTypeInfo y el tipo de valor corresponde a ValueTypeInfo.

Además de describir tipos, TypeInformation también proporciona soporte para serialización. Hay un método en TypeInformation: el método createSerializer, que se utiliza para crear un serializador. En el serializador se definen una serie de métodos. Entre ellos, el tipo especificado se puede serializar a través de los métodos serialize y deserialize, y estos serializadores de Flink El optimizador escribe objetos en la memoria de forma densa. Flink también proporciona un serializador muy completo. Cuando programamos en función de los tipos de datos admitidos por el sistema de tipo Flink, Flink inferirá la información del tipo de datos en tiempo de ejecución. Cuando programamos en función de Flink, apenas necesitamos preocuparnos por los tipos y la serialización.

4. Compatibilidad con tipos y expresiones lambda

En el momento de la compilación, el compilador puede leer la información de tipo completa del código fuente de Java y hacer cumplir las restricciones de tipo, pero al generar el código de bytes de la clase, se eliminará la información de tipo parametrizada. Esto es borrado de tipos. El borrado de tipos garantiza que no se creen nuevas clases de Java para los genéricos y que los genéricos no incurran en una sobrecarga adicional. Es decir, el tipo genérico solo puede entender el tipo cuando el compilador compila, pero cuando se ejecuta después de la compilación, el tipo genérico se borrará.

Para una ilustración global, consulte el código siguiente:

public static <T> boolean hasItems(T [] items, T item){ for (T i : items){ if(i.equals(item)){ return true; } } return false;}

El anterior es un método genérico de Java, pero después de la compilación, el compilador borrará el tipo T no enlazado y lo reemplazará con Object. Es decir, el código compilado es el siguiente:

public static Object boolean hasItems(Object [] items, Object item){ for (Object i : items){ if(i.equals(item)){ return true; } } return false;}

Los genéricos solo pueden evitar errores de tipo en el tiempo de ejecución, pero se producirán las siguientes excepciones en el tiempo de ejecución, y Flink le indicará de una manera muy sencilla:

could not be determined automatically, due to type erasure. You can give type information hints by using the returns(...) method on the result of the transformation call, or by letting your function implement the 'ResultTypeQueryable' interface.

Debido al tipo de borrado del compilador de Java, Flink no puede inferir cuál es el tipo de salida del operador (como flatMap), por lo que al usar expresiones Lambda en Flink, para evitar errores de tiempo de ejecución debido al tipo de error de borrado, es necesario para especificar TypeInformation o TypeHint.

Crear TypeInformation, el código es el siguiente:

.returns(TypeInformation.of(String.class))

Crear TypeHint, el código es el siguiente:

.returns(new TypeHint<String>() {})

02. Implementación de aplicaciones de flujo

Los componentes básicos de los programas de Flink son flujos y transformaciones. Conceptualmente, un flujo es un flujo (posiblemente interminable) de registros de datos, y una transformación es una operación que toma uno o más flujos como entrada y produce uno o más flujos de salida después del procesamiento/cómputo.

Implementemos un ejemplo completo y funcional de una aplicación de transmisión de Flink.

[Ejemplo 1] Tome el flujo de registros sobre personas como entrada y filtre la información menor.

El código de Scala es el siguiente:

(1) Cree un proyecto Flink en IntelliJ IDEA, usando la plantilla de proyecto flink-quickstart-scala

(2) Establecer dependencias. Agregue las siguientes dependencias al archivo pom.xml:

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_2.12</artifactId>
<version>1.13.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.12</artifactId>
<version>1.13.2</version>
<scope>provided</scope>
</dependency>

(3) Cree el programa principal StreamingJobDemo1 y edite el código de procesamiento de flujo de la siguiente manera:

import org.apache.flink.streaming.api.scala._
object StreamingJobDemo1 {//定义事件类  case class Person(name:String, age:Integer)
  def main(args: Array[String]) {
//设置流执行环境 val env = StreamExecutionEnvironment.getExecutionEnvironment
//读取数据源,构造数据流 val peoples = env.fromElements(      Person("张三", 21),      Person("李四", 16),      Person("王老五", 35)    )
//对数据流执行filter转换 val adults = peoples.filter(_.age>18)
//输出结果 adults.print
//执行 env.execute("Flink Streaming Job")  }}

Ejecute el código anterior, el resultado es el siguiente:

7> Person(张三,21)1> Person(王老五,35)

El código Java es el siguiente:

(1) Cree un proyecto Flink en IntelliJ IDEA, utilizando la plantilla de proyecto flink-quickstart-Java

(2) 设置依赖。在 pom.xml 文件中添加如下依赖内容:

<dependency><groupId>org.apache.flink</groupId> <artifactId>flink-Java</artifactId> <version>1.13.2</version> <scope>provided</scope></dependency>dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-Java_2.12</artifactId> <version>1.13.2</version> <scope>provided</scope></dependency>
(3) 创建一个 POJO 类,用来表示流中的数据,代码如下:

//POJO类,表示人员信息实体public class Person {  public String name; //存储姓名  public Integer age; //存储年龄 //空构造器  public Person() {}; //构造器,初始化属性  public Person(String name, Integer age) {    this.name = name;    this.age = age;  }; //用于调试时输出信息  public String toString() {    return this.name.toString() + ": age " + this.age.toString();  };}
(4) 打开项目中的 StreamingJob 对象文件,编辑流处理代码如下:

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.api.common.functions.FilterFunction; public class StreamingJobDemo1 { public static void main(String[] args) throws Exception { //获得流执行环境         final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment(); //读取数据源,构造DataStream         DataStream<Person> personDS = env.fromElements(    new Person("张三", 21),    new Person("李四", 16),    new Person("王老五", 35)         ); //执行转换运算(这里是过滤年龄不小于18岁的人)//注意,这里使用了匿名函数         DataStream<Person> adults = personDS.filter(new FilterFunction<Person>() {              @Override      public boolean filter(Person person) throws Exception {      return person.age >= 18;      }          }); //将结果输出到控制台          adults.print(); //触发流程序开始执行          env.execute("stream demo");  }}

(5) Ejecute el programa anterior y los resultados de salida son los siguientes.

张三: age 21王老五: age 35

Aviso

Flink ejecuta programas por lotes como un caso especial de programas de transmisión, donde la transmisión está limitada (número finito de elementos). Un conjunto de datos se considera una secuencia internamente, por lo que los conceptos anteriores se aplican por igual a los programas por lotes que a los programas de transmisión, con algunas excepciones:

  1. La tolerancia a errores para los programas por lotes no utiliza puntos de control. La recuperación de errores se implementa mediante la reproducción completa de la transmisión, lo que hace que la recuperación sea más costosa, pero hace que el procesamiento regular sea más liviano porque evita los puntos de control.
  2. Las operaciones con estado en la API de DataSet utilizan estructuras de datos simplificadas en memoria/fuera del núcleo en lugar de índices clave-valor.
  3. La API de DataSet presenta iteraciones sincrónicas especiales (basadas en superpasos), que solo son posibles en transmisiones limitadas.

03. Análisis de aplicaciones de streaming

Todas las aplicaciones de Flink funcionan en pasos específicos, que se muestran en la Figura 2.

■ Figura 2 Pasos de trabajo de la aplicación Flink

Es decir, cada programa Flink consta de las mismas partes básicas:

  1. Obtenga un entorno de ejecución.
  2. Cargar/crear datos iniciales.
  3. Especifica una transformación para estos datos.
  4. Especifica dónde colocar los resultados del cálculo.
  5. Activar la ejecución del programa.

1. Consigue un entorno de ejecución

Una aplicación Flink genera uno o más trabajos Flink a partir de su método main(). Estos trabajos se pueden ejecutar en una JVM local (LocalEnvironment) o en una configuración remota (RemoteEnvironment) de un clúster con varias máquinas. Para cada programa, ExecutionEnvironment proporciona métodos para controlar la ejecución del trabajo (como establecer paralelismo o tolerancia a fallas/parámetros de puntos de control) e interactuar con el entorno externo (acceso a datos).

Cada aplicación Flink necesita un entorno de ejecución (env en este caso). El entorno de ejecución requerido por las aplicaciones de transmisión utiliza StreamExecutionEnvironment. Para comenzar a escribir programas Flink, los usuarios primero deben obtener un entorno de ejecución existente y, de no ser así, primero deben crear uno. Según el propósito, Flink admite los siguientes métodos:

  1. Obtenga un entorno Flink existente.
  2. Crear un ambiente local.
  3. Crear un entorno remoto.

El punto de entrada de un programa de flujo Flink es una instancia de la clase StreamExecutionEnvironment, que define el contexto para la ejecución del programa. StreamExecutionEnvironment es la base de todos los programas de Flink. Se puede obtener una instancia de StreamExecutionEnvironment a través de algunos métodos estáticos, el código es el siguiente:

StreamExecutionEnvironment.getExecutionEnvironment()StreamExecutionEnvironment.createLocalEnvironment()StreamExecutionEnvironment.createRemoteEnvironment(String host, int port, String... jarFiles)

Para obtener el entorno de ejecución, normalmente llame al método getExecutionEnvironment(). Esto seleccionará el entorno de ejecución correcto según el contexto. Si se ejecuta en un entorno local en el IDE, inicia un entorno de ejecución local. Si se crea un archivo JAR a partir de un programa y se invoca a través de la línea de comandos, el administrador de clústeres de Flink ejecutará el método main() y getExecutionEnvironment() devolverá el entorno de ejecución utilizado para ejecutar el programa de forma distribuida en el clúster. .

En el programa de ejemplo anterior, se utiliza la siguiente instrucción para obtener el entorno de ejecución del programa de flujo.

El código de Scala es el siguiente:

//设置流执行环境val env = StreamExecutionEnvironment.getExecutionEnvironment

El código Java es el siguiente:

//获得流执行环境final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment();

StreamExecutionEnvironment contiene ExecutionConfig, que se puede usar para establecer valores de configuración específicos del trabajo para el tiempo de ejecución. Por ejemplo, si desea establecer el intervalo de envío automático de marcas de agua, puede configurarlo en el código de la siguiente manera.

El código de Scala es el siguiente:

val env = StreamExecutionEnvironment.getExecutionEnvironmentenv.getConfig.setAutoWatermarkInterval(long milliseconds)

El código Java es el siguiente:

final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment();env.getConfig().setAutoWatermarkInterval(long milliseconds);

2. Cargar/crear datos iniciales

El entorno de ejecución puede leer datos de una variedad de fuentes de datos, incluidos archivos de texto, archivos CSV, datos de socket, etc., y también puede usar formatos de entrada de datos personalizados. Por ejemplo, para leer un archivo de texto como una secuencia de líneas, el código es el siguiente:

final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment();DataStream<String> text = env.readTextFile("file://path/to/file");

Una vez que los datos se leen en la memoria fila por fila, Flink los organiza en DataStream, que es una clase especial utilizada en Flink para representar la transmisión de datos.

En el programa de ejemplo [Ejemplo 1], use el método fromElements() para leer los datos de la colección y almacene los datos leídos como tipo DataStream.

El código de Scala es el siguiente:

//读取数据源,构造数据流val personDS = env.fromElements(      Person("张三", 21),      Person("李四", 16),      Person("王老五", 35)    )

El código Java es el siguiente:

//读取数据源,构造DataStreamDataStream<Person> personDS = env.fromElements(        new Person("张三", 21),        new Person("李四", 16),        new Person("王老五", 35));

3. Transforma los datos

Cada programa Flink realiza una transformación en una colección distribuida de datos. La API DataStream de Flink proporciona una variedad de funciones de transformación de datos, que incluyen filtrado, mapeo, unión, agrupación y agregación. Por ejemplo, la siguiente es una aplicación de conversión de mapas que crea un nuevo DataStream al convertir cada cadena de la colección original en un número entero, de la siguiente manera:

En el programa de muestra [Ejemplo 1], la transformación de filtro se usa para convertir el conjunto de datos original en un nuevo flujo de datos que contiene solo información para adultos, el código es el siguiente:

DataStream<String> input = env.fromElements("12","3","25","5","32","6");
DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {    @Override    public Integer map(String value) { return Integer.parseInt(value); }});

El código de Scala es el siguiente:

//对数据流执行filter转换val adults = personDS.filter(_.age>18)

El código Java es el siguiente:

//对数据流执行filter转换DataStream<Person> adults = flintstones.filter(    new FilterFunction<Person>() {        @Override        public boolean filter(Person person) throws Exception {            return person.age >= 18;        }    });

No es necesario comprender el significado específico de cada conversión aquí, las presentaremos en detalle más adelante. Debe enfatizarse que las transformaciones en Flink son perezosas y en realidad no se ejecutan hasta que se llama a la operación de sumidero.

4. Especifique dónde colocar los resultados del cálculo

Una vez que tenga un DataStream que contenga el resultado final, puede escribirlo en un sistema externo creando un sumidero. Por ejemplo, imprima los resultados de los cálculos en la pantalla.

El código de Scala es el siguiente:

//输出结果adults.print

El código Java es el siguiente:

//输出结果adults.print();

Las operaciones de sumidero en Flink desencadenan la ejecución de secuencias para producir los resultados requeridos por el programa, como guardar los resultados en el sistema de archivos o imprimirlos en la salida estándar. El ejemplo anterior usa adult.print() para imprimir los resultados en el registro del administrador de tareas (cuando se ejecuta en el IDE, el registro del administrador de tareas se mostrará en la consola del IDE). Esto llamará a su método toString() en cada elemento de la secuencia.

5. Activar la ejecución del programa de flujo

Una vez que se escribe la lógica de procesamiento del programa, es necesario activar la ejecución del programa llamando a execute() en StreamExecutionEnvironment. Todos los programas de Flink se ejecutan con pereza: cuando se ejecuta el método principal del programa, la carga de datos y las transformaciones no ocurren directamente, sino que cada operación se crea y se agrega al plan de ejecución del programa. Estas operaciones se ejecutan realmente cuando la ejecución se activa explícitamente mediante una llamada de ejecución () en el entorno de ejecución. Que el programa se ejecute localmente o se envíe al clúster depende del tipo de ExecutionEnvironment.

La computación perezosa permite a los usuarios crear programas complejos, que luego Flink ejecuta como una unidad del plan general. En el programa de muestra [Ejemplo 1], use el siguiente código para activar la ejecución del programa de procesamiento de flujo.

El código de Scala es el siguiente:

//触发流程序执行env.execute("Flink Streaming Job") //参数是程序名称,会显示在Web UI界面上

El código Java es el siguiente:

//触发流程序执行env.execute("Flink Streaming Job"); //参数是程序名称,会显示在Web UI界面上

Las llamadas a la API de DataStream ejecutadas en la aplicación generarán un Job Graph adjunto al StreamExecutionEnvironment. Cuando se llama a env.execute(), este gráfico se empaqueta y se envía a Flink Master, que paraleliza el trabajo y distribuye sus piezas a los administradores de tareas para su ejecución. Cada segmento paralelo del trabajo se ejecutará en un slot de tarea (task slot), como se muestra en la Figura 3.

■Figura 3 Principio de ejecución de la aplicación Flink stream

Este tiempo de ejecución distribuido requiere que las aplicaciones de Flink sean serializables. También requiere que todas las dependencias estén disponibles para todos los nodos del clúster.

El método execute() en StreamExecutionEnvironment esperará a que se complete el trabajo y luego devolverá un JobExecutionResult que contiene el tiempo de ejecución y el resultado del acumulador. Tenga en cuenta que la aplicación no se ejecutará sin llamar a execute().

Si no desea esperar a que se complete el trabajo, puede desencadenar la ejecución asincrónica del trabajo llamando a executeAysnc() en StreamExecutionEnvironment. Devolverá un JobClient que se puede usar para comunicarse con el trabajo que acaba de enviar. Por ejemplo, el siguiente código de ejemplo demuestra cómo implementar la semántica de execute() a través de executeAsync().

El código de Scala es el siguiente:

val jobClient = evn.executeAsyncval jobExecutionResult =jobClient.getJobExecutionResult(userClassloader).get

El código Java es el siguiente:

final JobClient jobClient = env.executeAsync();final JobExecutionResult jobExecutionResult =jobClient.getJobExecutionResult(

 

Haga clic para seguir y conocer las nuevas tecnologías de Huawei Cloud por primera vez~

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4526289/blog/9913132
Recomendado
Clasificación