SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint

接上篇文章第2章2.3.4:SparkCore之转换算子:RDD概述属性介绍,RDD特点,RDD编程模型,RDD的创建与转换, transformation转换算子Value类型与双value类型交互,key-value类型算子

文章目录

2.4 Action

2.4.1 reduce(func)案例

2.4.2 collect()案例

2.4.3 count()案例

2.4.4 first()案例

2.4.5 take(n)案例

2.4.6 takeOrdered(n)案例

2.4.7 aggregate案例

2.4.8 fold(num)(func)案例

2.4.9 saveAsTextFile(path)

2.4.10 saveAsSequenceFile(path) 

2.4.11 saveAsObjectFile(path) 

2.4.12 countByKey()案例

2.4.13 foreach(func)案例

2.5 RDD中的函数传递

2.5.1 传递一个方法

2.5.2 传递一个属性

2.6 RDD依赖关系

2.6.1 Lineage

2.6.2 窄依赖

2.6.3 宽依赖

2.6.4 DAG

2.6.5 任务划分(面试重点)

2.7 RDD缓存

2.8 RDD CheckPoint

2.4 Action

2.4.1 reduce(func)案例

1. 作用:通过func函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据。

2. 需求:创建一个RDD,将所有元素聚合得到结果。

(1)创建一个RDD[Int]

scala> val rdd1 = sc.makeRDD(1 to 10,2)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[85] at makeRDD at :24

(2)聚合RDD[Int]所有元素

scala> rdd1.reduce(_+_)

res50: Int = 55

(3)创建一个RDD[String]

scala> val rdd2 = sc.makeRDD(Array(("a",1),("a",3),("c",3),("d",5)))

rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[86] at makeRDD at :24

(4)聚合RDD[String]所有数据

scala> rdd2.reduce((x,y)=>(x._1 + y._1,x._2 + y._2))

res51: (String, Int) = (adca,12)

2.4.2 collect()案例

1. 作用:在驱动程序中,以数组的形式返回数据集的所有元素。

2. 需求:创建一个RDD,并将RDD内容收集到Driver端打印

(1)创建一个RDD

scala> val rdd = sc.parallelize(1 to 10)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

(2)将结果收集到Driver端

scala> rdd.collect

res0: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)   

2.4.3 count()案例

1. 作用:返回RDD中元素的个数

2. 需求:创建一个RDD,统计该RDD的条数

(1)创建一个RDD

scala> val rdd = sc.parallelize(1 to 10)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

(2)统计该RDD的条数

scala> rdd.count

res1: Long = 10

2.4.4 first()案例

1. 作用:返回RDD中的第一个元素

2. 需求:创建一个RDD,返回该RDD中的第一个元素

(1)创建一个RDD

scala> val rdd = sc.parallelize(1 to 10)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

(2)统计该RDD的条数

scala> rdd.first

res2: Int = 1

2.4.5 take(n)案例

1. 作用:返回一个由RDD的前n个元素组成的数组

2. 需求:创建一个RDD,统计该RDD的条数

(1)创建一个RDD

scala> val rdd = sc.parallelize(Array(2,5,4,6,8,3))

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at :24

(2)统计该RDD的条数

scala> rdd.take(3)

res10: Array[Int] = Array(2, 5, 4)

2.4.6 takeOrdered(n)案例

1. 作用:返回该RDD排序后的前n个元素组成的数组

2. 需求:创建一个RDD,统计该RDD的条数

(1)创建一个RDD

scala> val rdd = sc.parallelize(Array(2,5,4,6,8,3))

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at :24

(2)统计该RDD的条数

scala> rdd.takeOrdered(3)

res18: Array[Int] = Array(2, 3, 4)

2.4.7 aggregate案例

1. 参数:(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)

2. 作用:aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。

3. 需求:创建一个RDD,将所有元素相加得到结果

(1)创建一个RDD

scala> var rdd1 = sc.makeRDD(1 to 10,2)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[88] at makeRDD at :24

(2)将该RDD所有元素相加得到结果

scala> rdd.aggregate(0)(_+_,_+_)

res22: Int = 55

2.4.8 fold(num)(func)案例

1. 作用:折叠操作,aggregate的简化操作,seqop和combop一样。

2. 需求:创建一个RDD,将所有元素相加得到结果

(1)创建一个RDD

scala> var rdd1 = sc.makeRDD(1 to 10,2)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[88] at makeRDD at :24

(2)将该RDD所有元素相加得到结果

scala> rdd1.fold(0)(_+_)

res24: Int = 55

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第1张图片

 

解释:如果num值为1,fold结果为58,是因为先对组内折叠时两组+1,然后组间折叠+1

2.4.9 saveAsTextFile(path)

作用:将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本

2.4.10 saveAsSequenceFile(path) 

作用:将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。

2.4.11 saveAsObjectFile(path) 

作用:用于将RDD中的元素序列化成对象,存储到文件中。

2.4.12 countByKey()案例

1. 作用:针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。

2. 需求:创建一个PairRDD,统计每种key的个数

(1)创建一个PairRDD

scala> val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)

rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[95] at parallelize at :24

(2)统计每种key的个数

scala> rdd.countByKey

res63: scala.collection.Map[Int,Long] = Map(3 -> 2, 1 -> 3, 2 -> 1)

2.4.13 foreach(func)案例

1. 作用:在数据集的每一个元素上,运行函数func进行更新。

2. 需求:创建一个RDD,对每个元素进行打印

(1)创建一个RDD

scala> var rdd = sc.makeRDD(1 to 5,2)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[107] at makeRDD at :24

(2)对该RDD每个元素进行打印

scala> rdd.foreach(println(_))

3

4

5

1

2

2.5 RDD中的函数传递

在实际开发中我们往往需要自己定义一些对于RDD的操作,那么此时需要主要的是,初始化工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的。下面我们看几个例子:

2.5.1 传递一个方法

1.创建一个类

class Search(query:String){

  //过滤出包含字符串的数据
  def isMatch(s: String): Boolean = {
    s.contains(query)
  }

  //过滤出包含字符串的RDD
  def getMatch1 (rdd: RDD[String]): RDD[String] = {
    rdd.filter(isMatch)
  }

  //过滤出包含字符串的RDD
  def getMatche2(rdd: RDD[String]): RDD[String] = {
    rdd.filter(x => x.contains(query))
  }

}

2.创建Spark主程序

object SeriTest {

 

  def main(args: Array[String]): Unit = {

 

    //1.初始化配置信息及SparkContext

    val sparkConf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")

    val sc = new SparkContext(sparkConf)

 

    //2.创建一个RDD

    val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))

 

    //3.创建一个Search对象

    val search = new Search("h")

 

    //4.运用第一个过滤函数并打印结果

    val match1: RDD[String] = search.getMatch1(rdd)

    match1.collect().foreach(println)

  }

}

3.运行程序

Exception in thread "main" org.apache.spark.SparkException: Task not serializable

at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)

at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)

at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)

at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)

at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:387)

at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:386)

at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)

at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)

at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)

at org.apache.spark.rdd.RDD.filter(RDD.scala:386)

at RDD.action.Search.getMatch1(SeriTest.scala:38)

at RDD.action.SeriTest$.main(SeriTest.scala:25)

at RDD.action.SeriTest.main(SeriTest.scala)

Caused by: java.io.NotSerializableException: RDD.action.Search

Serialization stack:

- object not serializable (class: RDD.action.Search, value: RDD.action.Search@238ad8c)

- field (class: RDD.action.Search$$anonfun$getMatch1$1, name: $outer, type: class RDD.action.Search)

- object (class RDD.action.Search$$anonfun$getMatch1$1, )

at org.apache.spark.serializer.SerializationDebugger$.improveException(SerializationDebugger.scala:40)

at org.apache.spark.serializer.JavaSerializationStream.writeObject(JavaSerializer.scala:46)

at org.apache.spark.serializer.JavaSerializerInstance.serialize(JavaSerializer.scala:100)

at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:295)

... 12 more

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第2张图片

 

4.问题说明

//过滤出包含字符串的RDD

  def getMatch1 (rdd: RDD[String]): RDD[String] = {

    rdd.filter(isMatch)

  }

在这个方法中所调用的方法isMatch()是定义在Search这个类中的,实际上调用的是this. isMatch(),this表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

5.解决方案

使类继承scala.Serializable即可。

class Search() extends Serializable{...}

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第3张图片

 

2.5.2 传递一个属性

1.创建Spark主程序

object TransmitTest {

 

  def main(args: Array[String]): Unit = {

 

    //1.初始化配置信息及SparkContext

    val sparkConf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")

    val sc = new SparkContext(sparkConf)

 

//2.创建一个RDD

    val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))

 

//3.创建一个Search对象

    val search = new Search("h"

)

 

//4.运用第一个过滤函数并打印结果

    val match1: RDD[String] = search.getMatche2(rdd)

    match1.collect().foreach(println)

    }

}

2.运行程序

Exception in thread "main" org.apache.spark.SparkException: Task not serializable

    at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)

    at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)

    at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)

    at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)

    at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:387)

    at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:386)

    at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)

    at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)

    at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)

    at org.apache.spark.rdd.RDD.filter(RDD.scala:386)

    at com.atguigu.Search.getMatche1(SeriTest.scala:39)

    at com.atguigu.SeriTest$.main(SeriTest.scala:18)

    at com.atguigu.SeriTest.main(SeriTest.scala)

Caused by: java.io.NotSerializableException: com.atguigu.Search

3.问题说明

  //过滤出包含字符串的RDD

  def getMatche2(rdd: RDD[String]): RDD[String] = {

    rdd.filter(x => x.contains(query))

  }

在这个方法中所调用的方法query是定义在Search这个类中的字段,实际上调用的是this. query,this表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

4.解决方案

1)使类继承scala.Serializable即可。

class Search() extends Serializable{...}

 

2)将类变量query赋值给局部变量

修改getMatche2为

  //过滤出包含字符串的RDD

  def getMatche2(rdd: RDD[String]): RDD[String] = {

    val query_ : String = this.query//将类变量赋值给局部变量

    rdd.filter(x => x.contains(query_))

  }

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第4张图片

 

2.6 RDD依赖关系

2.6.1 Lineage

RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第5张图片

 

(1)读取一个HDFS文件并将其中内容映射成一个个元组

scala> val wordAndOne = sc.textFile("/fruit.tsv").flatMap(_.split("\t")).map((_,1))

wordAndOne: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[22] at map at :24

(2)统计每一种key对应的个数

scala> val wordAndCount = wordAndOne.reduceByKey(_+_)

wordAndCount: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[23] at reduceByKey at :26

(3)查看“wordAndOne”的Lineage

scala> wordAndOne.toDebugString

res5: String =

(2) MapPartitionsRDD[22] at map at :24 []

 |  MapPartitionsRDD[21] at flatMap at :24 []

 |  /fruit.tsv MapPartitionsRDD[20] at textFile at :24 []

 |  /fruit.tsv HadoopRDD[19] at textFile at :24 []

(4)查看“wordAndCount”的Lineage

scala> wordAndCount.toDebugString

res6: String =

(2) ShuffledRDD[23] at reduceByKey at :26 []

 +-(2) MapPartitionsRDD[22] at map at :24 []

    |  MapPartitionsRDD[21] at flatMap at :24 []

    |  /fruit.tsv MapPartitionsRDD[20] at textFile at :24 []

    |  /fruit.tsv HadoopRDD[19] at textFile at :24 []

(5)查看“wordAndOne”的依赖类型

scala> wordAndOne.dependencies

res7: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@5d5db92b)

(6)查看“wordAndCount”的依赖类型

scala> wordAndCount.dependencies

res8: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@63f3e6a8)

注意:RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

2.6.2 窄依赖

窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第6张图片

 

2.6.3 宽依赖

宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition,会引起shuffle,总结:宽依赖我们形象的比喻为超生

 

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第7张图片

2.6.4 DAG

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第8张图片

 

2.6.5 任务划分(面试重点)

RDD任务切分中间分为:Application、Job、Stage和Task

1)Application:初始化一个SparkContext即生成一个Application

2)Job:一个Action算子就会生成一个Job

3)Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第9张图片

 

4)Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task。

注意:Application->Job->Stage-> Task每一层都是1对n的关系。

2.7 RDD缓存

RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。

但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第10张图片

 

通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第11张图片

 

在存储级别的末尾加上“_2”来把持久化数据存为两份

SparkCore之行动算子:Action算子与案例,RDD中的函数传递,方法与属性的传递,RDD依赖关系,Lineage,宽依赖与窄依赖,DAG,任务的划分,RDD缓存,RDD CheckPoint_第12张图片

缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。

(1)创建一个RDD

scala> val rdd = sc.makeRDD(Array("atguigu"))

rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[19] at makeRDD at :25

(2)将RDD转换为携带当前时间戳做缓存

scala> val nocache = rdd.map(_.toString+System.currentTimeMillis)

nocache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[20] at map at :27

(3)多次打印结果

scala> nocache.collect

res0: Array[String] = Array(atguigu1538978275359)

 

scala> nocache.collect

res1: Array[String] = Array(atguigu1538978282416)

 

scala> nocache.collect

res2: Array[String] = Array(atguigu1538978283199)

(4)将RDD转换为携带当前时间戳做缓存

scala> val cache =  rdd.map(_.toString+System.currentTimeMillis).cache

cache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[21] at map at :27

(5)多次打印做了缓存的结果

scala> cache.collect

res3: Array[String] = Array(atguigu1538978435705)                                   

 

scala> cache.collect

res4: Array[String] = Array(atguigu1538978435705)

 

scala> cache.collect

res5: Array[String] = Array(atguigu1538978435705)

2.8 RDD CheckPoint

Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。

为当前RDD设置检查点。该函数将会创建一个二进制的文件,并存储到checkpoint目录中,该目录是用SparkContext.setCheckpointDir()设置的。在checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。

案例实操:

(1)设置检查点

scala> sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")

(2)创建一个RDD

scala> val rdd = sc.parallelize(Array("atguigu"))

rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[14] at parallelize at :24

(3)将RDD转换为携带当前时间戳并做checkpoint

scala> val ch = rdd.map(_+System.currentTimeMillis)

ch: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[16] at map at :26

 

scala> ch.checkpoint

(4)多次打印结果

scala> ch.collect

res55: Array[String] = Array(atguigu1538981860336)

 

scala> ch.collect

res56: Array[String] = Array(atguigu1538981860504)

 

scala> ch.collect

res57: Array[String] = Array(atguigu1538981860504)

 

scala> ch.collect

res58: Array[String] = Array(atguigu1538981860504)

你可能感兴趣的:(Spark生态体系)