总览

第一、每个spark 应用都有一个驱动程序去运行着主函数和再每个节点上的并行操作。
spark提供了一个RDD(弹性分布式数据集)的数据集合,可以通过不同的节点并行操作运算,可以通过hdfs文件构建。RDD可以在内存中进行缓存,当需要复用的时候会有更高的效率。

第二、提供了共享变量(shared varibales)在不同节点的并行操作中使用。一个是广播变量(broadcast variables)一个是累加器(accumulators)。当一个变量需要在不同节点任务重共享或者节点任务跟驱动程序见有变量共享需求时,可以用这俩工具。

连接spark

from pyspark import SparkContext, SparkConf
  • SparkContext: 用来连接集群节点的一个接口
  • SparkConf:
  1. spark应用的配置,将各种spark参数设置为key-value对。程序启动时会从系统中自动读取各种配置参数,若SparkConf有配置则以SparkConf优先。
  2. 设置SparkConf(false),可以避免加在外部系统设置,完全以SparkConf为准。
  3. 所有的配置设置支持链式写法,比如:

SparkConf().setMaster(“local”).setAppName(“My app”)

初始化spark

from pyspark import SparkContext, SparkConf

conf = SparkConf().setAppName('programm_guide')
sc = SparkContext(conf=conf)
print ('the sc is: {}'.format(sc))

使用shell

将spark bin 目录加入环境变量,直接运行

pyspark

RDD

Parallelized Collections

data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)

使用 sc.parallelize 接口可以从一个迭代器或者集合中创建RDD。迭代器或者集合中的数据会被copy并创建为可以并行计算的弹性分布式数据集,即RDD。

通常RDD数据会被分区,可以指定参数说明数据要被划分为多少个分区。比如:

sc.parallelize(data, 10))

外部数据集

distFile = sc.textFile("data.txt")

spark支持从不限于以下途径读取数据创建RDD:

  1. HDFS
  2. Cassandra
  3. HBase
  4. Amazon S3
  5. local

spark支持直接读取 text files, SequenceFiles以及其他hadoop输入格式。

使用spark读取文件需要注意以下几点:

  1. 若读取路径为本地路径,需要保证节点上的相同路径必须是存在且可访问的。
  2. 所有spark读取文件操作比如sc.textFile 全都支持对文件夹、压缩文件及通配符文件名的支持。比如:
rdd = sc.textFile("/my/directory")
rdd = sc.textFile("/my/directory/*.txt")
rdd = sc.textFile("/my/directory/*.gz")

其他:

  1. SparkContext.wholeTextFiles 支持从一个文件夹中读取很多小文件,按照(filename, content)的pairs返回RDD。相反,sc.textFile的方法读取文件夹会返回一个RDD, 每个文件的每一行都是一条 RDD record。。
  2. RDD.saveAsPickleFileSparkContext.pickleFile 用于将RDD序列化为python对象的格式。
  3. 保存和加载 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. 保存和加载其他hadoop 输入输出格式的数据:...

RDD操作

RDD支持两类操作:

1. transformations

所有的transformations都是惰性(lazy)执行的,比如map。简单来说就是所有的transformations都不是立即执行的,程序知会记下来它对data都进行了哪些transformations以及相应的执行顺序,只有当他碰到一个action的时候才会真正进行计算并返回结果给驱动程序。lazy执行的设计可以让spark程序的运行更有效率。

每个transform后的的RDD都可以通过rdd.persist() 或者 rdd.cache() 的方法进行缓存再利用。

2. actions
跟transformation相反,立即执行的。

基础

lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
lineLengths.persist()

totalLength = lineLengths.reduce(lambda a, b: a + b)

读取数据,maop 并 持久化,reduce 操作。

将函数传递给spark

三种方式:

  • lambda 表达式
rdd.map(lambda s: len(s))
  • def 定义的函数
def myFunc(s):
    words = s.split(" ")
    return len(words)

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

由于节点访问外部对象的属性将会导致引用整个对象,最好在函数内通过复制一份局部变量来进行操作。

class MyClass(object):
    def __init__(self):
        self.field = "Hello"
    def doStuff(self, rdd):
        field = self.field
        return rdd.map(lambda s: field + s)
  • spark 本身封装的一些高级模块函数

理解闭包

理解spark的难点之一是跨节点执行代码时准确理解variables和methods的适用范围与生命周期。修改超出范围的变量的RDD操作可能会引起混乱。下面通过foreach()的案例来看一下。

案例
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)

这段代码用于计算rdd中record的条目数。注意这种写法是错的!

本地模式 vs 节点模式

上面的写法并不对。spark执行程序会把rdd操作分解为任务,每个任务由执行程序运行。执行任务前,spark会计算任务的closure,也就是执行程序进行计算时所必须可见的那些variables 和 methods,并发送给每个executor。

在上面代码中,counter变量已经copy并发送给每个节点的执行程序中,相当于不同节点执行程序的局部变量,每个节点都是对局部变量进行更改。最终驱动程序的counter仍然为0。

当local模式运行程序,某些时候foreach函数将在与驱动程序相同的JVM中执行,并引用全局的counter,这时候程序会有效。但这种写法仍然不是推荐的写法。

为解决上述场景中的问题,spark提供了累加器(Accumulator),后面会有详细用法介绍。

打印RDD元素

rdd.collect()获得rdd中的value,然后再进行打印。但这不是一个好的方式。因为collect操作会把rdd中所有数据汇总到一台机器上面,当rdd中的数据很多时会out of memory。

rdd.foreach(println)的方式也不行,标准输出会输出到不同节点上去。

正确的做法应该是rdd.take(100).foreach(println)

处理 key-value 数据对

大部分RDD的操作都支持任意类型的数据,但有些操作只支持key-value类型的数据。最常见的是分布式 shuffle 操作,比如group 和 aggregating。

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

transformations

详见:RDD接口文档

spark编译源码官网 spark编程指南_spark

actions

详见:RDD接口文档

spark编译源码官网 spark编程指南_驱动程序_02

Spark RDD API 支持异步操作,比如针对foreachforeachAsync

shuffle 操作

shuffle 是重新分配数据的机制,以便跨分区对数据进行不同分组。涉及跨执行程序和机器复制数据,是复杂且昂贵的操作。

背景

以reduceByKey操作为例。

单个key的所有value可能不在同一个分区甚至同一台机器上,但又必须把同个key的value一起计算出结果。

spark在计算期间,单个task在单个分区上操作,那为了组织同个key的所有的value,spark必须读取所有分区的所有数据将相同key的value汇总在一起,得出最终结果。这就是shuffle。

会导致shuffle的操作包括repartition 操作比如 repartition 和 coalesce;ByKey操作比如groupByKey 和 reduceByKey,join操作比如 cogroup 和 join。

性能影响

shuffle涉及磁盘IO,数据序列化,网络IO,比较昂贵的操作。

reduceByKey 和 aggregateByKey操作会格外消耗栈内存,这些操作涉及到的数据转存的操作都是要内存数据结构来组织。当数据不适合内存操作时会产生额外的磁盘IO的开销以及垃圾回收。

shuffle会产生大量中间文件,spark会暂时保留这些文件知道不再使用相应的RDD了才会进行垃圾回收。如果程序保留的对相应RDD的引用或者垃圾回收不经常启动,那么比较长时间的spark任务会占用大量的磁盘空间。

RDD 持久化

RDD需要在程序中重复使用时使用RDD的持久化技术可以加速程序运行。

rdd.persist()
rdd.cache()

持久化有不同的storage level可以选择

其中 MEMORY_ONLY 是 rdd.cache() 的default选项。

storage level 的选择

storage level 的选择就是在内存使用和cpu效率间的取舍。建议按照以下几个原则进行选择:

  1. 默认的MEMORY_ONLY是CPU高效率的选择,如果程序运行良好,尽量不要改。
  2. 若程序运行性能不佳,尝试 MEMORY_ONLY_SER(仅适用于JAVA/SCALA)
  3. 如果要快速修复故障,使用基于复制的storage level。在程序运行期间会有较好的容错性,丢失的数据会自动从备份中找回来继续计算。

remove data

spark会监控每个节点的内存使用情况,并根据LRU(least-recently-used)原则进行垃圾回收。也可以通过RDD.unpersist()的方式手动remove一个RDD。

共享变量

广播变量

broadcastVar = sc.broadcast([1, 2, 3])
broadcastVar.value

sc.broadcast 创立的共享变量可以传递给每个节点调用。他一旦传递出去后,不应该再被驱动程序更改。每个节点的执行程序都获得一样的value。

累加器(Accumulators)

两种方式

  1. sc.accumulator
accum = sc.accumulator(0)
sc.parallelize([1, 2, 3, 4]).foreach(lambda x: accum.add(x))
accum.value

sc.accumulator(0) 给 accum 一个初始化的值,accum 可以被所有节点使用add方法进行操作,但不能读取其中的value。只有驱动程序才能通过 accm.value 读取其value。

  1. 继承 AccumulatorParam 类,这玩意儿具体用法还有待研究。
class VectorAccumulatorParam(AccumulatorParam):
    def zero(self, initialValue):
        return Vector.zeros(initialValue.size)

    def addInPlace(self, v1, v2):
        v1 += v2
        return v1

# Then, create an Accumulator of this type:
vecAccum = sc.accumulator(Vector(...), VectorAccumulatorParam())

部署

详见:官方文档