Spark2x 学习笔记(1)RDD编程

1 概览

  1. 每个spark程序都有一个驱动程序运行在用户的main函数中,以及在集群中执行不同的并行操作。
  2. 第一个抽象概念:RDD是元素的集合。这个集合可以被分到集群中的不同机器中并行处理。
  3. RDD可以由hadoop支持的文件系统中的文件创建,或者是驱动程序中的scala集合。
  4. RDD可以被保存在内存中被并行操作有效服用。
  5. 第二个抽象概念:shared variables。
  6. 共享变量可以在task之间或者task与driver之间共享。
  7. 主要有两种类型,broadcast variables,缓存在所有节点的内存中。accumulators,用于累加场景,例如计算器和求和。

2 连接spark

  1. spark2.4.0可以在python2.7+或者python3.4+版本中运行,可以使用CPython插件,也可使用PyPy 2.3+
  2. python2.6在spark2.2.0不在支持
  3. 可以使用bin/spark-submit或者bin/pyspark在集群中提交spark应用程序。
  4. 如果想使用HDFS文件,需要构建pyspark和HDFS之间的连接。
  5. 需要在应用程序中引入spark类
from pyspark import SparkContext, SparkConf
  1. 在提交应用时,可以指定Python的版本
$ PYSPARK_PYTHON=python3.4 bin/pyspark
$ PYSPARK_PYTHON=/opt/pypy-2.5/bin/pypy bin/spark-submit examples/src/main/python/pi.py

3 初始化spark

  1. spark编程第一件事是创建SparkContext对象,用于告诉spark如何访问集群。
  2. 创建SparkContext对象前,需要创建SparkConf对象,该对象包含应用信息。
conf = SparkConf().setAppName(appName).setMaster(master)
sc = SparkContext(conf=conf)
  1. appName:在集群UI中显示的应用名称
  2. master:Spark/Mesos/YARN集群的URL或者local字符串
  3. 一般不会硬编码master参数,而是使用spark-submit方式提交应用。

4 使用Shell

  1. pyspark shell中,已经创建了一个特殊的SparkContext,变量名称为sc,无法再创建自己的SparkContext。
  2. –master:指定context的连接模式
$ ./bin/pyspark --master local[4]
  1. –py-files:增加python .zip .egg .py文件
$ ./bin/pyspark --master local[4] --py-files code.py
  1. 使用ipython
$ PYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark
  1. 使用Jupyter notbook
$ PYSPARK_DRIVER_PYTHON=jupyter PYSPARK_DRIVER_PYTHON_OPTS=notebook ./bin/pyspark

5 RDDs

5.1 Parallelized Collections

  1. 创建方式
data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)
  1. distData是一个分布式数据集,可以被并行处理
distData.reduce(lambda a, b: a + b)
  1. 集群中的,每个节点都会去处理RDDs的一个分片,一般每CPU处理2-4个分片,Spark会自动划分,也可以手动配置每CPU处理的分片数量
sc.parallelize(data, 10)

5.2 外部Datasets

  1. Spark可以从任何Hadoop支持的外部存储源创建RDD
  2. 可以使用SparkContext’s textFile方法读取外部文件,该方法的参数为文件的URL
>>> distFile = sc.textFile("data.txt")
  1. distData是一个RDD,可以执行并行计算操作
distFile.map(lambda s: len(s)).reduce(lambda a, b: a + b)
  1. spark读取文件注意点:
  • 如果使用本地文件,需要将本地文件放到所worker能访问的地方。例如将文件拷贝到所有worker节点,或者使用共享存储。
  • 所有Spark基于文件输入的方法,包括textFile,支持运行在目录、压缩文件、通配符。
textFile("/my/directory"), textFile("/my/directory/*.txt"), ("/my/directory/*.gz")
  • textFile方法也可以设置第二个参数来制定文件的分区,默认会为每个HDFS block创建1个分片,可以通过手工配置创建更多的分片,但是不能比HDFS block数量少。
  1. 其他从file读取文件方法
  • SparkContext.wholeTextFiles: 用于从目录中获取很多小文件,然后返回(filename,content)对。
  • RDD.saveAsPickleFile/SparkContext.pickleFile:使用python的pickle序列化将RDD以该格式存储。

5.2.1写文件:pyspark负责做python和Java类型转换

Writable Type Python Type
Text unicode str
IntWritable int
FloatWritable float
DoubleWritable float
BooleanWritable bool
BytesWritable bytearray
NullWritable None
MapWritable dict

array类型需自定义类型转换器,读取时,默认的转换器会将用户自定义的ArrayWritable转换为Java的Object[ ],序列化为python元组。

5.2.2 存储或加载SequenceFiles

>>> rdd = sc.parallelize(range(1, 4)).map(lambda x: (x, "a" * x))
>>> rdd.saveAsSequenceFile("path/to/file")
>>> sorted(sc.sequenceFile("path/to/file").collect())
[(1, u'a'), (2, u'aa'), (3, u'aaa')]

5.2.3 存储或加载其他Hadoop I/O 格式

$ ./bin/pyspark --jars /path/to/elasticsearch-hadoop.jar
>>> conf = {"es.resource" : "index/type"}  # assume Elasticsearch is running on localhost defaults
>>> rdd = sc.newAPIHadoopRDD("org.elasticsearch.hadoop.mr.EsInputFormat",
                             "org.apache.hadoop.io.NullWritable",
                             "org.elasticsearch.hadoop.mr.LinkedMapWritable",
                             conf=conf)
>>> rdd.first()  # the result is a MapWritable that is converted to a Python dict
(u'Elasticsearch ID',
 {u'field1': True,
  u'field2': u'Some Text',
  u'field3': 12345})

如果使用自定义序列化数据,例如从HBase加载数据,需要先在Java或者Scala侧将数据转换为 Pyrolite’的pickler可以处理的东西。

6 RDD操作

  1. RDD支持两类操作。transformations:从一个已经存在的dataset创建一个新的dataSet。actions:在dataset上进行运算后,返回一个值给驱动程序
  2. transformations操作不会立刻计算结果,只有当action操作需要返回给驱动程序一个计算结果的时候才会执行计算。
  3. 默认当执行action操作时,所有的transformations RDD会被重复计算。可以使用persist或者cache将RDD放在内存中。

6.1 基础使用

统计文件中字符数量

lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)
lineLengths.persist()

6.2 向Spark传递函数

  1. lambda 表达式
  2. 定义本地函数,将函数作为参数传入spark
  3. 在其他模块中定义函数

函数式编程

"""MyScript.py"""
if __name__ == "__main__":
    def myFunc(s):
        words = s.split(" ")
        return len(words)

    sc = SparkContext(...)
    sc.textFile("file.txt").map(myFunc)

面向对象编程

class MyClass(object):
    def func(self, s):
        return s
    def doStuff(self, rdd):
        return rdd.map(self.func)

如果将变量定义在了对象之外,如下所示,会将整个对象发送给集群

class MyClass(object):
    def __init__(self):
        self.field = "Hello"
    def doStuff(self, rdd):
        return rdd.map(lambda s: self.field + s)

应该将传入spark的数值定义为本地变量,则只会讲变量的值发送给集群

def doStuff(self, rdd):
    field = self.field
    return rdd.map(lambda s: field + s)

6.3 理解闭包

理解变量和方法被传入到集群的里程碑和生命周期

counter = 0
rdd = sc.parallelize(data)

# Wrong: Don't do this!!
def increment_counter(x):
    global counter
    counter += x
rdd.foreach(increment_counter)

print("Counter value: ", counter)
  1. 在集群模式下,counter在driver节点的内存中被序列化,然后发送到各工作节点上,执行spark程序时,工作节点的counter值会变化,但是不会传回到driver,最后driver中的值还是0
  2. 在单节点模式下,如果程序在同一个JVM中,counter会累加。
  3. 两种模式下表现不一致,如果有类似全局累加器的场景,需要用到Accumulator
  4. 想要查看RDD中的元素,应该使用rdd.collect().foreach(println)。:从集群中获取全量数据,take()从集群中获取制定数量数据rdd.take(100).foreach(println).

6.4 使用键值对

统计内同相同行的数量

lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.reduceByKey(lambda a, b: a + b)

按字符顺序排序

lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.sortByKey()

6.5 Transformations算子

Transformations 含义
map(func) 将源RDD的每个元素通过func进行计算,返回一个新的RDD
filter(func) 将源RDD中的每个元素传入func,将func计算返回true的元素作为新的RDD
flatMap(func) 和Map类似,但是每个输入元素map处理后,会有0个或者多个输出,func函数的返回值是一个序列
mapPartitions(func) 类似于map,但是以RDD的分片(block)为单位进行处理的,例如,处理元素为T的RDD时,func的输入输出形式为 Iterator => Iterator
mapPartitionsWithIndex(func) 和mapPartitions类似,但是可以将一个整型值作为源RDD分片的索引,例如处理元素为T的RDD时,func的输入输出形式为 (Int, Iterator) => Iterator
sample(withReplacement, fraction, seed) 使用给定的随机数发生器种子,以可选的替换值对数据的一小部分进行采样。
union(otherDataset) 返回一个新的数据集,该数据集包含源RDD和参数中RDD元素的并集。
intersection(otherDataset) 返回包含源RDD和参数中元素的交集的新RDD。
distinct([numPartitions])) 返回一个包含源数据集的不同元素的新数据集。
groupByKey([numPartitions]) 在(K , V)RDD上调用时,返回(K , Iterable< V >)对的数据集。注意:如果要在每个键上执行聚合(如求和或平均值),则使用reduceByKey或AggregateByKey将获得更好的性能。注意:默认情况下,输出的并行度取决于父RDD的分区数量。可以通过一个可选的numPartitions参数来设置不同数量的任务。
reduceByKey(func, [numPartitions]) 在(K,V)RDD对上调用时,返回(K,V)对的数据集,使用给定的减少函数func聚合每个密钥的值,该函数必须是类型(V,V)= > V,通过可选的第二个参数可配置减少任务的个数。
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions]) 举例
sortByKey([ascending], [numPartitions]) 在一个Key可排序的的(K,V)RDD上调用时,根据传入的ascending参数(true为升序,false为降序),按K对源RDD中的参数进行排序,然后新的RDD
join(otherDataset, [numPartitions]) 在(K,V)和(K,W)RDD上调用时,对每个key做聚合,返回(K,(V,W))的RDD,还可以执行eftOuterJoin, rightOuterJoin, and fullOuterJoin三种join
cogroup(otherDataset, [numPartitions]) 在(K,V)和(K,W)RDD上调用时,返回一个元素为(K, (Iterable, Iterable)) 的元组的RDD
cartesian(otherDataset) 在类型为T和U的RDD上调用时,返回元素类型为(T,U)元组的RDD
pipe(command, [envVars]) 通过系统的stdin和和stdout源RDD和结果RDD输入和从目的RDD输出(以字符串的形式)可以传入bash或者perl命令
coalesce(numPartitions) 将RDD的分区减少到制定数目,通常在使用filter将一个大的数据集精简之后使用
repartition(numPartitions) 按指定的数量分区重新分区,可以增加也可以减少,通常所有数据都需要重新shuffle,占用较多带宽
repartitionAndSortWithinPartitions(partitioner) 重新分区后,再在各分区内对各元素按键排序,由于在shuffle的时候就已经排序了,比repartition效率更高

6.6 Actions算子

Action 意义
reduce(func) 使用func将源RDD中的元素聚合
collect() 将RDD的所有元素都返回给驱动程序,通常用在filter或者别的返回比较小的子数据集的场景
count() 返回RDD元素的个数
first() 返回RDD的第一个元素
take(n) 返回RDD的前n个元素
takeSample(withReplacement, num, [seed]) 按随机采样的方式返回n个元素,可以设置替换值和随机数种子
saveAsTextFile(path) 将RDD保存为文件,存在本地或者其他Hadoop支持的文件系统中
saveAsSequenceFile(path) (Java and Scala) 将数据集的元素作为Hadoop SequenceFile写入本地文件系统,HDFS或任何其他Hadoop支持的文件系统中的给定路径中。 这可以在实现Hadoop的Writable接口的键值对的RDD上使用。 在Scala中,它也可以在可隐式转换为Writable的类型上使用(Spark包括基本类型的转换,如Int,Double,String等)。
saveAsObjectFile(path) (Java and Scala) 使用Java 序列化进行格式化,可以使用SparkContext.objectFile()来加载
countByKey() 在类型为(K,V)的RDD上调用,返回一个元素类型为(K,int)的hashmap,计算每个K的个数
foreach(func) 在数据集的每个元素上运行函数func。 不适用于如更新累加器或与外部存储系统交互的场景。

RDD API中也支持部分异步场景,例如foreachAsync,不会造成进程堵塞

6.7 shuffle 操作

  1. 某些操作包括shuffle过程,shuffle过程是一种数据重分布机制,可以将RDD中的数据在不同分区中重新分组。
  2. shuffle通常会将数据在执行器和节点之间进行拷贝,比较复杂且开销很大

6.7.1 背景

  1. 以reduceByKey为例说明shuffle:
    reduceByKey将源RDD(K,V)中的所有元素按Key进行分组,然后在组内执行func函数,将所有value合并,最后输出一个(K,V)RDD。由于源RDD中具有相同K的数据不一定在同一个分区上,需要进行shuffle操作,将分散的数据进行排序、重分布后在计算输出。
  2. shuffle的定义:spark在集群内执行一个all-to-all的操作,先读取到所有K的下各自的全部V,然后按K对每个K下的所有V的做计算操作。
  3. 由于RDD下的所有数据都拿到手了,因此会对RDD的数据做一次排序。
  4. 重分区: repartition、coalesce;byKey:groupByKey 、reduceByKey;join: cogroup 、join通常都会引起shuffle

6.7.2 性能

  1. shuffle会大量调用磁盘I/O,数据序列化,网络I/O。
  2. spark会生成一系列任务,map任务组织数据,reduce任务聚合数据。
  3. 来自单个map任务的结果数据会被包租在内存里面,在数据稳定后,他们会按目标分区进行排序,存储在单个文件里。reduce任务会读取已经排好序的块文件。
  4. shuffle会在磁盘上生成大量中间文件。通常由垃圾收集器在一定周期内自动清除。可以通过spark.local.dir指定零时文件的存储位置。
  5. shuffle性能调优参数

6.8 RDD persistence

  1. RDD persistence可以将RDD的数据在内存中持久化,从而做到数据复用,提升性能
  2. 使用persist()或者cache()函数标记RDD
  3. 在第一次action操作后,RDD会被缓存在内存中,这种缓存具有容错性,一旦掉电,内存中数据丢失,spark会自动按之前的transformations重新计算一遍。
  4. persist有不同的存储等级,persist在内存中、persist在磁盘中、创建序列化对象、创建两副本。
Storage Level 含义
MEMORY_ONLY 将RDD以反序列化Java对象存储在JVM中。如果RDD不适合放在内存中,一些分区将不会被缓存,每次需要用的时候会被重新计算,为默认级别
MEMORY_AND_DISK 将RDD以反序列化Java对象存储在JVM中,如果RDD不适合放在内存中,将这些RDD的分区放在磁盘中,当需要被用到的时候会被读取
MEMORY_ONLY_SER (Java and Scala) 将RDD以序列化Java对象的方式存储在内存中,每个分区一个byte数组。比非序列化的存储方式空间利用率更高,但是会消耗更多CPU
MEMORY_AND_DISK_SER (Java and Scala) 和MEMORY_ONLY_SER类似,但是会将不适合放在内存中的分区存储在硬盘
DISK_ONLY 将RDD分区只存储在硬盘上
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc 和上面的级别一致,但是会缓存在集群中的两个节点
OFF_HEAP (experimental) 类似MEMORY_ONLY_SER,但是是将数据存储在堆外内存

python会始终使用pickle库对RDD的分区进行序列化,所以不存选择是否序列化,只有如下级别MEMORY_ONLY, MEMORY_ONLY_2, MEMORY_AND_DISK, MEMORY_AND_DISK_2, DISK_ONLY, and DISK_ONLY_2
spark也会自动persist某些中间数据,用于避免节点shuffle失败后的重复计算,但是如果数据确实可能复用,仍然建议手工申明presist。

6.8.1 如何选择存储等级

  1. 如果数据适合存储在内存中,使用默认等级。
  2. 如果不是,使MEMORY_ONLY_SER,并使用效率最高的序列化库
  3. 除非类似filter一个很到数据量的数据集,尽量不要使用存储在磁盘中的模式
  4. 所有级别都有容错功能,但是副本模式可以让你不用重新计算,在这种场景下使用服务级别

6.8.2 删除数据

spark基于最近至少使用过的原则自动删除缓存数据,如果想要手动清除,使用RDD.unpersist()方法。

7 共享变量

  1. 执行spark操作时,spark会将驱动程序中涉及的变量拷贝到集群中的所有节点,但是当计算结束后,这些变量的值不会回传到驱动程序。
  2. spark基于两种常见场景提供共享变量:broadcast variables和accumulators

7.1 Broadcast Variables

  1. 广播变量会被缓存在所有节点的内存中,而不是在执行task的时候在去拷贝数据。
  2. spark会有一套高效的广播算法
  3. spark操作是由一系列的stage组成,stage被shuffle操作分开。数据在stage中的task中被使用。
  4. 广播变量会以序列化的格式缓存在各节点的内存中,然后在执行task前反序列化。
  5. 因此,广播变量的适用场景为:跨stage执行task的时候,需要用的相同的数据(广播变量),或者

你可能感兴趣的:(大数据)