Spark编程-RDD

文章详细介绍了Spark中的弹性分布式数据集(RDD)的创建方式,包括从外部数据集加载和通过并行集合创建。接着,讨论了RDD的转换操作如filter、map、flatMap等,以及行动操作如count、collect、reduce。还提到了RDD的惰性求值机制和持久化机制,如cache和unpersist,以及分区原则。最后,提到在不同模式下打印RDD元素到控制台的方法和注意事项。
摘要由CSDN通过智能技术生成

RDD创建方式

第一种:

        读取外部数据集。例如:从本地文件加载数据集,或者从HDFS文件系统、HBase等外部数据源中加载数据集。

        Spark可以支持文本文件、SequenceFile文件(Hadoop提供的 SequenceFile是一个由二进制序列化过的key/value的字节流组成的文本存储文件)和其他符合Hadoop InputFormat格式的文件。
第二种:

        调用SparkContext的parallelize方法,在Driver中一个已经存在的集合(数组)上创建。

        eg:工作中基本上都是直接接口读取数据集,然后转换一下,例如下面代码

var gis_data = s.get(xxx).asInstanceOf[org.apache.spark.sql.DataFrame]

从本地文件中获取RDD

        Spark采用textFile()方法来从文件系统中加载数据创建RDD,该方法把文件的URI作为参数,这个URI可以是本地文件系统的地址,或者是分布式文件系统HDFS的地址,或者是Amazon S3的地址等等。

import org.apache.spark.{SparkConf, SparkContext}

object wcPerson {
  def main (args:Array[String]): Unit ={
    val conf = new SparkConf().setAppName("wcPerson").setMaster("local[1]")
    val sc = new SparkContext(conf)
    val inputFile = sc.textFile("D:\\workspace\\spark\\src\\main\\Data\\person")
    val wc = inputFile.flatMap(line => line.split(","))
      .map(word => (word,1)).reduceByKey((a,b) => a + b)
    wc.foreach(println)
  }
}

从HDFS文件系统中加载数据

在Spark-shell界面,sc读取hdfs上的文件

scala> val lines = sc.textFile("hdfs://localhost:9000/user/hadoop/testspark.txt")
scala> val lines = sc.textFile("/user/hadoop/testspark.txt")
scala> val lines = sc.textFile("testspark.txt")

 sc读取注意事项

        在使用Spark读取文件时,需要说明以下几点:
        (1)如果使用了本地文件系统的路径,那么,必须要保证在所有的worker节点上,也都能够采用相同的路径访问到该文件,比如,可以把该文件拷贝到每个worker节点上,或者也可以使用网络挂载共享文件系统。
        (2)textFile()方法的输入参数,可以是文件名,也可以是目录,也可以是压缩文件等。比如,textFile("/my/directory"), textFile("/my/directory/*.txt"), and textFile("/my/directory/*.gz").
        (3)textFile()方法也可以接受第2个输入参数(可选),用来指定分区的数目。默认情况下,Spark会为HDFS的每个block创建一个分区(HDFS中每个block默认是128MB)。你也可以提供一个比block数量更大的值作为分区数目,但是,你不能提供一个小于block数量的值作为分区数目。

并行集合创建RDD

        调用SparkContext的parallelize方法,在Driver中一个已经存在的集合(数组)上创建。

使用数组创建
scala> val array = Array(11,22,33,23,4,545)
array: Array[Int] = Array(11, 22, 33, 23, 4, 545)
scala> val rdd = sc.parallelize(array)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[6] at parallelize at <console>:24

 使用列表创建
scala> val list = List(11,234,234,5425,342)
list: List[Int] = List(11, 234, 234, 5425, 342)
scala> val rdd1 = sc.parallelize(list)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[7] at parallelize at <console>:24

RDD操作(转换与行动)

        RDD的操作有两种,一种是 转换(Transformation),一种是行动类(action),对于RDD而言,每一次转换操作都会产生不同的RDD,供给下一个“转换”使用。转换得到的,RDD是惰性求值的,也就是说,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,只有遇到行动操作时,才会发生真正的计算,开始从血缘关系源头开始,进行物理的转换操作。
Spark常见的转换(Transformation)操作

filter(func):                 筛选出满足函数func的元素,并返回一个新的数据集
map(func):                 将每个元素传递到函数func中,并将结果返回为一个新的数据集
flatMap(func):            与map()相似,但每个输入元素都可以映射到0或多个输出结果
groupByKey():           应用于(K,V)键值对的数据集时,返回一个新的(K, Iterable)形式的数据集
reduceByKey(func):   应用于(K,V)键值对的数据集时,返回一个新的(K, V)形式的数据集,其中的每个值是将每个key传递到函数func中进行聚合

样例代码

// 创建一个包含整数的RDD
val nums = sc.parallelize(List(1, 2, 3, 4, 5))
// 使用filter()筛选出偶数
val even_nums = nums.filter(x => x % 2 == 0)
println(even_nums.collect().mkString(", "))  
// 输出 2, 4

// 使用map()将每个元素加倍
val doubled_nums = nums.map(x => x * 2)
println(doubled_nums.collect().mkString(", "))  
// 输出 2, 4, 6, 8, 10

// 使用flatMap()将每个元素拆分成多个字符
val words = sc.parallelize(List("hello", "world", "spark"))
val chars = words.flatMap(x => x.toList)
println(chars.collect().mkString(", "))  
// 输出 h, e, l, l, o, w, o, r, l, d, s, p, a, r, k

// 创建一个键值对的RDD
val pairs = sc.parallelize(List((1, "apple"), (2, "banana"), (1, "orange"), (2, "grape")))
// 使用groupByKey()按键分组
val grouped_pairs = pairs.groupByKey()
println(grouped_pairs.collect().mkString(", "))  
// 输出 (1, CompactBuffer(apple, orange)), (2, CompactBuffer(banana, grape))

// 使用reduceByKey()将相同键的值相加
val sum_by_key = pairs.reduceByKey((x, y) => x + y)
println(sum_by_key.collect().mkString(", "))  
// 输出 (1, appleorange), (2, bananagrape)
单value
(1)map
(2)mapPartitions
(3)mapPartitionsWithIndex
(4)flatMap
(5)glom
(6)groupBy
(7)filter
(8)sample
(9)distinct
(10)coalesce
(11)repartition
(12)sortBy
(13)pipe
双Value
(1)intersection
(2)union
(3)subtract
(4)zip
key-value
(1)partitionBy
(2)reduceByKey
(3)groupByKey
(4)aggregateByKey
(5)foldByKey
(6)combineByKey
(7)sortByKey
(8)mapValues
(9)join
(10)cogroup

        

Spark中的action算子

        行动操作是真正触发计算的地方。Spark程序执行到行动操作时,才会执行真正的计算,从文件中加载数据,完成一次又一次转换操作,最终,完成行动操作得到结果。
下面列出一些常见的行动操作(Action API):
count()                     返回数据集中的元素个数
collect()                    以数组的形式返回数据集中的所有元素
first()                        返回数据集中的第一个元素
take(n)                     以数组的形式返回数据集中的前n个元素
reduce(func)            通过函数func(输入两个参数并返回一个值)聚合数据集中的元素
foreach(func)           将数据集中的每个元素传递到函数func中运行

代码样例:

假设我们有一个包含整数的示例数据集:
val data = Seq(1, 2, 3, 4, 5)
// count() 返回数据集中的元素个数:
val count = data.count()
println(count)  // 输出 5

// collect() 以数组的形式返回数据集中的所有元素
val collected = data.collect()
println(collected.mkString(", "))  // 输出 1, 2, 3, 4, 5

// first() 返回数据集中的第一个元素:
val firstElement = data.first()
println(firstElement)  // 输出 1

// take(n) 以数组的形式返回数据集中的前n个元素:
val taken = data.take(3)
println(taken.mkString(", "))  // 输出 1, 2, 3

// reduce(func) 通过函数func(输入两个参数并返回一个值)聚合数据集中的元素:
val sum = data.reduce((x, y) => x + y)
println(sum)  // 输出 15,即 1 + 2 + 3 + 4 + 5

//foreach(func) 将数据集中的每个元素传递到函数func中运行:
data.foreach(x => println(x * 2))  // 输出每个元素的两倍值

惰性机制(懒加载-Lazy Loading)

val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)

        第一行首先从外部文件data.txt中构建得到一个RDD,名称为lines,但是,由于textFile()方法只是一个转换操作,因此,这行代码执行后,不会立即把data.txt文件加载到内存中,这时的lines只是一个指向这个文件的指针

        第二行代码用来计算每行的长度(即每行包含多少个单词),同样,由于map()方法只是一个转换操作,这行代码执行后,不会立即计算每行的长度。

        第三行代码的reduce()方法是一个“动作”类型的操作,这时,就会触发真正的计算。这时,Spark会把计算分解成多个任务在不同的机器上执行,每台机器运行位于属于它自己的map和reduce,最后把结果返回给Driver Program。

部分实例

1、filter操作

           实例:计算出结果集中的元素“山西”个数
import org.apache.spark.{SparkConf, SparkContext}

object RDD_transformation {
  def main(args:Array[String]):Unit = {
    val conf = new SparkConf().setAppName("transformation").setMaster("local[1]")
    val sc = new SparkContext(conf)
    val personData = sc.textFile("D:\\workspace\\spark\\src\\main\\Data\\person")
    //filter操作
    val lines = personData.filter(line => line.contains("山西")).count
    //包含山西的
    println("文件中包含山西字段的数量:"+ lines)
  }
}
     ​​​运行结果及解释

    上面的代码中,lines是一个RDD。lines.filter()会遍历lines中的每行文本,并对每行文本执行括号中的匿名函数,也就是执行Lamda表达式:line => line.contains("山西"),在执行Lamda表达式时,会把当前遍历到的这行文本内容赋值给参数line,然后,执行处理逻辑line.contains("山西"),也就是只有当改行文本包含“Spark”才满足条件,才会被放入到结果集中。最后,等到lines集合遍历结束后,就会得到一个结果集,这个结果集中包含了所有包含“山西”的行。最后,对这个结果集调用count(),这是一个行动操作,会计算出结果集中的元素个数。

2、map操作

        实例:找出文本文件中单行文本所包含的单词数量的最大值
import org.apache.spark.{SparkConf, SparkContext}

object RDD_transformation {
  def main(args:Array[String]):Unit = {
    val conf = new SparkConf().setAppName("transformation").setMaster("local[1]")
    val sc = new SparkContext(conf)
    val personData = sc.textFile("D:\\workspace\\spark\\src\\main\\Data\\person")
    //map操作,文本文件中单行文本所包含的单词数量的最大值
    val lines1 = personData.map(line => line.split(",")
      .size).reduce((a,b) => if (a > b) a else b)
    println("文本文件中单行文本所包含的单词数量的最大值为:" + lines1)

  }
}
//拆分代码拆开来看
lines.map(line => line.split(" "))
res8: org.apache.spark.rdd.RDD[Array[String]] = MapPartitionsRDD[19] at map at <console>:30
//从上面执行结果可以发现,lines.map(line => line.split(" "))返回的结果是一个
//Array[String]类型的RDD,
//也就是说,这个RDD中的每个元素都是一个Array[String]
//(一行文本被切分成多个单词后就是保存在这个数组中)
scala> lines.map(line => line.split(" ").size)
res9: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[20] at map at <console>:30
//从上面执行结果信息可以发现,lines.map(line => line.split(" ").size)得到的
//RDD是Int类型的RDD,
//这个RDD中的每个元素都是一个整数值(也就是一行文本包含的单词数)
scala> lines.map(line => line.split(" ").size).reduce((a,b) => if (a>b) a else b)
res10: Int = 5
        运行结果解释

         上面代码中,lines是一个RDD,是String类型的RDD,因为这个RDD里面包含了很多行文本。

        lines.map(),是一个转换操作,B站林子雨老师之前说过,map(func):将每个元素传递到函数func中,并将结果返回为一个新的数据集,所以,lines.map(line => line.split(" ").size)会把每行文本都传递给匿名函数,也就是传递给Lamda表达式line => line.split(" ").size中的line,然后执行处理逻辑line.split(" ").size。

        line.split(" ").size这个处理逻辑的功能是,对line文本内容进行单词切分,得到很多个单词构成的集合,然后,计算出这个集合中的单词的个数。

        因此,最终lines.map(line => line.split(" ").size)转换操作得到的RDD,是一个整型RDD,里面每个元素都是整数值(也就是单词的个数)。

        最后,针对这个RDD[Int],调用reduce()行动操作,完成计算。reduce()操作每次接收两个参数,取出较大者留下,然后再继续比较,例如,RDD[Int]中包含了1,2,3,4,5,那么,执行reduce操作时,首先取出1和2,把a赋值为1,把b赋值为2,然后,执行大小判断,保留2。下一次,让保留下来的2赋值给a,再从RDD[Int]中取出下一个元素3,把3赋值给b,然后,对a和b执行大小判断,保留较大者3.依此类推。最终,reduce()操作会得到最大值是5。

   

持久化操作

        在Spark中,RDD采用惰性求值的机制,每次遇到行动操作,都会从头开始执行计算。如果整个Spark程序中只有一次行动操作,这当然不会有什么问题。但是,在一些情形下,我们需要多次调用不同的行动操作,这就意味着,每次调用行动操作,都会触发一次从头开始的计算。这对于迭代计算而言,代价是很大的,迭代计算经常需要多次重复使用同一组数据。

案例1:多次计算同一个RDD,无cache

scala> val list = List("hadoop","spark","hive")
list: List[String] = List(hadoop, spark, hive)
scala> val rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[0] at parallelize at <console>:24

scala> println(rdd.count()) // 行动操作,触发一次真正从头到尾的计算
[Stage 0:>                                                          (0 + 0) / 1[Stage 0:>                                                          (0 + 1) / 1          
                                                                     3
scala> println(rdd.collect())
[Ljava.lang.String;@afcfc63
scala> println(rdd.collect().mkString(",")) //行动操作,触发一次真正从头到尾的计算
hadoop,spark,hive

        上面代码执行过程中,前后共触发了两次从头到尾的计算。
        实际上,可以通过持久化(缓存)机制避免这种重复计算的开销可以使用persist()方法对一个RDD标记为持久化,之所以说“标记为持久化”,是因为出现persist()语句的地方,并不会马上计算生成RDD并把它持久化,而是要等到遇到第一个行动操作触发真正计算以后,才会把计算结果进行持久化,持久化后的RDD将会被保留在计算节点的内存中被后面的行动操作重复使用。
        persist()的圆括号中包含的是持久化级别参数,比如,persist(MEMORY_ONLY)表示将RDD作为反序列化的对象存储于JVM中,如果内存不足,就要按照LRU原则替换缓存中的内容。

        persist(MEMORY_AND_DISK)表示将RDD作为反序列化的对象存储在JVM中,如果内存不足,超出的分区将会被存放在硬盘上。

        一般而言,使用cache()方法时,会调用persist(MEMORY_ONLY)。

 案例2:多次计算同一个RDD,有cache操作

scala> val list = List("Spark","OOzie","flink","Hive")
list: List[String] = List(Spark, OOzie, flink, Hive)
scala> val rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[0] at parallelize             at <console>:24
scala> rdd.cache()          //rdd.cache会调用persist(MEMORY_ONLY),但是,
                            //语句执行到这里,并不会缓存rdd,这是rdd还没有被计算生成
res0: rdd.type = ParallelCollectionRDD[0] at parallelize at <console>:24
                            
scala> println(rdd.count()) //第一次行动操作rdd.count(),触发一次真正从头到尾的计算,
                            // 这时才会执行上面的rdd.cache(),把这个rdd放到缓存中
[Stage 0:>                                                          (0 + 0) / 1            [Stage 0:>                                                          (0 + 1) / 1                                                                                                       4

scala> println(rdd.collect().mkString(","))
//第二次行动操作,不需要触发从头到尾的计算,只需要重复使用上面缓存的RDD
Spark,OOzie,flink,Hive

案例3:使用unpersist()方法手动地把持久化的RDD从缓存中移除

//创建一个名为rdd1的List对象,包含一些整数元素。
scala> val rdd1 = List(1,2,3,23,34,54,67,78,89) 
rdd1: List[Int] = List(1, 2, 3, 23, 34, 54, 67, 78, 89)
//将List对象rdd1转换为RDD对象rdd2,使用sc.parallelize()方法。
scala> val rdd2 = sc.parallelize(rdd1)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at parallelize at <console>:24
//使用cache()方法将RDD对象rdd2缓存到内存中。
scala> rdd2.cache()
res3: rdd2.type = ParallelCollectionRDD[1] at parallelize at <console>:24
//使用map()方法对RDD中的每个元素进行乘以2的操作,生成新的RDD对象squared_rdd。
scala> val squared_rdd = rdd2.map(elem => elem*2)
squared_rdd: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[2] at map at <console>:23
//使用filter()方法对新的RDD对象squared_rdd进行筛选,保留大于20的元素,生成新的RDD对象filteredRDD。
scala> val filteredRDD = squared_rdd.filter(elem => elem > 20)
filteredRDD: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[3] at filter at <console>:23
//使用collect()方法将筛选后的结果收集起来,并使用mkString()方法
//将结果转换为字符串并打印出来。
scala> println(filteredRDD.collect().mkString(","))
46,68,108,134,156,178
//使用collect()方法将筛选后的结果收集起来,并使用mkString()方法将结果转换为字符串,
//并使用下划线作为分隔符,并打印出来。
scala> println(filteredRDD.collect().mkString("_"))
46_68_108_134_156_178
//使用unpersist()方法释放缓存的RDD对象rdd2。
scala> rdd2.unpersist()
res7: rdd2.type = ParallelCollectionRDD[1] at parallelize at <console>:24
//使用stop()方法关闭SparkContext对象sc。
scala> sc.stop()

分区

RDD分区原则

        RDD是弹性分布式数据集,通常RDD很大,会被分成很多个分区,分别保存在不同的节点上。 RDD分区的一个分区原则是使得分区的个数尽量等于集群中的CPU核心(core)数目。
        对于不同的Spark部署模式而言(本地模式、Standalone模式、YARN模式、Mesos模式),都可以通过设置spark.default.parallelism这个参数的值,来配置默认的分区数目,一般而言:
        1、本地模式:默认为本地机器的CPU数目,若设置了local[N],则默认为N;
        2、Apache Mesos:默认的分区数为8;
        3、Standalone或YARN:在“集群中所有CPU核心数目总和”和“2”二者中取较大值作为默认值;

        因此,对于parallelize而言,如果没有在方法中指定分区数,则默认为spark.default.parallelism,比如:


scala> val seq =Seq(1,2,3,4,5,6,7,88,23)
seq: Seq[Int] = List(1, 2, 3, 4, 5, 6, 7, 88, 23)
//设置三个分区
scala> val rdd = sc.parallelize(seq,3)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize
at <console>:24

textFile的rdd分区原则

        对于textFile而言,如果没有在方法中指定分区数,则默认为min(defaultParallelism,2),其中,defaultParallelism对应的就是spark.default.parallelism。
如果是从HDFS中读取文件,则分区数为文件分片数(比如,128MB/片)。

cpu核数怎么查看

        在Scala中,可以使用Runtime类的availableProcessors方法来获取当前系统的CPU核数。示例代码如下:

val numCores = Runtime.getRuntime.availableProcessors() println(s"当前系统的CPU核数为: $numCores")

打印RDD元素到控制台怎么做

本地模式打印(local)       

工作中,经常需要把RDD中的元素打印输出到屏幕上(标准输出stdout),一般会采用语句:

        1、rdd.foreach(println)

        2、rdd.map(println)。

        当采用本地模式(local)在单机上执行时,这些语句会打印出一个RDD中的所有元素。

//使用collect()方法将所有元素抓取到Driver Program中,并打印出来:
val rdd: RDD[String] = ...
val collected: Array[String] = rdd.collect()

collected.foreach(println)

集群模式打印(IM重要)

        但当采用集群模式执行时,在worker节点上执行打印语句是输出到worker节点的stdout中,而不是输出到任务控制节点Driver Program中,因此,任务控制节点Driver Program中的stdout是不会显示打印语句的这些输出内容的。

       1、 为了能够把所有worker节点上的打印输出信息也显示到Driver Program中,可以使用

        collect()方法,比如,rdd.collect().foreach(println),

        2、但由于collect()方法会把各个worker节点上的所有RDD元素都抓取到Driver Program中,因此,这可能会导致内存溢出。因此,当你只需要打印RDD的部分元素时,可以采用语句        

        rdd.take(100).foreach(println)。

//使用take()方法只获取RDD的部分元素,并打印出来:
val rdd: RDD[String] = ...
val taken: Array[String] = rdd.take(100)
taken.foreach(println)

val rdd: RDD[String] = ...
rdd.foreachPartition(iter => iter.foreach(println))

Scala如何进行遍历List(1,2,3,4,5,6)

scala> val list = List(11,22,33,4,43,2323)
list: List[Int] = List(11, 22, 33, 4, 43, 2323)
// foreach方法
scala> list.foreach(elem => println(list))
List(11, 22, 33, 4, 43, 2323)
List(11, 22, 33, 4, 43, 2323)
List(11, 22, 33, 4, 43, 2323)
List(11, 22, 33, 4, 43, 2323)
List(11, 22, 33, 4, 43, 2323)
List(11, 22, 33, 4, 43, 2323)
//for循环
scala> for (elem <- list)
     | {print(elem + " ")}
11 22 33 4 43 2323

scala> for (elem <- list)
     | {println(elem)}
11
22
33
4
43
2323
//map方法
scala> list.map(elem => println(elem))
11
22
33
4
43
2323
res5: List[Unit] = List((), (), (), (), (), ())

        注意:使用map方法时,它会返回一个新的List,该List包含了对原始List中的每个元素应用函数后的结果。因此,使用map方法来进行遍历时,需要注意返回的List是否需要被使用。如果只是为了遍历而不需要返回结果,使用foreachfor循环更为合适。

(注:部分内容参考林子雨老师课程,林子雨NB!)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值