Parte 4 | Guía de programación de Spark Streaming (1)

Spark Streaming es un marco de procesamiento de transmisión construido en Spark Core y es una parte muy importante de Spark. Spark Streaming se introdujo en la versión Spark 0.7.0 en febrero de 2013 y se ha convertido en una plataforma de procesamiento de flujo ampliamente utilizada en las empresas. En julio de 2016, se introdujo Structured Streaming en Spark 2.0 y alcanzó el nivel de producción en Spark 2.2. Structured Streaming es un motor de procesamiento de flujo construido en Spark SQL. Los usuarios pueden usar la API DataSet / DataFreame para realizar Procesamiento de Stream En la actualidad, Structured Streaming se está desarrollando rápidamente en diferentes versiones. Vale la pena señalar que este artículo no explicará demasiado el Streaming estructurado y discutirá principalmente Spark Streaming, incluido lo siguiente:

  • Introducción a Spark Streaming
  • Transformaciones 与 Operaciones de salida
  • Fuentes de datos de Spark Streaming (Fuentes)
  • Receptor de datos Spark Streaming (fregaderos)
    Inserte la descripción de la imagen aquí

Introducción a Spark Streaming

Que es DStream

Spark Streaming se basa en el RDD de Spark Core. Al mismo tiempo, Spark Streaming presenta un nuevo concepto: DStream (Discretized Stream), que representa un flujo de datos continuo. La abstracción DStream es el modelo de procesamiento de flujo de Spark Streaming. En su implementación interna, Spark Streaming segmenta los datos de entrada según un intervalo de tiempo (como 1 segundo) y convierte cada segmento de datos en RDD en Spark. Estos segmentos son Dstreams y Las operaciones de DStream finalmente se transforman en operaciones en el RDD correspondiente. Como se muestra abajo:

Inserte la descripción de la imagen aquí

Como se muestra en la figura anterior, estas operaciones de conversión de RDD de bajo nivel las completa el motor Spark. El funcionamiento de DStream protege muchos detalles de bajo nivel y proporciona a los usuarios una API de alto nivel más conveniente.

Modelo de cálculo

En Flink, el procesamiento por lotes es un caso especial de procesamiento de flujo, por lo que Flink es un motor de procesamiento de flujo natural. Este no es el caso de Spark Streaming. Spark Streaming cree que el procesamiento de transmisiones es un caso especial de procesamiento por lotes, es decir, Spark Streaming no es un motor de procesamiento de transmisiones en tiempo real puro. Utiliza un microBatchmodelo internamente, es decir , el procesamiento de transmisiones se trata como un pequeño intervalo de tiempo ( intervalo de lote) serie de procesamiento por lotes. Con respecto a la configuración del intervalo de tiempo, es necesario combinar los requisitos de demora empresarial específicos y se puede realizar el intervalo de segundos o minutos.

Spark Streaming almacena los datos recibidos en cada intervalo de tiempo corto en el clúster y luego le aplica una serie de operaciones de operador (mapear, reducir, groupBy, etc.). El proceso de ejecución se muestra en la siguiente figura:

Inserte la descripción de la imagen aquí

Como se muestra en la figura anterior: Spark Streaming divide el flujo de datos de entrada en pequeños lotes, cada lote representa el RDD de estas columnas y luego almacena estos lotes en la memoria. Al iniciar un trabajo de Spark para procesar los datos por lotes, se realiza una aplicación de procesamiento de flujo.

El mecanismo de trabajo de Spark Streaming

Visión de conjunto

Inserte la descripción de la imagen aquí

  • En Spark Streaming, habrá un componente Receiver que se ejecutará como una tarea de larga duración en un Executor
  • Cada receptor será responsable de un DStream de entrada (como un flujo de archivo que lee datos de un archivo, como un flujo de socket o un flujo de entrada leído desde Kafka, etc.)
  • Spark Streaming se conecta a fuentes de datos externas a través de DStream de entrada y lee datos relacionados

Detalles de implementacion

Inserte la descripción de la imagen aquí

  • 1. Inicie StreamingContext
  • 2. StreamingContext inicia el receptor, que siempre se ejecutará en la tarea Ejecutor. Usado para recibir fuentes de datos de forma continua, hay dos tipos principales de receptores, uno es un receptor fiable, cuando los datos se reciben y almacenan en Spark, se envía la confirmación de recepción y el otro son receptores no fiables, que no se envían a la fuente de datos Acuse de recibo. Los datos recibidos se almacenarán en caché en la memoria del nodo de trabajo y se copiarán en la memoria del nodo donde se encuentran otros ejecutores para un procesamiento tolerante a fallas.
  • 3. El contexto de transmisión activa el trabajo periódicamente (según el intervalo de tiempo del intervalo de lote) para el procesamiento de datos.
  • 4. Genere los datos.

Pasos de programación de Spark Streaming

Después del análisis anterior, tengo una comprensión preliminar de Spark Streaming. Entonces, ¿cómo escribir una aplicación Spark Streaming? Un Spark Streaming generalmente incluye los siguientes pasos:

  • 1. CrearStreamingContext
  • 2. Crear entrada DStreampara definir la fuente de entrada
  • 3. Defina la lógica de procesamiento aplicando operaciones de conversión y operaciones de salida a DStream
  • 4. Use streamingContext.start () para comenzar a recibir datos y procesar el flujo
  • 5.streamingContext.awaitTermination () método para esperar el final del procesamiento
  object StartSparkStreaming {
    def main(args: Array[String]): Unit = {
      val conf = new SparkConf()
        .setMaster("local[2]")
        .setAppName("Streaming")
      // 1.创建StreamingContext
      val ssc = new StreamingContext(conf, Seconds(5))
      Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
      Logger.getLogger("org.apache.hadoop").setLevel(Level.OFF)
      // 2.创建DStream
      val lines = ssc.socketTextStream("localhost", 9999)
      // 3.定义流计算处理逻辑
      val count = lines.flatMap(_.split(" "))
        .map(word => (word, 1))
        .reduceByKey(_ + _)
      // 4.输出结果
      count.print()
      // 5.启动
      ssc.start()
      // 6.等待执行
      ssc.awaitTermination()
    }
  }

Transformaciones 与 Operaciones de salida

Los DStreams son inmutables, lo que significa que su contenido no se puede cambiar directamente, pero se realizan una serie de transformaciones (Transformación) en DStreams para lograr la lógica de aplicación esperada. Cada conversión crea un nuevo DStream, que representa los datos convertidos del DStream principal. La conversión de DStream es lenta, lo que significa que solo después de que se realice la operación de salida, se realizará la operación de conversión y se llamarán las operaciones que desencadenan la ejecución output operation.

Transformaciones

Spark Streaming proporciona una gran cantidad de operaciones de transformación. Estas transformaciones se dividen en transformaciones con estado y transformaciones sin estado . Además, Spark Streaming también proporciona algunas operaciones de ventana. Vale la pena señalar que las operaciones de ventana también tienen estado. Los detalles son los siguientes:

Transformación sin estado

La transformación sin estado significa que el procesamiento de cada micro-lote es independiente entre sí, es decir, el resultado del cálculo actual no se ve afectado por el resultado del cálculo anterior. La mayoría de los operadores de Spark Streaming no tienen estado, como el mapa común () , flatMap (), reduceByKey () y así sucesivamente.

  • mapa (func)

Utilice la función func para convertir cada elemento del DStream de origen para obtener un nuevo DStream

    /** Return a new DStream by applying a function to all elements of this DStream. */
    def map[U: ClassTag](mapFunc: T => U): DStream[U] = ssc.withScope {
      new MappedDStream(this, context.sparkContext.clean(mapFunc))
    }
  • flatMap (func)

Similar al mapa, pero cada elemento de entrada se puede asignar a 0 o más elementos de salida

  /**
   * Return a new DStream by applying a function to all elements of this DStream,
   * and then flattening the results
   */
  def flatMap[U: ClassTag](flatMapFunc: T => TraversableOnce[U]): DStream[U] = ssc.withScope {
    new FlatMappedDStream(this, context.sparkContext.clean(flatMapFunc))
  }
  • filtro (func)

Devuelve un nuevo DStream que contiene solo los elementos del DStream de origen que satisfacen la función func

  /** Return a new DStream containing only the elements that satisfy a predicate. */
  def filter(filterFunc: T => Boolean): DStream[T] = ssc.withScope {
    new FilteredDStream(this, context.sparkContext.clean(filterFunc))
  }
  • repartición (numPartitions)

Cambie el grado de paralelismo de DStream creando más o menos particiones

/**
   * Return a new DStream with an increased or decreased level of parallelism. Each RDD in the
   * returned DStream has exactly numPartitions partitions.
   */
  def repartition(numPartitions: Int): DStream[T] = ssc.withScope {
    this.transform(_.repartition(numPartitions))
  }

  • reducir (func)

Utilice la función func para recopilar los elementos de cada RDD en el DStream de origen y devolver un nuevo DStream que contenga RDD de un solo elemento

  /**
   * Return a new DStream in which each RDD has a single element generated by reducing each RDD
   * of this DStream.
   */
  def reduce(reduceFunc: (T, T) => T): DStream[T] = ssc.withScope {
    this.map((null, _)).reduceByKey(reduceFunc, 1).map(_._2)
  }

  • contar()

Cuente el número de elementos en cada RDD en el DStream de origen

/**
   * Return a new DStream in which each RDD has a single element generated by counting each RDD
   * of this DStream.
   */
  def count(): DStream[Long] = ssc.withScope {
    this.map(_ => (null, 1L))
        .transform(_.union(context.sparkContext.makeRDD(Seq((null, 0L)), 1)))
        .reduceByKey(_ + _)
        .map(_._2)
  }
  • unión (otherStream)

Devuelve un nuevo DStream que contiene el DStream de origen y otros elementos de DStream

/**
   * Return a new DStream by unifying data of another DStream with this DStream.
   * @param that Another DStream having the same slideDuration as this DStream.
   */
  def union(that: DStream[T]): DStream[T] = ssc.withScope {
    new UnionDStream[T](Array(this, that))
  }
  • countByValue ()

Aplicado a un DStream con el tipo de elemento K, devuelve un nuevo DStream del tipo de par clave-valor (K, V). El valor de cada clave es el número de ocurrencias en cada RDD del DStream original lines.flatMap(_.split(" ")).countByValue().print(). Por ejemplo , para input :, spark spark flinksaldrá : (spark,2),(flink,1), Es decir, agrupe por valor de elemento y luego cuente el número de elementos en cada grupo.

Se puede ver en el código fuente que la capa inferior se implementa como mapa ((_, 1L)). ReduceByKey ((x: Long, y: Long) => x + y, numPartitions), que primero se asigna a una tupla según el elemento actual, donde La clave es el valor del elemento actual y luego se resume de acuerdo con la clave.

/**
   * Return a new DStream in which each RDD contains the counts of each distinct value in
   * each RDD of this DStream. Hash partitioning is used to generate
   * the RDDs with `numPartitions` partitions (Spark's default number of partitions if
   * `numPartitions` not specified).
   */
  def countByValue(numPartitions: Int = ssc.sc.defaultParallelism)(implicit ord: Ordering[T] = null)
      : DStream[(T, Long)] = ssc.withScope {
    this.map((_, 1L)).reduceByKey((x: Long, y: Long) => x + y, numPartitions)
  }

  • reduceByKey (func, [numTasks])

Cuando la operación se realiza en un DStream compuesto por (K, V) pares clave-valor, se devuelve un nuevo DStream compuesto por (K, V) pares clave-valor, y el valor de cada clave viene dado por la función recuce (Func) reunirse

como:lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_ + _).print()

Para entrada: chispa chispa flink, salida: (chispa, 2), (flink, 1)

  /**
   * Return a new DStream by applying `reduceByKey` to each RDD. The values for each key are
   * merged using the associative and commutative reduce function. Hash partitioning is used to
   * generate the RDDs with Spark's default number of partitions.
   */
  def reduceByKey(reduceFunc: (V, V) => V): DStream[(K, V)] = ssc.withScope {
    reduceByKey(reduceFunc, defaultPartitioner())
  }

  • unirse (otherStream, [numTasks])

Cuando se aplica a dos DStreams (uno que contiene pares clave-valor (K, V), otro que contiene pares clave-valor (K, W)), devuelve un nuevo DStream que contiene pares clave-valor (K, (V, W))

  /**
   * Return a new DStream by applying 'join' between RDDs of `this` DStream and `other` DStream.
   * Hash partitioning is used to generate the RDDs with Spark's default number of partitions.
   */
  def join[W: ClassTag](other: DStream[(K, W)]): DStream[(K, (V, W))] = ssc.withScope {
    join[W](other, defaultPartitioner())
  }
  • cogroup (otherStream, [numTasks])

Cuando se aplica a dos DStreams (uno que contiene pares clave-valor (K, V), otro que contiene pares clave-valor (K, W)), devuelve una tupla que contiene (K, Seq [V], Seq [W])

// 输入:spark
// 输出:(spark,(CompactBuffer(1),CompactBuffer(1)))
val DS1 = lines.flatMap(_.split(" ")).map((_,1))
val DS2 = lines.flatMap(_.split(" ")).map((_,1))
DS1.cogroup(DS2).print()
  /**
   * Return a new DStream by applying 'cogroup' between RDDs of `this` DStream and `other` DStream.
   * Hash partitioning is used to generate the RDDs with Spark's default number
   * of partitions.
   */
  def cogroup[W: ClassTag](
      other: DStream[(K, W)]): DStream[(K, (Iterable[V], Iterable[W]))] = ssc.withScope {
    cogroup(other, defaultPartitioner())
  }
  • transformar (func)

Cree un nuevo DStream aplicando la función RDD-to-RDD a cada RDD del DStream de origen. Admite cualquier operación RDD en el nuevo DStream

// 输入:spark spark flink
// 输出:(spark,2)、(flink,1)
val lines = ssc.socketTextStream("localhost", 9999)
val resultDStream = lines.transform(rdd => {
rdd.flatMap(_.split("\\W")).map((_, 1)).reduceByKey(_ + _)
})
resultDStream.print()
  /**
   * Return a new DStream in which each RDD is generated by applying a function
   * on each RDD of 'this' DStream.
   */
  def transform[U: ClassTag](transformFunc: RDD[T] => RDD[U]): DStream[U] = ssc.withScope {
    val cleanedF = context.sparkContext.clean(transformFunc, false)
    transform((r: RDD[T], _: Time) => cleanedF(r))
  }

Transformación con estado

La transformación con estado significa que el procesamiento de cada micro-lote no es independiente entre sí, es decir, el procesamiento actual del micro-lote depende de los resultados previos del cálculo del micro-lote. Las transformaciones con estado comunes incluyen principalmente countByValueAndWindow, reduceByKeyAndWindow, mapWithState, updateStateByKey, etc. De hecho, todas las operaciones basadas en ventanas tienen estado, porque se realiza un seguimiento de los datos de toda la ventana.

Para la transformación con estado y las operaciones de ventana, consulte a continuación.

Operaciones de salida

Utilice las operaciones de salida para escribir DStream en varios dispositivos de almacenamiento externos o imprima en la consola. Como se mencionó anteriormente, la transformación de Spark Streaming es lenta, por lo que la operación de salida es necesaria para el cálculo del disparador y su función es similar a la operación de acción de RDD. Para obtener más información, consulte la recopilación de datos de Spark Streaming (sumideros) a continuación.

Fuente de datos Spark Streaming

El propósito de Spark Streaming es convertirse en un marco de procesamiento de transmisión general. Para lograr este objetivo, Spark Streaming utiliza Receiver para integrar varias fuentes de datos. Sin embargo, para algunas fuentes de datos (como kafka), Spark Streaming admite el uso de Direct para recibir datos, que tiene un mejor rendimiento que Receiver.

Enfoque basado en receptor

Inserte la descripción de la imagen aquí

La función de Receiver es recopilar datos de fuentes de datos y luego transmitir los datos a Spark Streaming. El principio básico es: a medida que continúan llegando datos, estos datos se recopilarán y empaquetarán en bloques durante el intervalo de lote correspondiente. Siempre que se complete el intervalo de lote, los bloques de datos recopilados se enviarán a Spark para su procesamiento .

Como se muestra arriba: cuando se inicia Spark Streaming, el receptor comienza a recopilar datos. Al t0final del intervalo del lote (es decir, los datos recopilados en este período de tiempo), el bloque # 0 recopilado se enviará a Spark para su procesamiento. Por el t2momento, Spark procesará t1el bloque de datos del intervalo de lote y, al mismo tiempo, recopilará continuamente t2el bloque correspondiente al intervalo de lote ** # 2 **.

Las fuentes de datos comunes basadas en receptores incluyen: Kafka, Kinesis, Flume, Twitter. Además, los usuarios también pueden heredar la clase abstracta de Receiver , implementada onStart()con onStop()dos métodos, personalizar Receiver. Este artículo no discutirá demasiado sobre las fuentes de datos basadas en Receiver, pero explicará principalmente en detalle las fuentes de datos Kafka basadas en Direct.

Directo

Spark 1.3 introdujo este nuevo método directo sin receptor para garantizar una garantía de extremo a extremo más sólida. Este método no utiliza Receiver para recibir datos, pero consulta periódicamente el último desplazamiento en cada tema + partición de Kafka y, en consecuencia, define el rango de desplazamiento que se procesará en cada lote. Al iniciar un trabajo para procesar datos, la API de consumidor simple de Kafka se utiliza para leer el rango de compensación definido por Kafka (similar a leer un archivo del sistema de archivos). Tenga en cuenta que esta función se introdujo en Spark 1.3 de Scala y Java API y Spark 1.4 de Python API.

El enfoque basado en Direct tiene las siguientes ventajas:

  • Simplifique la lectura paralela

Si desea leer varias particiones, no necesita crear varios DStreams de entrada y luego realizar operaciones de unión en ellos. Spark creará tantas particiones RDD como particiones Kafka y leerá datos de Kafka en paralelo. Por lo tanto, existe una correspondencia uno a uno entre la partición kafka y la partición RDD.

  • alto rendimiento

Si desea garantizar una pérdida de datos cero, en el enfoque basado en el receptor, debe activar el mecanismo WAL. En realidad, este método es muy ineficiente, porque los datos se copian dos veces, y Kafka tiene un mecanismo altamente confiable para copiar los datos, y aquí los copiará al WAL. El enfoque basado en Direct no depende de Receiver y no necesita abrir el mecanismo WAL. Siempre que los datos se repliquen en Kafka, se pueden restaurar mediante la copia de Kafka.

  • Semántica exactamente una vez

Según el método Receiver, la API de alto nivel de Kafka se utiliza para guardar la compensación consumida en Zookeeper. Esta es la forma tradicional de consumir datos de Kafka. De esta manera, junto con el mecanismo WAL, puede garantizar una alta confiabilidad con cero pérdida de datos, pero no puede garantizar la semántica Exactamente una vez (puede haber asincrónica entre Spark y Zookeeper). Basado en el enfoque Direct y utilizando la API simple de Kafka, Spark Streaming es responsable de rastrear la compensación de consumo y almacenarla en el punto de control. Spark en sí debe estar sincronizado, por lo que puede garantizar que los datos se consuman una vez y solo una vez.

Spark Streaming integra Kafka

Cómo utilizar

Use KafkaUtils para agregar la fuente de datos de Kafka, el código fuente es el siguiente:

  def createDirectStream[K, V](
      ssc: StreamingContext,
      locationStrategy: LocationStrategy,
      consumerStrategy: ConsumerStrategy[K, V]
    ): InputDStream[ConsumerRecord[K, V]] = {
    val ppc = new DefaultPerPartitionConfig(ssc.sparkContext.getConf)
    createDirectStream[K, V](ssc, locationStrategy, consumerStrategy, ppc)
  }

Explicación de parámetros específicos:

  • K : tipo de clave de mensaje de Kafka

  • V : Tipo de valor de mensaje de Kafka

  • ssc : StreamingContext

  • locationStrategy : LocationStrategy, que programa el consumidor de acuerdo con la partición del tema en el Ejecutor, es decir, mantiene al consumidor lo más cerca posible de la partición líder. Esta configuración puede mejorar el rendimiento, pero la elección de la ubicación es solo una referencia, no absoluta. Puede elegir los siguientes métodos:

    • PreferBrokers: Spark y Kafka se ejecutan en el mismo nodo, puede usar este método
    • PreferConsistent: este método se usa en la mayoría de los casos, asignará particiones uniformemente entre todos los ejecutores
    • PreferFixed: coloque una partición de tema específico en un host específico, que se usa cuando la carga de datos no está equilibrada

    Nota : PreferConsisten se usa en la mayoría de los casos, los otros dos métodos solo se usan en escenarios específicos. Esta configuración es solo una referencia, y la situación específica aún se ajustará automáticamente de acuerdo con los recursos del clúster.

  • ConsumerStrategy : estrategia de consumo, hay tres formas principales:

    • Suscribirse: suscribirse a la colección de temas del nombre de tema especificado
    • SubscribePattern: suscríbase a los datos del tema coincidente mediante la coincidencia regular
    • Asignar: suscríbase a una colección de tema + partición

    Nota : utilice el método Suscribirse en la mayoría de los casos.

Casos de uso

object TolerateWCTest {

  def createContext(checkpointDirectory: String): StreamingContext = {

    val sparkConf = new SparkConf()
      .set("spark.streaming.backpressure.enabled", "true")
      //每秒钟从kafka分区中读取的records数量,默认not set
      .set("spark.streaming.kafka.maxRatePerPartition", "1000") //
      //Driver为了获取每个leader分区的最近offsets,连续进行重试的次数,
      //默认是1,表示最多重试2次,仅仅适用于 new Kafka direct stream API
      .set("spark.streaming.kafka.maxRetries", "2")
      .setAppName("TolerateWCTest")

    val ssc = new StreamingContext(sparkConf, Seconds(3))
    ssc.checkpoint(checkpointDirectory)
    val topic = Array("testkafkasource2")
    val kafkaParam = Map[String, Object](
      "bootstrap.servers" -> "kms-1:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "group0",
      "auto.offset.reset" -> "latest", //默认latest,
      "enable.auto.commit" -> (false: java.lang.Boolean)) //默认true,false:手动提交

    val lines = KafkaUtils.createDirectStream(
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topic, kafkaParam))

    val words = lines.flatMap(_.value().split(" "))
    val wordDstream = words.map(x => (x, 1))
    val stateDstream = wordDstream.reduceByKey(_ + _)

    stateDstream.cache()
    //参照batch interval设置,
    //不得低于batch interval,否则会报错,
    //设为batch interval的2倍
    stateDstream.checkpoint(Seconds(6))

    //把DStream保存到MySQL数据库中
    stateDstream.foreachRDD(rdd =>
      rdd.foreachPartition { record =>
        var conn: Connection = null
        var stmt: PreparedStatement = null
        // 给每个partition,获取一个连接
        conn = ConnectionPool.getConnection
        // 遍历partition中的数据,使用一个连接,插入数据库

        while (record.hasNext) {
          val wordcounts = record.next()
          val sql = "insert into wctbl(word,count) values (?,?)"
          stmt = conn.prepareStatement(sql);
          stmt.setString(1, wordcounts._1.trim)
          stmt.setInt(2, wordcounts._2.toInt)
          stmt.executeUpdate()
        }
        // 用完以后,将连接还回去
        ConnectionPool.returnConnection(conn)
      })
    ssc
  }

  def main(args: Array[String]) {

    val checkpointDirectory = "hdfs://kms-1:8020/docheckpoint"

    val ssc = StreamingContext.getOrCreate(
      checkpointDirectory,
      () => createContext(checkpointDirectory))
    ssc.start()
    ssc.awaitTermination()
  }
}

Receptor de datos Spark Streaming (fregaderos)

Introducción a la operación de salida

Spark Streaming proporciona la siguiente operación de salida incorporada, de la siguiente manera:

  • imprimir ()

Imprimir datos en salida estándar, si no se pasan parámetros, los primeros 10 elementos se imprimen por defecto

  • saveAsTextFiles ( prefijo , [ sufijo ])

Almacene el contenido de DStream en el sistema de archivos, el nombre de archivo de cada intervalo de lote es ` prefix-TIME_IN_MS [.suffix]

  • saveAsObjectFiles ( prefijo , [ sufijo ])

Guarde el contenido de DStream como SequenceFile del objeto java serializado. El nombre de archivo de cada intervalo de lote es prefix-TIME_IN_MS[.suffix]. La API de Python no admite este método.

  • saveAsHadoopFiles ( prefijo , [ sufijo ])

Guarde el contenido de DStream como un archivo Hadoop. El nombre de archivo de cada intervalo de lote es prefix-TIME_IN_MS[.suffix]. La API de Python no admite este método.

  • foreachRDD ( func )

Un operador de salida de datos general, la función func envía los datos de cada RDD a un dispositivo de almacenamiento externo, como escribir el RDD en un archivo o base de datos.

 /**
   * Apply a function to each RDD in this DStream. This is an output operator, so
   * 'this' DStream will be registered as an output stream and therefore materialized.
   */
  def foreachRDD(foreachFunc: RDD[T] => Unit): Unit = ssc.withScope {
    val cleanedF = context.sparkContext.clean(foreachFunc, false)
    foreachRDD((r: RDD[T], _: Time) => cleanedF(r), displayInnerRDDOps = true)
  }

  /**
   * Apply a function to each RDD in this DStream. This is an output operator, so
   * 'this' DStream will be registered as an output stream and therefore materialized.
   */
  def foreachRDD(foreachFunc: (RDD[T], Time) => Unit): Unit = ssc.withScope {
    // because the DStream is reachable from the outer object here, and because
    // DStreams can't be serialized with closures, we can't proactively check
    // it for serializability and so we pass the optional false to SparkContext.clean
    foreachRDD(foreachFunc, displayInnerRDDOps = true)
  }

  private def foreachRDD(
      foreachFunc: (RDD[T], Time) => Unit,
      displayInnerRDDOps: Boolean): Unit = {
    new ForEachDStream(this,
      context.sparkContext.clean(foreachFunc, false), displayInnerRDDOps).register()
  }

foreachRDD es una operación muy importante, los usuarios pueden usarla para enviar los datos procesados ​​a un dispositivo de almacenamiento externo. Con respecto al uso de foreachRDD, necesita funciones y no presta atención a algunos detalles. El análisis específico es el siguiente:

Si escribe datos en MySQL, necesita una conexión. Los usuarios pueden crear inadvertidamente un objeto de conexión en Spark Driver y luego usarlo en Work para escribir datos en un dispositivo externo. El código es el siguiente:

dstream.foreachRDD { rdd =>
  val connection = createNewConnection()  // ①注意:该段代码在driver上执行
  rdd.foreach { record =>
    connection.send(record) // ②注意:该段代码在worker上执行
  }
}

Sugerencia: el método de uso anterior es incorrecto, porque el objeto de conexión debe serializarse y luego enviarse al nodo del controlador. Este objeto de conexión no se puede serializar, por lo que no se puede transmitir entre nodos. El código anterior reportará un error de serialización La forma correcta de usarlo es crear una conexión en el nodo trabajador, rdd.foreaches decir, crear una conexión internamente. El camino es el siguiente:

dstream.foreachRDD { rdd =>
  rdd.foreach { record =>
    val connection = createNewConnection()
    connection.send(record)
    connection.close()
  }
}

El método anterior resuelve el problema de no ser serializable, pero crea una conexión para cada registro RDD. Por lo general, existe una cierta sobrecarga de rendimiento al crear un objeto de conexión, por lo que la creación y destrucción frecuentes de objetos de conexión reducirá el rendimiento general . Un mejor enfoque es rdd.foreachreemplazarlo con `` rdd.foreachPartition , de modo que no necesite crear una conexión para cada registro con frecuencia, sino crear una conexión para la partición RDD, lo que reduce en gran medida la sobrecarga de crear una conexión.

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    val connection = createNewConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    connection.close()
  }
}

De hecho, el uso anterior se puede optimizar aún más reutilizando objetos de conexión entre múltiples RDD o lotes de datos. Los usuarios pueden mantener un grupo de objetos de conexión estática y reutilizar los objetos del grupo para enviar varios lotes de RDD a sistemas externos para ahorrar aún más costos:

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    ConnectionPool.returnConnection(connection)  
  }
}

Casos de uso

  • Simular grupo de conexiones de base de datos
/**
 * 简易版的连接池
 */
public class ConnectionPool {
    
    

    // 静态的Connection队列
    private static LinkedList<Connection> connectionQueue;

    /**
     * 加载驱动
     */
    static {
    
    
        try {
    
    
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 获取连接,多线程访问并发控制
     *
     * @return
     */
    public synchronized static Connection getConnection() {
    
    
        try {
    
    
            if (connectionQueue == null) {
    
    
                connectionQueue = new LinkedList<Connection>();
                for (int i = 0; i < 10; i++) {
    
    
                    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/wordcount", "root",
                            "123qwe");
                    connectionQueue.push(conn);
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return connectionQueue.poll();
    }

    /**
     * 用完之后,返回一个连接
     */
    public static void returnConnection(Connection conn) {
    
    
        connectionQueue.push(conn);
    }

}

  • Estadísticas en tiempo real escritas en MySQL
object WordCount {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(5))
    val lines = ssc.socketTextStream("localhost", 9999)
    val words = lines.flatMap(_.split(" "))
    val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
    wordCounts.print()
    // 存储到MySQL
    wordCounts.foreachRDD { rdd =>
      rdd.foreachPartition { partition =>
        var conn: Connection = null
        var stmt: PreparedStatement = null
        // 给每个partition,获取一个连接
        conn = ConnectionPool.getConnection
        // 遍历partition中的数据,使用一个连接,插入数据库
        while (partition.hasNext) {
          val wordcounts = partition.next()
          val sql = "insert into wctbl(word,count) values (?,?)"
          stmt = conn.prepareStatement(sql);
          stmt.setString(1, wordcounts._1.trim)
          stmt.setInt(2, wordcounts._2.toInt)
          stmt.executeUpdate()

        }
        // 用完以后,将连接还回去
        ConnectionPool.returnConnection(conn)
      }
    }
    ssc.start()
    ssc.awaitTermination()
  }
}

para resumir

Debido a las limitaciones de espacio, este artículo analiza principalmente el mecanismo de ejecución de Spark Streaming, las transformaciones y operaciones de salida, las fuentes de datos de Spark Streaming (fuentes) y los receptores de datos de Spark Streaming (Sinks). El próximo artículo compartirá operaciones de ventana basadas en el tiempo , cálculos con estado , puntos de control , ajuste de rendimiento y más.

Supongo que te gusta

Origin blog.csdn.net/jmx_bigdata/article/details/107676548
Recomendado
Clasificación