Parte 2 | Guía de programación de Spark Core

En el artículo "First | Spark Overview" , se explica la apariencia general de Spark. Este artículo profundizará en el componente principal de Spark : Spark Core . Spark Core es el motor de ejecución básico de propósito general de la plataforma Spark. Todas las demás funciones se basan en este motor. No solo proporciona funciones de computación de memoria para aumentar la velocidad, sino que también proporciona un modelo de ejecución general para admitir varias aplicaciones. Además, los usuarios pueden utilizar las API de Java, Scala y Python para desarrollar aplicaciones. Spark core se basa en un RDD abstracto unificado, que permite que varios componentes de Spark se integren a voluntad, y se pueden usar diferentes componentes en la misma aplicación para completar tareas complejas de procesamiento de big data. Los principales temas tratados en este artículo son:

  • Que es RDD
    • La intención original del diseño RDD
    • Conceptos básicos y características principales de RDD
    • Amplia dependencia y dependencia estrecha
    • División de etapas y programación de trabajos
  • Operador RDD
    • Transformaciones
    • Comportamiento
  • Variable compartida
    • Variable de difusión
    • acumulador
  • Resistencia
  • Caso completo
    Inserte la descripción de la imagen aquí

Que es RDD

Intención del diseño original

RDD (Resilient Distributed Datasets) se diseñó originalmente para resolver el problema de que algunos marcos informáticos existentes no son eficientes para procesar dos tipos de escenarios de aplicación, que son algoritmos iterativos y minería de datos interactiva . En estos dos escenarios de aplicación, al almacenar datos en la memoria, el rendimiento se puede mejorar en varios órdenes de magnitud. Para algoritmos iterativos , como PageRank, agrupación en clústeres de K-medias, regresión logística, etc., los resultados intermedios a menudo deben reutilizarse. Otro escenario de aplicación es la minería de datos interactiva , como ejecutar varias consultas ad hoc en el mismo conjunto de datos. En la mayoría de los marcos informáticos (como Hadoop), la forma de utilizar los resultados de cálculo intermedios es escribirlos en un dispositivo de almacenamiento externo (como HDFS), lo que aumentará la carga adicional (replicación de datos, E / S de disco y serialización). Aumentará el tiempo de ejecución de la aplicación.

RDD puede admitir eficazmente la reutilización de datos en la mayoría de las aplicaciones. Es una estructura de datos paralela tolerante a fallos que permite a los usuarios conservar explícitamente los resultados intermedios en la memoria y puede optimizar el almacenamiento de datos mediante particiones. , RDD admite una gran cantidad de operaciones de operador, los usuarios pueden usar fácilmente estos operadores para operar en RDD.

conceptos básicos

Un RDD es una colección de objetos distribuidos, que es esencialmente una colección de registros particionada de solo lectura. Cada RDD se puede dividir en múltiples particiones, y las diferentes particiones se almacenan en diferentes nodos de clúster ( como se muestra en la figura siguiente ). RDD es un modelo de memoria compartida altamente restringido, es decir, RDD es una colección de registros de partición de solo lectura, por lo que no se puede modificar. Solo hay dos formas de crear un RDD, una es crear un RDD basado en datos almacenados físicamente y la otra es obtener un nuevo RDD aplicando operaciones de transformación (transformación, como mapa, filtro, unión, etc.) en otros RDD.

Inserte la descripción de la imagen aquí

RDD no necesita materializarse, utiliza el linaje para determinar que se calcula a partir de RDD. Además, los usuarios pueden controlar la persistencia y la partición de RDD , y los usuarios pueden realizar operaciones de persistencia (como memoria o disco) en RDD que deben reutilizarse para mejorar la eficiencia informática. También es posible distribuir los elementos del RDD en diferentes máquinas de acuerdo con la clave registrada Por ejemplo, al realizar una operación JOIN en dos conjuntos de datos, la partición hash se puede asegurar de la misma manera.

caracteristica principal

  • Basado en memoria

    RDD es una colección de objetos ubicados en la memoria. RDD se puede almacenar en memoria, disco o memoria más disco, pero la razón por la que Spark es rápido se basa en el hecho de que los datos se almacenan en la memoria y cada operador no extrae datos del disco.

  • Dividir

    El particionamiento consiste en dividir el conjunto de datos lógicos en diferentes partes independientes. El particionamiento es un medio técnico para optimizar el rendimiento de los sistemas distribuidos, lo que puede reducir la transmisión del tráfico de la red. La distribución de los elementos de la misma clave en la misma partición puede reducir la mezcla causada influencias. RDD se divide en varias particiones, que se distribuyen en diferentes nodos del clúster.

  • Tipo fuerte

    Los datos en el RDD están fuertemente tipados. Cuando se crea el RDD, todos los elementos son del mismo tipo, lo que depende del tipo de datos del conjunto de datos.

  • Carga lenta

    La operación de conversión de Spark es el modo de carga diferida, lo que significa que solo después de que se ejecuten las operaciones de acción (como contar, recopilar, etc.), se ejecutará una serie de operaciones de operador.

  • Inmutable

    Una vez que se crea un RDD, no se puede modificar. Solo convierta de un RDD a otro RDD.

  • Paralelización

    RDD se puede operar en paralelo, porque RDD está particionado y cada partición se distribuye en una máquina diferente, por lo que cada partición se puede operar en paralelo.

  • Resistencia

    Dado que RDD tiene una carga diferida, solo las operaciones de acción harán que se ejecute la operación de conversión de RDD y luego crearán el RDD correspondiente. Para algunos RDD que se reutilizan, pueden persistir (como almacenarlos en memoria o disco, Spark admite múltiples estrategias de persistencia) para mejorar la eficiencia informática.

Amplia dependencia y dependencia estrecha

Diferentes operaciones en el RDD harán que las particiones en diferentes RDD produzcan dependencias diferentes Existen principalmente dos tipos de dependencias: dependencias amplias y dependencias estrechas . La dependencia amplia significa que una partición de un RDD principal corresponde a múltiples particiones de un RDD secundario, y una dependencia estrecha significa que una partición de un RDD principal corresponde a una partición de un RDD secundario, o que varias particiones de un RDD principal corresponden a una partición RDD secundaria. Acerca de la dependencia amplia y la dependencia estrecha, como se muestra en la siguiente figura:

Inserte la descripción de la imagen aquí

División de la etapa

Las dependencias estrechas se dividirán en la misma etapa, que se puede ejecutar iterativamente en forma de canalizaciones. En general, hay varias particiones de las que dependen amplias dependencias, por lo que los datos deben transmitirse a través de los nodos. Desde la perspectiva de la tolerancia a desastres, los dos resultados de cálculo dependientes se restauran de diferentes maneras. La dependencia estrecha solo necesita restaurar las particiones perdidas por el RDD principal, mientras que la dependencia amplia debe considerar la recuperación de todas las particiones perdidas por el RDD principal.

DAGScheduler divide el RDD del trabajo en diferentes etapas y crea una dependencia de etapa, a saber, DAG. El propósito de esta división no es solo asegurar que las etapas sin dependencias se puedan ejecutar en paralelo, sino también asegurar que las etapas con dependencias se ejecuten secuencialmente. Stage se divide principalmente en dos tipos, uno es ShuffleMapStage y el otro es ResultStage . Entre ellos, ShuffleMapStage pertenece a la etapa ascendente y ResulStage pertenece a la etapa más descendente, lo que significa que la etapa ascendente se ejecuta primero y el ResultStage se ejecuta en último lugar.

  • ShuffleMapStage

ShuffleMapStage es la etapa intermedia del proceso de programación de DAG. Puede contener una o más ShuffleMapTasks, que se utilizan para generar datos Shuffle. ShuffleMapStage puede ser la etapa previa de ShuffleMapStage, pero debe ser la etapa previa de ResultStage. Parte del código fuente es el siguiente:

private[spark] class ShuffleMapStage(
    id: Int,
    rdd: RDD[_],
    numTasks: Int,
    parents: List[Stage],
    firstJobId: Int,
    callSite: CallSite,
    val shuffleDep: ShuffleDependency[_, _, _],
    mapOutputTrackerMaster: MapOutputTrackerMaster)
  extends Stage(id, rdd, numTasks, parents, firstJobId, callSite) {
      // 省略代码
  }
}
  • ResultStage

ResultStage puede usar la función especificada para calcular la partición en el RDD y obtener el resultado final ResultStage es la última etapa que se ejecutará, como imprimir datos en la consola o escribir datos en un dispositivo de almacenamiento externo. Parte del código fuente es el siguiente:

private[spark] class ResultStage(
    id: Int,
    rdd: RDD[_],
    val func: (TaskContext, Iterator[_]) => _,
    val partitions: Array[Int],
    parents: List[Stage],
    firstJobId: Int,
    callSite: CallSite)
  extends Stage(id, rdd, partitions.length, parents, firstJobId, callSite) {
// 省略代码
}

Como se mencionó anteriormente, Spark genera DAG al analizar las dependencias de cada RDD y determina cómo dividir la etapa a través de las dependencias entre las particiones en cada RDD. La idea específica es: análisis inverso en DAG, desconectarse cuando encuentre una dependencia amplia y agregar el RDD actual a la etapa actual cuando encuentre una dependencia estrecha. Es decir, las dependencias estrechas se dividen en la misma etapa, formando así una tubería para mejorar la eficiencia informática. Por lo tanto, un gráfico DAG se puede dividir en varias etapas. Cada etapa representa un conjunto de tareas relacionadas que no tienen dependencias de orden aleatorio entre sí. Cada conjunto de tareas se enviará a TaskScheduler para su procesamiento de programación y, finalmente, Distribuya las tareas al Ejecutor para su ejecución.

Proceso de programación de trabajos de Spark

Spark primero realiza una serie de operaciones de conversión de RDD en el trabajo y crea un DAG (gráfico acíclico directo) basado en las dependencias entre los RDD. Luego divida el RDD en diferentes etapas de acuerdo con las dependencias de RDD, y cada etapa crea múltiples tareas según la cantidad de particiones, y finalmente envía estas tareas a los nodos de trabajo del clúster para su ejecución. El proceso específico se muestra en la siguiente figura:

Inserte la descripción de la imagen aquí

  • 1. Cree DAG y envíe DAG al sistema de programación;
  • 2. DAGScheduler es responsable de recibir DAG, dividir DAG en múltiples etapas y, finalmente, enviar la tarea en cada etapa a un TaskScheduler en forma de un conjunto de tareas (TaskSet) para el siguiente paso;
  • 3. Utilice el administrador del clúster para asignar recursos y programar tareas, y habrá un mecanismo de reintento correspondiente para las tareas fallidas. TaskScheduler es responsable de recibir TaskSet de DAGScheduler, luego creará TaskSetManager para administrar el TaskSet, y finalmente SchedulerBackend programará la tarea;
  • 4. Realizar tareas específicas y almacenar los resultados intermedios y finales de las tareas en el sistema de almacenamiento.

Operador RDD

Spark ofrece una gran cantidad de operadores de operaciones de RDD, que incluyen principalmente dos categorías: Transformación y Acción . A continuación se explican algunos operadores comunes.

Transformación

Las siguientes son algunas operaciones de transformación comunes. Vale la pena señalar que para RDD ordinario, se admiten las API de Scala, Java, Python y R, y para pairRDD, solo se admiten las API de Scala y Java. Algunos operadores comunes se explicarán a continuación:

  • mapa ( func )
  /**
   * 将每个元素传递到func函数中,并返回一个新的RDD
   */
  def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
  }
  • filtro ( func )
/**
   * 筛选出满足func函数的元素,并返回一个新的RDD
   */
  def filter(f: T => Boolean): RDD[T] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[T, T](
      this,
      (context, pid, iter) => iter.filter(cleanF),
      preservesPartitioning = true)
  }
  • flatMap ( func )
/**
   * 首先对该RDD所有元素应用func函数,然后将结果打平,一个元素会映射到0或者多个元素,返回一个新RDD 
   */
  def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
  }
  • mapPartitions ( func )
/**
   * 将func作用于该RDD的每个分区,返回一个新的RDD
   */
  def mapPartitions[U: ClassTag](
      f: Iterator[T] => Iterator[U],
      preservesPartitioning: Boolean = false): RDD[U] = withScope {
    val cleanedF = sc.clean(f)
    new MapPartitionsRDD(
      this,
      (context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(iter),
      preservesPartitioning)
  }
  • unión (otro conjunto de datos )
/**
   * 返回一个新的RDD,包含两个RDD的元素,类似于SQL的UNION ALL
   */
  def union(other: RDD[T]): RDD[T] = withScope {
    sc.union(this, other)
  }
  • intersección ( otherDataset )
/**
   * 返回一个新的RDD,包含两个RDD的交集
   */
  def intersection(other: RDD[T]): RDD[T] = withScope {
    this.map(v => (v, null)).cogroup(other.map(v => (v, null)))
        .filter { case (_, (leftGroup, rightGroup)) => leftGroup.nonEmpty && rightGroup.nonEmpty }
        .keys
  }
  • distinto ([ numPartitions ]))
 /**
   * 返回一个新的RDD,对原RDD元素去重
   */
  def distinct(): RDD[T] = withScope {
    distinct(partitions.length)
  }

  • groupByKey ([ numPartitions ])
/**
   * 将pairRDD按照key进行分组,该算子的性能开销较大,可以使用PairRDDFunctions.aggregateByKey
   *或者PairRDDFunctions.reduceByKey进行代替
   */
  def groupByKey(): RDD[(K, Iterable[V])] = self.withScope {
    groupByKey(defaultPartitioner(self))
  }

  • reduceByKey ( func , [ numPartitions ])
/**
   * 使用reduce函数对每个key对应的值进行聚合,该算子会在本地先对每个mapper结果进行合并,然后再将结果发送到reducer,类似于MapReduce的combiner功能
   */
  def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
    reduceByKey(defaultPartitioner(self), func)
  }

  • aggregateByKey ( zeroValue ) ( seqOp , combOp , [ numPartitions ])
/**
   * 使用给定的聚合函数和初始值对每个key对应的value值进行聚合
   */
  def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
      combOp: (U, U) => U): RDD[(K, U)] = self.withScope {
    aggregateByKey(zeroValue, defaultPartitioner(self))(seqOp, combOp)
  }

  • sortByKey ([ ascendente ], [ numPartitions ])
/**
   * 按照key对RDD进行排序,所以每个分区的元素都是排序的
   */

  def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length)
      : RDD[(K, V)] = self.withScope
  {
    val part = new RangePartitioner(numPartitions, self, ascending)
    new ShuffledRDD[K, V, V](self, part)
      .setKeyOrdering(if (ascending) ordering else ordering.reverse)
  }

  • unirse ( otherDataset , [ numPartitions ])
/**
   * 将相同key的pairRDD JOIN在一起,返回(k, (v1, v2))tuple类型
   */
  def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))] = self.withScope {
    join(other, defaultPartitioner(self, other))
  }

  • cogroup ( otherDataset , [ numPartitions ])
/**
   * 将相同key的元素放在一组,返回的RDD类型为(K, (Iterable[V], Iterable[W1], Iterable[W2])
   * 第一个Iterable里面包含当前RDD的key对应的value值,第二个Iterable里面包含W1 RDD的key对应的    * value值,第三个Iterable里面包含W2 RDD的key对应的value值
   */
  def cogroup[W1, W2](other1: RDD[(K, W1)], other2: RDD[(K, W2)], numPartitions: Int)
      : RDD[(K, (Iterable[V], Iterable[W1], Iterable[W2]))] = self.withScope {
    cogroup(other1, other2, new HashPartitioner(numPartitions))
  }

  • coalesce ( numPartitions )
/**
   * 该函数用于将RDD进行重分区,使用HashPartitioner。第一个参数为重分区的数目,第二个为是否进行      * shuffle,默认为false;
   */
def coalesce(numPartitions: Int, shuffle: Boolean = false,
               partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
              (implicit ord: Ordering[T] = null)
      : RDD[T] = withScope {
    require(numPartitions > 0, s"Number of partitions ($numPartitions) must be positive.")
    if (shuffle) {
      /** Distributes elements evenly across output partitions, starting from a random partition. */
      val distributePartition = (index: Int, items: Iterator[T]) => {
        var position = new Random(hashing.byteswap32(index)).nextInt(numPartitions)
        items.map { t =>
          // Note that the hash code of the key will just be the key itself. The HashPartitioner
          // will mod it with the number of total partitions.
          position = position + 1
          (position, t)
        }
      } : Iterator[(Int, T)]

      // include a shuffle step so that our upstream tasks are still distributed
      new CoalescedRDD(
        new ShuffledRDD[Int, T, T](mapPartitionsWithIndex(distributePartition),
        new HashPartitioner(numPartitions)),
        numPartitions,
        partitionCoalescer).values
    } else {
      new CoalescedRDD(this, numPartitions, partitionCoalescer)
    }
  }

  • repartición ( numPartitions )
/**
   * 可以增加或者减少分区,底层调用的是coalesce方法。如果要减少分区,建议使用coalesce,因为可以避    * 免shuffle
   */
  def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
  }

Acción

Algunos operadores de acción comunes se muestran en la siguiente tabla

operando sentido
contar() Devuelve el número de elementos del conjunto de datos.
recoger() Devuelve todos los elementos del conjunto de datos como una matriz
primero() Devuelve el primer elemento del conjunto de datos.
tomado) Devuelve los primeros n elementos del conjunto de datos como una matriz
reducir (func) Agregue los elementos en el conjunto de datos a través de la función func (ingrese dos parámetros y devuelva un valor)
foreach (func) Pase cada elemento del conjunto de datos a la función func para ejecutar

Variable compartida

Spark proporciona dos tipos de variables compartidas: variables de difusión y acumuladores . Broadcast variables (Broadcast variables) es una variable de solo lectura, y guarda una copia en cada nodo, sin necesidad de enviar datos en el cluster. Los acumuladores pueden acumular datos de todas las tareas en un resultado compartido.

Variable de difusión

Las variables de difusión permiten a los usuarios compartir un valor inmutable en el clúster, y el valor compartido e inmutable se programa para cada nodo del clúster. Por lo general, se usa cuando es necesario copiar un pequeño conjunto de datos (como una tabla de dimensiones) en cada nodo del clúster, como las aplicaciones de análisis de registros, los registros web generalmente solo contienen pageId y el título de cada página se almacena en una tabla, si Para analizar el registro (por ejemplo, a qué páginas se accede con más frecuencia), debe unir las dos, luego puede usar las variables de transmisión para transmitir la tabla a cada nodo del clúster. Los detalles se muestran en la siguiente figura:

Inserte la descripción de la imagen aquí

Como se muestra en la figura anterior, el controlador primero divide el objeto serializado en pequeñas bases de datos y luego almacena estos bloques de datos en el BlockManager del nodo Controlador. Cuando se ejecuta una tarea específica en el ececutor, cada ejecutor primero intenta extraer datos del BlockManager del nodo donde se encuentra, si el valor de la variable broadcast se ha extraído antes, se utilizará directamente. Si no se encuentra extraerá el valor de la variable broadcast del Driver remoto u otro Ejecutor, una vez obtenido el valor será almacenado en el BlockManager de su propio nodo. Este mecanismo puede evitar el cuello de botella en el rendimiento causado por el controlador que envía datos a varios ejecutores.

El uso básico es el siguiente:

// 模拟一个数据集合
scala> val mockCollection = "Spark Flink Hadoop Hive".split(" ")
mockCollection: Array[String] = Array(Spark, Flink, Hadoop, Hive)
// 构造RDD
scala> val words = sc.parallelize(mockCollection,2)
words: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[7] at parallelize at <console>:29
// 模拟广播变量数据
scala> val mapData = Map("Spark" -> 10, "Flink" -> 20,"Hadoop" -> 15, "Hive" -> 9)
mapData: scala.collection.immutable.Map[String,Int] = Map(Spark -> 10, Flink -> 20, Hadoop -> 15, Hive -> 9)
// 创建一个广播变量
scala> val broadCast = sc.broadcast(mapData)
broadCast: org.apache.spark.broadcast.Broadcast[scala.collection.immutable.Map[String,Int]] = Broadcast(4)
// 在算子内部使用广播变量,根据key取出value值,按value升序排列
scala> words.map(word => (word,broadCast.value.getOrElse(word,0))).sortBy(wordPair => wordPair._2).collect
res5: Array[(String, Int)] = Array((Hive,9), (Spark,10), (Hadoop,15), (Flink,20))

acumulador

Acumulador (Acumulador) es otra variable compartida proporcionada por Spark. A diferencia de las variables de difusión, el acumulador se puede modificar y es variable. Cada transformación transmitirá el valor del acumulador modificado al nodo Driver, y el acumulador puede implementar una función de acumulación, similar a un contador. El propio Spark admite acumuladores numéricos y los usuarios también pueden personalizar el tipo de acumulador.

Inserte la descripción de la imagen aquí

Uso básico

Por sparkContext. longAccumulator()O SparkContext.doubleAccumulator()cree acumuladores de tipo Long y Double separados. Las tareas que se ejecutan en el clúster pueden llamar al método add para acumular la variable del acumulador, pero no pueden leer el valor del acumulador. Solo el programa Driver puede leer el valor del acumulador llamando al método del valor.

object SparkAccumulator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName(SparkShareVariable.getClass.getSimpleName)
    val sc = new SparkContext(conf)
    Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
    Logger.getLogger("org.apache.hadoop").setLevel(Level.OFF)
    val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
    val listRDD = sc.parallelize(list)
    var counter = 0 //外部变量
    //初始化一个accumulator,初始值默认为0
    val countAcc = sc.longAccumulator("my accumulator")
    val mapRDD = listRDD.map(num => {
      counter += 1 //在算子内部使用了外部变量,这样操作不会改变外部变量的值
      if (num % 3 == 0) {
        //遇到3的倍数,累加器+1
        countAcc.add(1)
      }
      num * 2
    })
    mapRDD.foreach(println)
    println("counter = " + counter) // counter = 0
    println("countAcc = " + countAcc.value) // countAcc = 4
    sc.stop()
  }
}

Recordatorio de gritos:

Algunas variables locales o variables miembro declaradas en dirver se pueden usar directamente en la transformación, pero después de la operación de transformación, el resultado final no se reasignará a la variable correspondiente en dirver. Porque después de que la operación de transformación se desencadena por acción, la operación de transformación es empaquetar el código a través de DAGScheduler, luego serializarlo y finalmente enviarlo al Ejecutor en cada nodo Worker para que sea ejecutado por TaskScheduler. Las variables ejecutadas en la transformación están en el propio nodo La variable no es la variable original en dirver, sino solo una copia de la variable correspondiente en el controlador.

Acumulador personalizado

Spark proporciona algunos acumuladores predeterminados y también admite acumuladores personalizados. Se puede implementar un acumulador personalizado heredando la clase AccumulatorV2. El código específico es el siguiente:

class customAccumulator extends AccumulatorV2[BigInt, BigInt]{
  private var num:BigInt = 0
  /**
    * 返回该accumulator是否为0值,比如一个计数器,0代表zero,如果是一个list,Nil代表zero 
    */
  def isZero: Boolean = {
    this.num == 0
  }
  // 创建一个该accumulator副本
  def copy(): AccumulatorV2[BigInt, BigInt] = {
    new customAccumulator
  }
  /**
    * 重置accumulator的值, 该值为0,调用 `isZero` 必须返回true
    */
  def reset(): Unit = {
    this.num = 0
  }
  // 根据输入的值,进行累加,
  // 判断为偶数时,累加器加上该值
  def add(intVal: BigInt): Unit = {
    if(intVal % 2 == 0){
      this.num += intVal
    }
  }
  /**
    * 合并其他的同一类型的accumulator,并更新该accumulator值
    */
  def merge(other: AccumulatorV2[BigInt, BigInt]): Unit = {
    this.num += other.value
  }
  /**
    * 定义当前accumulator的值
    */
  def value: BigInt = {
    this.num
  }
}


Utilice este acumulador personalizado

    val acc = new customAccumulator
    val newAcc = sc.register(acc, "evenAcc")
    println(acc.value)
    sc.parallelize(Array(1, 2, 3, 4)).foreach(x => acc.add(x))
    println(acc.value)

Resistencia

Método de persistencia

En Spark, RDD utiliza un mecanismo de evaluación diferido. Cada vez que se encuentra una operación de acción, el cálculo se ejecutará desde el principio. Cada vez que se llama a la operación de acción, se dispara un cálculo desde el principio. Para los RDD que deben reutilizarse, Spark admite la persistencia. Al llamar a los métodos persist () o cache (), puede lograr el plan de persistencia RDD. El mecanismo de persistencia puede evitar la sobrecarga causada por cálculos repetidos. Vale la pena señalar que cuando se llama al método de persistencia, el RDD solo se marca para persistencia, y el resultado del cálculo se mantendrá solo después de que se realice la primera operación de acción. El RDD persistente se mantendrá en la memoria del nodo informático y se reutilizará en acciones posteriores.

La principal diferencia entre los dos métodos de persistencia proporcionados por Spark es que el método cache () usa el nivel de memoria de forma predeterminada y la llamada subyacente es el método persist (). Los fragmentos de código fuente específicos son los siguientes:

def persist(newLevel: StorageLevel): this.type = {
    if (isLocallyCheckpointed) {
      persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
    } else {
      persist(newLevel, allowOverride = false)
    }
  }

  /**
   * 使用默认的存储级别持久化RDD (`MEMORY_ONLY`).
   */
  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

  /**
   * 使用默认的存储级别持久化RDD (`MEMORY_ONLY`).
   */
  def cache(): this.type = persist()

/**
  * 手动地把持久化的RDD从缓存中移除
   */
  def unpersist(blocking: Boolean = true): this.type = {
    logInfo("Removing RDD " + id + " from persistence list")
    sc.unpersistRDD(id, blocking)
    storageLevel = StorageLevel.NONE
    this
  }

Nivel de almacenamiento del plan de soporte

Spark proporciona una variedad de niveles de persistencia, como memoria, disco, memoria + disco, etc. Los detalles se muestran en la siguiente tabla:

Nivel de almacenamiento Sentido
MEMORY_ONLY De forma predeterminada, significa que el RDD se almacena en la JVM como un objeto Java deserializado. Si la memoria no es suficiente, algunas particiones no se conservarán. Cuando se utilicen estas particiones, se volverán a calcular.
MEMORY_AND_DISK Almacene el RDD como un objeto Java deserializado en la JVM. Si la memoria es insuficiente, la partición sobrante se almacenará en el disco duro.
MEMORY_ONLY_SER (Java y Scala) Serialice el RDD en un objeto Java para la persistencia, y cada partición corresponde a una matriz de bytes. Este método ahorra espacio que la deserialización, pero ocupará más recursos de la CPU
MEMORY_AND_DISK_SER (Java y Scala) Con MEMORY_ONLY_SER, si el almacenamiento interno no es suficiente, se desbordará y escribirá en el disco.
DISK_ONLY Almacene los datos de la partición RDD en el disco
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. Similar al método anterior, pero copiará los datos de la partición a los dos clústeres
OFF_HEAP (experimental) Similar a MEMORY_ONLY_SER, para almacenar datos en la memoria fuera de la pila fuera de la pila, la pila fuera de pila debe estar activada

Elección del nivel de persistencia

El nivel de almacenamiento persistente proporcionado por Spark es una compensación entre el uso de la memoria y la eficiencia de la CPU . Generalmente, se recomiendan las siguientes opciones:

  • Si la memoria puede contener RDD, puede usar el nivel de persistencia predeterminado, a saber, MEMORY_ONLY. Esta es la opción más eficiente para que la CPU haga que los operadores que actúan en el RDD se ejecuten lo más rápido posible.

  • Si la memoria no es suficiente, puede intentar usar MEMORY_ONLY_SER, usar una biblioteca de serialización rápida puede ahorrar mucho espacio, como Kryo.

    Sugerencias: en algunos operadores de reproducción aleatoria, como reduceByKey, incluso si no hay una llamada explícita al método persistir, Spark conservará automáticamente los resultados intermedios. El propósito de esto es evitar fallas durante la reproducción aleatoria y hacer que se recalcule toda la entrada. Aun así, se recomienda conservar los RDD que deben reutilizarse.

Caso completo

  • caso 1
/**
  *  1.数据集
  *          [orderId,userId,payment,productId]
  *          1,108,280,1002
  *          2,202,300,2004
  *          3,210,588,3241
  *          4,198,5000,3567
  *          5,200,590,2973
  *          6,678,8000,18378
  *          7,243,200,2819
  *          8,236,7890,2819
  *  2.需求描述
  *           计算Top3订单金额
  *           
  *  3.结果输出
  *    		 1	8000
  *		     2	7890
  *          3	5000       
  */
object TopOrder {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("TopN").setMaster("local")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    val lines = sc.textFile("E://order.txt")
    var num = 0;
    val result = lines.filter(line => (line.trim().length > 0) && (line.split(",").length == 4))
      .map(_.split(",")(2))     // 取出支付金额
      .map(x => (x.toInt,""))   
      .sortByKey(false)         // 按照支付金额降序排列    
      .map(x => x._1).take(3)   // 取出前3个
      .foreach(x => {
        num = num + 1
        println(num + "\t" + x)
      })
  } 
}

  • caso 2
/**
  * 1.数据集(movielensSet)
  *        用户电影评分数据[UserID::MovieID::Rating::Timestamp]
  *        电影名称数据[MovieId::MovieName::MovieType]
  * 2.需求描述
  *        求平均评分大于5的电影名称
  *
  */
object MovieRating {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("MovieRating").setMaster("local")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    // 用户电影评分数据[UserID::MovieID::Rating::Timestamp]
    val userRating = sc.textFile("E://ml-1m/ratings.dat")
    // 电影名称数据[MovieId::MovieName::MovieType]
    val movies = sc.textFile("E://ml-1m/movies.dat")
    //提取电影id和评分,(MovieID, Rating)
    val movieRating = userRating.map { line => {
      val rating = line.split("::")
      (rating(1).toInt, rating(2).toDouble)
    }
    }
    // 计算电影id及其平均评分,(MovieId,AvgRating)
    val movieAvgRating = movieRating
      .groupByKey()
      .map { rating =>
          val avgRating = rating._2.sum / rating._2.size
          (rating._1, avgRating)
      }
    //提取电影id和电影名称,(MovieId,MovieName)
   val movieName =  movies.map { movie =>
        val fields = movie.split("::")
        (fields(0).toInt, fields(1))

    }.keyBy(_._1)

    movieAvgRating
      .keyBy(_._1)
      .join(movieName) // Join的结果(MovieID,((MovieID,AvgRating),(MovieID,MovieName)))
      .filter(joinData => joinData._2._1._2 > 5.0)
      .map(rs => (rs._1,rs._2._1._2,rs._2._2._2))
      .saveAsTextFile("E:/MovieRating/")
  }

}

para resumir

Este artículo explica Spark Core en detalle, incluyendo principalmente los conceptos básicos de RDD, operadores de operación RDD, variables compartidas y planes de espera Finalmente, se dan dos casos completos de programación Spark Core. El próximo artículo compartirá la guía de programación Spark SQL.

Supongo que te gusta

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