Spark(1)-- 一文看懂Spark

Spark 介绍:        

Spark是一个快速而通用的大数据处理框架,它提供了高效的分布式数据处理和分析能力。

spark与Hadoop的关系: 

        Spark虽然不是Hadoop的一部分,但与Hadoop生态系统紧密集成。Spark提供了更快的数据处理和分析能力,具备批处理、流处理、机器学习和图计算等功能,spark可以理解为Hadoop中MapReduce的升级后的计算模型。

Hadoop VS Spark
Hadoop Spark
类型 完整的分布式基础平台,支持计算、存储、调度等 分布式计算工具,可用于迭代计算, 交互式计算, 流计算
生态系统 拥有Hive、Pig、HBase、Sqoop等完整的大数据处理和分析生态 支持与Hive、Pig、HBase、kafka等的集成;支持跨平台,可以与各种数据存储和处理系统集成
对机器要求 对内存要求很高
数据处理模型 使用MapReduce模型进行数据处理,数据通过HDFS分布在多个节点上,并在每个节点上进行Map和Reduce操作 使用 RDD 抽象来表示和处理分布式数据集,采用基于内存的计算模式,通过内存计算提高了数据分析和处理的速度。
计算速度 MapReduce模型适合处理批处理任务,但是在迭代计算和实时数据处理上因为磁盘IO比较慢,速度相对较慢 Spark通过内存计算和更高级别的数据抽象(如RDD、DataFrames和Datasets)提供了更快的计算速度,在迭代计算、流处理和交互式查询上具有优势
数据处理灵活性 Hadoop的HDFS,适用于长期存储和批量处理,但对于复杂的数据处理需求,需要借助其他工具和技术,如Hive和Pig Spark支持批处理、流处理、机器学习和图计算等,且提供了高级API(如Spark SQL)和专门的库(如MLlib和GraphX)进行更复杂的数据分析和处理
数据存储结构 MapReduce 中间计算结果存在 HDFS 磁盘上, 延迟大 RDD 中间运算结果存在内存中 , 延迟小
编程范式 Map+Reduce, API 较为底层, 算法适应性差 RDD 组成 DAG 有向无环图, API 较为顶层, 方便使用
运行方式 Task 以进程方式维护, 任务启动慢 Task 以线程方式维护, 任务启动快

spark体系结构: 

  • Spark Core: Spark的核心组件, 提供了RDD、分布式任务调度、内存计算、数据传输、错误恢复、与存储系统交互等基本功能,是其他spark组件的基础
    • RDD(Resilient Distributed Datasets):弹性分布式数据集, 是Spark的核心数据抽象, 代表一个可分区、可并行计算的不可变数据集。RDD提供了弹性的容错性、数据分区和数据转换操作,是实现分布式计算的基本单元
  • Spark SQL:是Spark的SQL查询和处理模块, 提供了对结构化数据的处理能力,支持使用SQL语句查询和分析数据,还可以与RDD进行无缝集成
  • Spark Streaming:Spark的流处理模块,用于处理实时数据流。 提供了高级API,可以对来自各种数据源的数据进行实时计算和处理
  • MLib (Machine Leaning Libary):是Spark的机器学习库,提供了一系列常用的机器学习算法和工具,用于大规模数据集上的机器学习任务。
  • GraphX: Spark的图处理库, 用于处理数据和图计算。提供了用于构建和操作图的API,支持图算法和图分析等任务。
  • Spark Job:通过Spark提交到集群上执行的一个任务, 他包含了需要执行的代码、数据和设置

Spark Core: 

 RDD:

RDD(Resilient Distributed Datasets):弹性分布式数据集, 是Spark的核心数据抽象, 代表一个可分区、可并行计算的不可变数据集。RDD提供了弹性的容错性、数据分区和数据转换操作,是实现分布式计算的基本单元。

  • Resilient :它是弹性的,RDD 里面的中的数据可以保存在内存中或者磁盘里面;
  • Distributed : 它里面的元素是分布式存储的,可以用于分布式计算;
  • Dataset它是一个集合,可以存放很多元素。

RDD源码中有这么一段描述: 

A list of partitions
A function for computing each split
A list of dependencies on other RDDs
Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
Optionally, a list of preferred locations to compute each split on(e.g. block locations for an HDFS file)
  • A list of partitions: 分区列表,一个分区就是RDD的一个计算单元,分区数决定并行度。
  • A function for computing each split:函数会被作用在每个分区上,RDD以分区为单位进行计算。
  • A list of dependencies on other RDDs:RDD会以来其他多个RDD,RDD每次转换又会生成新的RDD。一旦有分区数据丢失,可以通过这种依赖关系,重新计算丢失分区的数据。(Spark 的容错机制)
  • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned):可选项,对于KV类型的RDD会有一个Partitioner(分区函数), 默认是HashPartitioner
  • Optionally, a list of preferred locations to compute each split on(e.g. block locations for an HDFS file):可选项,存储分区的优先位置。对于HDFS文件,列表保存的是每个分区所在块的位置。

上面的描述一次对应Spark的分区列表、计算函数、依赖关系、分区函数、最佳位置。

  • 分区列表、分区函数、最佳位置,这三个属性其实说的就是数据集在哪,在哪计算更合适,如何分区;
  • 计算函数、依赖关系,这两个属性其实说的是数据集怎么来的。

RDD的一些关键特性和使用方式:

  1. 分区(Partitioning):RDD将数据集划分为多个逻辑分区(logical partitions),并将这些分区分布在集群中的多个节点上。分区的数量决定了并行计算的程度。

  2. 弹性(Resilience):RDD具备弹性特性,即当某个分区的数据或节点发生故障时,Spark可以通过血缘关系(lineage)重新计算丢失的分区,从而实现了容错性

  3. 不可变性(Immutability):RDD是不可变的,一旦创建就不能被修改。若要对RDD进行转换或操作,需要生成一个新的RDD。

  4. 转换操作(Transformations):RDD提供了一系列的转换操作,例如map、filter、reduce、join等。通过这些转换操作,可以从一个RDD中创建另一个具有不同数据结构或内容的RDD,实现数据转换和处理。

  5. 动作操作(Actions):RDD提供了一些动作操作,如count、collect、reduce等。动作操作会触发实际的计算,并返回结果给驱动程序或将结果保存到外部存储系统。

  6. 数据持久化(Data Persistence):RDD支持通过将数据缓存在内存中来加速数据访问。可以选择将RDD持久化到内存、磁盘或者以序列化的方式存储。

RDD API

1、创建RDD 

RDD创建有三种方法: 

1.1 从集合创建RDD, 使用SparkContext的parallelize方法: 

val sparkConf = new SparkConf().setAppName("RDD creation")
val sc = new SparkContext(sparkConf)

val data = List(1, 2, 3, 4, 5)
val rdd = sc.parallelize(data)

1.2 从外部存储创建RDD, 适用SparkContext的textFile方法

val sparkConf = new SparkConf().setAppName("RDD creation")
val sc = new SparkContext(sparkConf)

// 指定读取文件路径或包含文件的目录路径
val rdd = sc.textFile("hdfs://path/to/textfile.txt")
// 指定分区数
val rdd2 = sc.textFile("hdfs://path/to/textfile.txt", 7)

1.3 从其他RDD进行转换创建RDD:可以使用RDD的转换操作(转换算子)(如mapfilterflatMap等)

val sparkConf = new SparkConf().setAppName("RDD creation")
val sc = new SparkContext(sparkConf)

val input = sc.parallelize(List(1, 2, 3, 4, 5))
val rdd = input.map(x => x * 2)

2. RDD算子

RDD 不实际存储真正要计算的数据,而是记录了数据的位置在哪里,数据的转换关系(调用了什么方法,传入什么函数)。

RDD算子主要分为两类: 

  • 转换算子(transformation):返回一个新的 RDD。RDD 中的所有转换都是惰性求值/延迟执行的,也就是说并不会直接计算。只有当发生一个要求返回结果给 Driver 的 Action动作时,这些转换才会真正运行。因为这样可以在 Action 时对 RDD 操作形成 DAG有向无环图进行 Stage 的划分和并行优化,这种设计让 Spark 更加有效率地运行
  • 动作算子(action):返回值不是 RDD(无返回值或返回其他的)。

转换算子(transformation): 

  1. 单个元素转换:
    1. map(func):对RDD中的每个元素应用func函数,并返回一个新的RDD
    2. flatMap(func):对RDD中的每个元素应用func函数,并返回一个扁平化的结果集合的新RDD
    3. filter(func):根据func函数的返回值为true或false,对RDD中的元素进行过滤,并返回一个新的RDD
    4. mapPartitions(func):类似于 map,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] => Iterator[U]
    5. mapPartitionsWithIndex(func):类似于 mapPartitions,但 func 带有一个整数参数表示分片的索引值,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是(Int, Interator[T]) => Iterator[U]
  2. 多个元素转换:
    1. union(other):返回一个包含两个RDD(当前RDD和other RDD)所有元素的新RDD
    2. intersection(other):返回一个包含两个RDD(当前RDD和other RDD)共有元素的新RDD
    3. subtract(other):返回一个只包含当前RDD中存在而other RDD中不存在元素的新RDD
    4. distinct([numTasks]):返回一个去重后的新RDD
    5. cartesian(other):返回一个包含两个RDD(当前RDD和other RDD)所有元素组合的新RDD
    6. join(otherDataset, [numTasks]):在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的 RDD
    7. cogroup(otherDataset, [numTasks]):在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
    8. cartesian(otherDataset):笛卡尔积
  3. 分区相关转换:
    1. repartition(numPartitions):重新分区,返回一个具有指定分区数的新RDD
    2. coalesce(numPartitions):重新分区,减少 RDD 的分区数到指定值。在过滤大量数据之后,可以执行此操作
    3. partitionBy(partitioner):根据指定的分区器对RDD进行分区,并返回一个新RDD。
    4. groupByKey([numTasks]):在一个(K,V)的 RDD 上调用,返回一个(K, Iterator[V])的 RDD
    5. reduceByKey(func, [numTasks]):在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的 reduce 函数,将相同 key 的值聚合到一起,与 groupByKey 类似,reduce 任务的个数可以通过第二个可选的参数来设置
    6. sortByKey([ascending], [numTasks]):在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口,返回一个按照 key 进行排序的(K,V)的 RDD
    7. sortBy(func, ascending=True, numPartitions=None):根据func函数的返回值对RDD进行排序,并返回一个新的排序后的RDD。
  4. 其他转换
    1. sample(withReplacement, fraction, seed):根据给定的采样比例fraction,对RDD进行采样,并返回一个新的RDD
    2. glom():将每个分区的元素转换为数组,并返回一个新的RDD
    3. pipe(command, [envVars]):对 rdd 进行管道操作

动作算子(action):

  1. 基本动作 
    1. reduce(func):通过func函数来减少RDD中的元素,并返回减少后的结果
    2. collect():以数组的形式返回RDD中的所有元素
    3. count():返回RDD中的元素数量
    4. first():返回RDD中的第一个元素
    5. take(n):返回RDD中的前n个元素
  2. 统计相关动作
    1. max():返回RDD中的最大值
    2. min():返回RDD中的最小值
    3. sum():返回RDD中的所有元素的总和
    4. mean():返回RDD中的平均值
    5. variance():返回RDD中元素的方差
    6. stdev():返回RDD中元素的标准差
  3. 其他动作
    1. foreach(func):对RDD中的每个元素应用func函数
    2. foreachPartition(func):对RDD中的每个分区应用func函数
    3. countByValue():针对(K,V)类型的 RDD,返回一个(V,Int)的 map,表示每一个 value 对应的元素个数
    4. countByKey():针对(K,V)类型的 RDD,返回一个(K,Int)的 map,表示每一个 key 对应的元素个数

3、RDD持久化/缓存 

        某些 RDD 的计算或转换可能会比较耗费时间,如果这些 RDD 后续还会频繁的被使用到,那么可以将这些 RDD 进行持久化/缓存来提高RDD的重用和计算效率。持久化和缓存操作允许将RDD的数据存储在内存或磁盘上,从而避免重复计算和提高数据访问速度。

val rdd1 = sc.textFile("hdfs://node01:8020/words.txt")
val rdd2 = rdd1.flatMap(x=>x.split(" ")).map((_,1)).reduceByKey(_+_)
rdd2.cache //缓存, 等同于rdd2.persist(StorageLevel.MEMORY_ONLY)
rdd2.sortBy(_._2,false).collect//触发action,会去读取HDFS的文件,rdd2会真正执行持久化
rdd2.sortBy(_._2,false).collect//触发action,会去读缓存中的数据,执行速度会比之前快,因为rdd2已经持久化到内存中了

3.1 persist(storageLevel: StorageLevel)方法:将RDD标记为持久化,并可以指定数据存储级别Storage Level:

  1. MEMORY_ONLY:将RDD的数据存储在内存中
  2. MEMORY_AND_DISK:如果内存不足时,将RDD的数据存储在内存中,其余数据溢出到磁盘(一般在开发中使用)
  3. MEMORY_ONLY_SER:将RDD的数据以序列化的方式存储在内存中
  4. MEMORY_AND_DISK_SER:类似于MEMORY_AND_DISK,内存中放不下,则溢写到磁盘上,但以序列化方式存储数据。
  5. DISK_ONLY:将RDD分区存储在磁盘上
  6. MEMORY_ONLY_2, MEMORY_AND_DISK_2等:将持久化数据存储为2份,备份每个分区存储在两个集群节点上

3.2  cache()方法:是persist()方法的一种简化形式,使用默认的存储级别MEMORY_ONLY标记RDD为持久化

注意: 

  • 持久化和缓存只对后续操作有效,需要在RDD的转换操作之前进行。
  • 只有执行了action操作时,才会真正将RDD数据进行持久化/缓存
  • 如果RDD的数据量较大,而内存不足以容纳全部数据,可以选择将RDD存储在磁盘上(MEMORY_AND_DISKMEMORY_AND_DISK_SER),以便在需要时从磁盘读取数据。
  • 使用unpersist()方法可以手动将持久化的RDD从内存中移除

3.3 checkpoint 持久化 (容错机制)

RDD的Checkpoint是一种在Spark中进行持久化的机制的容错机制,它可以将中间结果保存到可靠的分布式存储系统(如HDFS)上,以便在节点故障时可以快速恢复。

设置检查点:

val conf = new SparkConf()
        .setAppName("CheckpointExample")
        .set("spark.checkpoint.dir", "hdfs://path/to/checkpoints")
val sc = new SparkContext(conf)
val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
rdd.checkpoint()

使用检查点恢复数据: 

val conf = new SparkConf().setAppName("CheckpointRecovery").set("spark.checkpoint.dir", "hdfs://path/to/checkpoints")
val sc = new SparkContext(conf)
sc.setCheckpointDir("hdfs://path/to/checkpoints")

使用Checkpoint机制时需要注意以下几点:

  • 检查点是一种开销较高的操作,因为它需要将数据写入磁盘。因此,只有在需要长时间或重复使用中间结果时才使用它。
  • 检查点操作会中断RDD的依赖关系图,因此建议在需要持久化结果之后立即执行检查点操作。
  • 检查点数据占用磁盘存储空间,应该根据可用资源和应用程序的需求进行适当的管理和清理。

相比于persist : 

  • Persist适用于需要对RDD的数据进行反复使用,或者需要缓存中间结果供后续计算使用的场景。
  • Checkpoint适用于需要长时间保留数据、进行可靠性保护、或者RDD依赖关系图较长等场景。

Spark SQL 

Spark SQL 介绍:

Spark SQL结合了传统的RDD API和SQL查询语言,使得开发人员可以使用SQL语句或Spark特有的DataFrame API进行数据操作和分析,从而更加方便和高效地处理结构化数据。

主要特性:

  1. DataFrame和Dataset:Spark SQL引入了DataFrame和Dataset这两个抽象概念,它们提供了更高级的数据操作接口。

    1. DataFrame是一种以RDD为基础的分布式数据集合,具有类似关系型数据库表的结构,可以进行筛选、聚合和SQL查询等操作。可以理解为JSON结构,只有列名和字段值类型限制。

    2. Dataset则提供了更加类型安全的API,可以通过编译时校验来避免运行时错误。相比于DataFrame,DataSet可以理解为Entity,除了字段名和字段值类型限制,还有实体类型限制。

  2. SQL查询和内置函数:Spark SQL支持使用标准的SQL查询语句对数据进行查询和聚合操作,允许开发人员利用熟悉的SQL语法进行数据分析。此外,Spark SQL还内置了许多常用的SQL函数(如聚合函数、日期函数等),以便更方便地对数据进行处理。

  3. 数据源扩展:Spark SQL支持各种数据源的连接和操作,包括关系型数据库(如MySQL、PostgreSQL)、Hive、Parquet、Avro、JSON、CSV等。借助Spark SQL的数据源接口,可以方便地读取、写入和处理不同格式的数据。

  4. Catalyst优化器:Spark SQL通过Catalyst优化器对SQL查询进行优化,包括查询解析、逻辑优化、物理计划生成和执行优化等。Catalyst使用基于规则和成本的优化策略,能够自动转换和优化查询计划,提高查询性能。

Spark SQL 简单shell 操作示例:

Spark SQL 的 DSL风格示例:

# 打开Spark shell 控制台
sh bin/spark-shell

# 读取文件,创建RDD 
val lineRDD = sc.textFile("file:///Users/zhoushimiao/Desktop/people.txt").map(_.split(" ")) 

# 定义Person 字段和字段类型
case class Person(id:Int, name:String, age:Int)

#将 RDD 和 Person 关联
val personRDD = lineRDD.map(x => Person(x(0).toInt, x(1), x(2).toInt)) //RDD[Person] , DataSet
# 将 RDD 转换成 DataFrame
val personDF = personRDD.toDF //DataFrame

# 查看数据
personDF.show
personDF.select(personDF.col("name")).show
personDF.select(personDF("name")).show
personDF.select(col("name")).show
personDF.select("name").show 

也可以直接构建DataFrame:

val dataFrame=spark.read.text("file:///Users/zhoushimiao/Desktop/people.txt")
dataFrame.show //注意:直接读取的文本文件没有完整schema信息
dataFrame.printSchema

# 或者, 直接读取JSON文件, json中已经包含了schema信息
val jsonDF= spark.read.json("file:///Users/zhoushimiao/Desktop/people.json")
jsonDF.show

SQL 风格: 

# 将DataFrame 转换成注册表
personDF.createOrReplaceTempView("t_person")

# 执行查询SQL
spark.sql("select id,name from t_person where id > 3").show

        Spark SQL提供了两种不同的API,分别是SQL语法和DataFrame/Dataset API,也被称为Spark SQL DSL(Domain Specific Language):

  • SQL语法: Spark SQL支持标准的SQL查询语法,可以使用SQL语句对数据进行查询、过滤和聚合等操作。使用SQL语法的好处是可以直接使用熟悉的SQL语句进行数据处理,无需学习新的API。SQL语法在处理结构化数据时非常直观和方便,并且适用于熟悉SQL的开发人员。
  • DataFrame/Dataset API(Spark SQL DSL):DataFrame/Dataset API是Spark SQL提供的一套编程接口,它基于RDD,但提供了以结构化方式组织数据的能力。DataFrame/Dataset API定义了丰富的操作方法,允许开发人员通过编程方式构建查询和转换操作。它提供了类型安全的操作,可以在编译时捕获错误,并且提供了更好的性能优化和代码调优能力。

Spark的流处理

        Spark Streaming和Structured Streaming都是Apache Spark的流处理模块,用于处理实时数据流。

Spark Streaming: 

        Spark Streaming 是一个基于 Spark Core 之上的实时计算框架,可以从很多数据源消费数据并对数据进行实时的处理,具有高吞吐量和容错能力强等特点。Spark Streaming使用DStreams (离散流), 可以使用高级函数和RDD操作进行数据处理。

Spark Streaming的特点: 

  1. 低延迟:Spark Streaming通过将输入流数据分割成小的批次,并在每个批次上进行处理,从而实现了低延迟的流处理。默认情况下,每个批次的大小是几秒钟。
  2. 容错性:Spark Streaming提供了容错机制,可以处理节点故障或数据丢失的情况。它使用Spark的弹性分布式数据集(RDDs)来恢复丢失的数据和故障的节点。

  3. 可扩展性:Spark Streaming可以与Spark的其他模块(如Spark SQL、MLlib)集成,从而实现更复杂的流处理和分析任务。它可以在大规模集群上运行,并根据数据量和负载自动扩展。

  4. 支持丰富的数据源:Spark Streaming支持多种数据源,包括Kafka、Flume、Hadoop HDFS、Apache NiFi等。这使得从多种来源获取流数据变得简单,并能够将数据处理结果发送到多个目标。

示例代码: 

import org.apache.spark.streaming._

// 创建StreamingContext,批次时间间隔为5秒
val ssc = new StreamingContext(sparkConf, Seconds(5)) 
// 从TCP套接字获取流数据
val lines = ssc.socketTextStream("localhost", 9999)

// 对流数据进行处理
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(word => (word, 1)).reduceByKey(_ + _)

// 打印处理结果
wordCounts.print()

// 启动StreamingContext 和 等待流数据的到达和处理
ssc.start() 
ssc.awaitTermination() 

ssc.stop()

Structured Streaming

        Structured Streaming基于DataFrame和Dataset API,这使得流处理代码和批处理代码之间的转换非常容易。开发者可以使用SQL查询、DataFrame的操作和内置函数来处理实时流数据,让开发者能够以类似于静态数据处理的方式进行流处理。

        可以理解为,Structured Streaming 将数据源映射为类似于关系数据库中的表,然后将经过计算得到的结果映射为另一张表,完全以结构化的方式去操作流式数据,这种编程模型非常有利于处理分析结构化的实时数据;Spark Structured Streaming提供了更简化和统一的流处理编程模型,使得对实时流数据的处理变得更容易和直观。它支持更复杂的查询和操作,并提供了高级抽象和语义保证,以满足各种实时数据处理需求。        

主要特点和优势:

  1. 基于事件时间(event time):Structured Streaming支持处理基于事件时间的数据,这意味着可以根据事件发生的真实时间处理数据,而不是根据数据到达的时间。这对于处理延迟和乱序事件数据非常有用。

  2. Exactly-once语义保证:Structured Streaming提供了"恰好一次"(exactly-once)的语义保证,确保输出结果的一致性和可靠性,即每个输入的数据都会被精确处理一次,不会有丢失或重复数据。

  3. 高级抽象:Structured Streaming提供了高级的抽象,如窗口操作、流-静态数据连接、流-流连接等,使得处理更复杂的流处理场景更加简单和直观。

  4. 支持丰富的数据源和数据接收器:Structured Streaming支持多个数据源和数据接收器,如Kafka、Socket、File、HDFS等,并且可以将处理结果发送到外部存储系统(如Hadoop HDFS、Apache Cassandra)或集成其他模块(如Spark SQL)进行进一步分析。

示例代码: 

// 导入必要的库
import org.apache.spark.sql.SparkSession

// 创建SparkSession, 也是一个Structured Streaming作业
val spark = SparkSession
  .builder()
  .appName("StructuredStreamingExample")
  .getOrCreate()

// 从数据源创建输入DataFrame
val inputData = spark
  .readStream
  .format("socket")
  .option("host", "localhost")
  .option("port", 9999)
  .load()

// 对输入数据进行转换和处理
val transformedData = inputData
  .groupBy("word")
  .count()

// 将结果DataFrame写入输出数据接收器
val query = transformedData
  .writeStream
  .format("console")
  .outputMode("complete")
  .start()

query.awaitTermination()

Spark Streaming VS Structured Streaming

共同点: 

  1. 基于Spark: 二者都是建立在Spark 框架智商,可以利用Spark的分布式计算能力和优化引擎
  2. 高可靠性和容错性: 二者都具备故障恢复和容错能力, 能够处理节点故障和数据丢失的问题
  3. 集成数据源和数据接收器: 二者都支持广泛的数据源, 包括Kafka、Flume、Hadoop HDFS 等,并且可以将处理结果发送到外部存储系统或集成其它模块进行

区别: 

  1. 数据处理模型:
    1. Spark Streaming 是基于微批处理模型的, 将实时数据流划分为小的时间批次,然后对每个批次进行处理;
    2. Structured Steaming 是基于连续处理模型的, 以类似静态表的形式处理数据, 可提供更接近实时的结果
  2. 容错性和一致性: 
    1. Spark Streaming 的容错性更依赖于批处理间隔, 不提供严格的一致性保证
    2. Structured Streaming 提供恰好一次的语义保证,确保结果的一致性和可靠性
  3. API和语义: 
    1. Spark Streaming 使用DStream(离散流),可以使用高级函数和RDD操作进行数据处理
    2. Structured Streaming 提供了恰好一次的语义保证,确保结果的一致性和可靠性

        此外, Structured Streaming提供了更简单和直观的编程模型,开发者可以通过类似静态表的API进行开发,减少了与批处理的编程范式之间的学习成本

Spark Shuffle 

        Shuffle是指在数据的重新分区和重组过程中发生的数据洗牌操作。

        在MapReduce 框架中, Shuffle 阶段是连结Map 和 Reduce 的桥梁, Map阶段通过Shuffle过程,将数据传输到Reduce。 由于Shuffle 涉及磁盘的读写和网络IO, 因此Shuffle 性能直接影响整个程序的性能。 Spark 也有Map和Reduce阶段, 因此也会出现shuffle。

Shuffle通常发生在以下情况

  1. 数据重分区:当需要将数据根据键或其他条件进行重新分发到集群中的不同节点时, 会发生数据重分区的Shuffle 操作。如GroupByKey、ReduceByKey 等操作
  2. 合并数据:当需要将来自不同节点的数据合并到一起,会发生合并数据的Shuffle 操作。 如 join 操作

Shuffle 的过程分为三个阶段:

  1. Map阶段: 每个Executor会根据数据的键或其他条件生成一个中间结果集,并将其写入本地磁盘
  2. Shuffle阶段:Spark会将各个Executor上的中间结果按照目标分区进行排序,并将相同分区的数据合并为一个文件块并写入磁盘。 这个阶段设计大量数据传输和IO读写。
  3. Reduce阶段:每个分区的数据会被发送到相应的Reduce任务进行聚合、合并、计算。

Spark中有三种主要的Shuffle实现方式:

  1. SortShuffle(排序洗牌):
    SortShuffle是一种基于排序的Shuffle实现方式,它将Map任务输出的数据先写入本地磁盘,然后Reducer任务再从Map任务所在的节点上拉取数据,并按键进行排序和合并。SortShuffle的优点是能够保证结果数据的全局有序性,适用于需要全局排序的场景,但可能会产生大量的磁盘写入和网络传输开销。

  2. HashShuffle(哈希洗牌):
    HashShuffle是一种基于哈希的Shuffle实现方式,它将Map任务输出的数据根据键的哈希值分组,将相同Key的数据发送到同一个Reducer任务上进行处理。HashShuffle具有良好的扩展性,因为数据在网络传输时是分散的,减少了磁盘写入和网络传输开销。但它不能保证结果的全局有序性。

  3. TungstenSortShuffle(Tungsten排序洗牌):
    TungstenSortShuffle是一种优化的Shuffle实现方式,通过使用Tungsten排序引擎来提高SortShuffle的性能。TungstenSortShuffle使用内存和磁盘结合的方式进行排序和合并,可以显著降低排序和合并过程中的磁盘读写开销,提高Shuffle的性能

        Spark默认情况下会根据可用资源和配置参数自动选择适合的Shuffle实现方式。通常情况下,当可用内存较小时,会使用SortShuffle;而当系统具备足够的内存资源时,会使用TungstenSortShuffle来提高性能。

Shuffle性能优化方法:

  • 调整并行度:通过调整分区数和并行度参数,可以减少数据移动和网络传输,从而提高Shuffle的性能。

  • 提前合并:对于多个Shuffle操作,可以将它们合并在一起以减少Shuffle的次数。

  • 使用本地性优化:通过使用数据本地性调度和预测性执行,将任务分配给尽可能靠近数据的节点,减少数据传输和读取的开销。

  • 使用内存和磁盘结合:合理配置内存和磁盘使用比例,将适当的数据存储在内存中,减少对磁盘的读写操作。

  • 使用压缩:对于大量数据的Shuffle操作,可以考虑使用数据压缩来减小网络传输和磁盘存储的开销。

Shuffle的过程是计算中非常昂贵的部分,因为它涉及数据的移动、排序和磁盘读写。因此,优化Shuffle操作对于提高Spark应用的性能和效率非常重要。

猜你喜欢

转载自blog.csdn.net/zhoushimiao1990/article/details/131794076