两万字笔记快速掌握Spark大数据处理平台
* 版权声明 * : 引用请注明出处,转载请联系: h0.1c@foxmail.com
文章目录
1 简介
Spark是Hadoop MapReduce的继任者,用于分布式计算。其中最核心的接口是RDD,用于存储数据,其他的功能如Spark SQL数据查询、Spark Streaming流式数据、 MLlib机器学习、GraphX图计算都基于RDD。
Spark可以以shell或者独立应用方式运行。Shell程序本身和独立应用(通过spark-submit脚本提交)会启用driver program,后者创建的的SparkContext对象可以访问Spark集群、创建和操作RDD。
1.1 Spark集群
Spark集群包括驱动器节点(运行driver program进程的节点)和执行器(executor)节点(运行executor program进程的节点)。当shell或者独立程序的main()函数运行,驱动器启动并通过集群管理器(可选自带/YARN/Mesos)访问执行器,将用户指令转化为任务、将任务分给执行器进程、调度这些任务并进行异常处理等工作;执行器在这期间则执行任务、保存结果;直到驱动器退出。
注:主(master)节点代表集群管理器所在的中心化节点,工作(worker)节点代表非中心化的部分,一般情况下,工作节点上运行执行器进程。但主节点并不是驱动器节点,工作节点也不一定是执行器节点。
1.2 RDD
RDD是Spark的抽象结构,用于存放数据。Spark提供了关于RDD的输入输出(从文件或驱动器程序的内存中创建或写出到文件)、RDD的转化(transformation)操作(将RDD变为另一个RDD)、RDD的行动(action)操作(将RDD变为值)。这些操作的一个重要特征就是惰性(lazy execution),即行动(action)操作产生前,不会进行任何实际行动,除非使用了缓存操作persist()。
1.3 Spark高级功能
Spark SQL使用Hive的数据查询语言HQL查询RDD数据,并在应用中使用查询和其他分析手段;
Spark streaming使用与RDD相同的操作接口处理流式数据;
MLlib是基于Spark的机器学习库,提供了操作数据和使用机器学习模型的方法,包括选取feature、转化数据、机器学习模型(estimator)、evaluator(根据给定参数和模型给出评分)、validator(通过evaluator给出的评分决定一系列参数和模型中最优者) 和pipeline模型(将上述操作串联成流水线作业)等多个模块。
2 开始运行独立应用
独立的Spark应用一般需要进行如下步骤来启动一个驱动器(以python版本的pyspark为例):
from pyspark import SparkConf, SparkContext # import Spark library
conf = SparkConf().set("PROPERTY_NAME","PROPERTY_VALUE") # configure SparkConf instance
sc = SparkContext(conf=conf) # create SparkContext instance using the SparkConf instance
注:set(“PROPERTY_NAME”,“PROPERTY_VALUE”)是配置参数的伪代码,详见2.1部分。
2.1 应用配置:方法
配置Spark程序有3种方式:在应用程序中使用set()或set类型函数显式配置、在spark-submit时携带参数、在Spark配置文件中设置;这三种方式的优先级依次降低。对于没有配置的值使用默认值。本质上,这些操作都修改了SparkConf对象中的配置键值对。所有的配置都可以登录http://[driver_IP]:4040/environment/查看。
(1)使用set()函数更改配置直接按照set(“PROPERTY_NAME”,“PROPERTY_VALUE”)形式填入参数,例如:
conf = SparkConf()
conf.set("spark.app.name","mySparkApp")
conf.set("spark.master","local[*]")
conf.set("spark.ui.port","12345")
还可以使用set类型函数配置常用的参数,例如setAppName、setMaster,上述代码等价为:
conf = SparkConf().setAppName("mySparkApp").setMaster("local[*]").set("spark.ui.port","12345")
#函数用法上等价
#写法上连续设置与上面的分开设置也等价
(2)在spark-submit时携带参数,例如(与上方代码等价):
spark-submit --master local[*] --name "mySparkApp" --conf spark.ui.port=12345 app_script.py
(3)或者spark-submit时携带配置文件,例如:
spark-submit --properties-file config.conf app_script.py
在config.conf文件中,则应该包含如下内容:
spark.app.name "mySparkApp"
spark.master local[*]
spark.ui.port 12345
2.2 应用配置:内容
上面说到,三种方法的本质搜是修改了SparkConf对象中的配置键值对,而这些配置键值对可以在文档:https://spark.apache.org/docs/latest/configuration.html中查询。此处列举几条常用的配置:
Property Name | Meaning | Property Value |
---|---|---|
spark.master (命令行中为--master) | 设置Spark集群的集群管理器(主节点) | spark://host:port为Spark集群,mesos://host:port为Mesos集群,yarn为hadoop集群,local[N]表示在本地用N个核心运行,local[*]表示在本地用尽量多的核心运行 |
spark.submit.deployMode(命令行中为--deploy-mode) | 在何处执行Spark驱动器进程 | "client"本地,"cluster"集群中的一个工作节点 |
spark.driver.cores和spark.executor.cores | 驱动器进程/每个执行器进程的核心数 | 数字代表核心数量* |
spark.driver.memory和spark.executor.memory (命令行中为--driver-memory和--executor-memory) | 驱动器进程/每个执行器进程的内存 | 数字+单位, in the same format as JVM memory strings with a size unit suffix (“k”, “m”, “g” or “t”),默认为512m |
spark.[x].port ([x]为ui,driver,executor等) | 设置x的端口值 | 数字代表端口号 |
spark.serializer | 序列化器** | 默认为Java的序列化器org.apache.spark.serializer.JavaSerializer,可以使用更快的org.apache.spark.serializer.KryoSerializer并调优*** |
* 不同的集群管理器有相应规则,需参考文档
** 序列化器是执行序列化(即“将对象转化为二进制数据”)的程序,反序列化器(deserializer)则是将二进制数据恢复为对象
*** 序列化器相关内容见https://spark.apache.org/docs/latest/tuning.html
2.3 SparkContext
在SparkConf被动态配置完成的情况下,可以根据SparkConf的实例创建出SparkContext对象的实例。SparkContext是后续处理RDD的基础,其中提供了众多有关RDD的操作方法,包括一系列输入输出、转化和行动操作,这些将在第3部分中阐述;除此之外,SparkContext还提供了包括累加器、广播变量等共享变量(在2.4中阐述)。
参考:https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.SparkContext
2.4 共享变量
(1)第一种共享变量是累加器程序中声明的普通变量会在每一个执行器上产生副本,这些副本独立变化,如果需要进行全局计数,则需要使用SparkContext的累加器对象:
ac = sc.accumulator(INIT_VAL) # INIT_VAL indicates the initial value of accumulator
在函数中使用的时候需要引入这个全局变量(因为是左值,必须使用global关键字):
def function():
global ac
ac += 1
这样,当任意执行器执行function的时候,累加器c的值就会增加1。
累加器变量在驱动器中可读写(只有在行动操作发生后读取的值才正确,因为惰性执行,使用.value读取),而对执行器来说是只写而不可读的。
转化操作中的累加器并不保证计数的准确,因为存在失败任务重新运行的情况,这样会产生重复计数;但行动操作可以保证计数正确。
另外,只要满足交换律和结合律,累加器可以使用任意的运算符。
(2)第二种共享变量是广播变量。广播变量适用于存储一个只读值,例如大尺寸的只读查询表,这个只读表可以被执行器读取,例如:
bc = sc.broadcast(readOnlyData)
其中,任何可以被spark.serializer指定的序列化器序列化的数据类型都可以用于初始化广播变量。同样使用.value读取值。
3 RDD: Spark的抽象结构
RDD全名弹性分布式数据集Resilient Distributed Dataset。程序运行的时候,驱动器根据用户指令将RDD操作转变为有向无环图(DAG),该图之间的连接代表RDD之间的依赖关系(谱系,lineage);驱动器只是创建DAG而并不执行(lazy execution),直到有action触发立即执行,才会将这些任务分配给执行器进程执行。
RDD的API参考文档(python):https://spark.apache.org/docs/latest/api/python/pyspark.html?highlight=combinebykey#pyspark.RDD
3.1 RDD创建与保存
RDD的创建是一种转化(transformation)操作,会惰性执行,RDD的保存(指保存为文件)则是行动(action)操作,会触发立即执行。
3.1.1 示例
RDD只有两种被创建的方式:通过外部文件和驱动器内存中的对象。
驱动器内存中的对象,当可以被spark.serializer指定的序列化器序列化时,都可以转化为RDD;文件读取也支持多种文件类型(如文本文件、SequenceFile等)和多种文件系统(S3、HDFS等)。
rdd1 = sc.parallelize(["val1","val2","val3"]) # serialize a dictionary
rdd2 = sc.textFile("val.txt") # read from a text file
Spark程序一般会输出最终结果(行动操作的结果)至标准输出或者保存到文件系统。
rdd1.collect() # print all content of rdd1
rdd2.saveAsTextFile("res.txt") # save rdd2 to a output text file
3.1.2 SparkContext文件接口
SparkContext支持的文件类型和相应的读写接口如下表所示(JSON、CSV、Parquet等格式在SparkSQL中有更好的读写接口,此处不表):
类型 | 接口-读 | 接口-写 |
---|---|---|
Text File | 读取单个文件rdd=sc.textFile(FILEPATH),读取文件夹内所有文件rdd=sc.wholeTextFiles(DIR_PATH) | rdd.saveAsTextFile(FILEPATH) |
SequenceFile | rdd=sc.sequenceFile(FILEPATH, *classString),其中classString代表sequenceFile支持的数据类型列表* | rdd.saveAsSequenceFile(FILEPATH) |
* Hadoop writable类型(例如IntWritable、FloatWritable等)见https://hadoop.apache.org/docs/r3.0.1/api/org/apache/hadoop/io/Writable.html
**Hadoop格式、Pickle格式、对象文件效率低或不支持python,此处不表,详见https://spark.apache.org/docs/2.1.0/api/python/pyspark.html#pyspark.SparkContext
SparkContext支持的文件系统如下:
文件系统 | 地址字符串 |
---|---|
本地 | “file:///path/to/file” |
HDFS | “hdfs://master:port/path/to/file” |
Amazon S3 | “s3n://bucket/path/to/file” (需要设置访问密钥) |
3.1.3 Spark SQL文件接口
Spark SQL提供了一系列更加方便的文件读写接口,区别是读取进来的数据成为了DataFrame(RDD+Schema),这是一种特殊的RDD,可以进行RDD的通用操作,也可以方便地与Spark的其他高级功能协作(具体在第4部分阐述)。
类型 | 例程-读 | 例程-写 |
---|---|---|
通用Generic | spark.read.format(FORMAT).load(FILEPATH),其中FORMAT可以为“json”、“csv”、“parquet”等 | spark.write.format(FORMAT).mode(MODE).save(FILEPATH),其中MODE可以为“append”、“overwrite”等 |
JSON | spark.read.json(FILEPATH) | spark.write.json(FILEPATH) |
Parquet | spark.read.parquet(FILEPATH) | spark.write.parquet(FILEPATH) |
CSV | spark.read.csv(FILEPATH, sep=":", inferSchema=“true”, header=“true”),其中sep为分割符,header为是否有表头,inferSchema为推断数据类型 | spark.write.csv(FILEPATH, sep=":", header=“true”) |
Text | spark.read.text(FILEPATH, lineSep=’\n’),其中lineSep为换行符 | text(FILEPATH, lineSep=None) |
注:更多信息可以参阅文档:
Spark SQL 数据源 https://spark.apache.org/docs/latest/sql-data-sources.html
Spark DataFrame Reader API https://spark.apache.org/docs/latest/api/python/pyspark.sql.html#pyspark.sql.DataFrameReader
Spark DataFrame Writer API https://spark.apache.org/docs/latest/api/python/pyspark.sql.html#pyspark.sql.DataFrameWriter
3.1.4 Spark和Spark SQL的数据库接口
Spark支持与JDBC、HBase、Cassandra、ElasticSearch等数据库连接。
Spark SQL支持与Hive、JDBC等数据库连接。
此处暂不表。
3.2 RDD Transformation
本节参考RDD编程文档中有关RDD转化的内容 https://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations
3.2.1 通用RDD转化:速查表
(1)单个RDD
Transformation | Meaning | Example | 效果逻辑等价*代码 |
---|---|---|---|
map(func) | Return a new distributed dataset formed by passing each element of the source through a function func. | rdd.map(lambda x:x+1) 返回新的rdd,元素值为源rdd中各元素+1,例如[1,2,3]->[2,3,4] | newRDD=oldRDD.map(f(x)) ⇒ for elem in oldRDD {newRDD.insert(f(elem))} return newRDD |
filter(func) | Return a new dataset formed by selecting those elements of the source on which func returns true. | rdd.filter(lambda x:x>0) 返回新的rdd,其中只保留了符合判断条件x>0(使函数为真)的元素,例如 [-1,0,1]->[1] | newRDD=oldRDD.filter(f(x)) ⇒ for elem in oldRDD {if f(elem) == True {newRDD.insert(elem)}} return newRDD |
flatMap(func) | Similar to map, but each input item can be mapped to 0 or more output items (so func should return a Seq rather than a single item). | rdd.flatMap(lambda x:range(x,5)) 返回新的rdd,其中接收的函数返回range(x,5)是一个序列而不是单一值,新rdd将该迭代器中内容平铺开来,例如[2,3,4]->[2,3,4,3,4,4] | newRDD=oldRDD.flatMap(f(x)) ⇒ for elem in oldRDD {for r_elem in f(elem) {newRDD.insert(r_elem)}} |
sample(withReplacement, fraction, seed) | Sample a fraction fraction of the data, with or without replacement, using a given random number generator seed. | rdd.sample(False,0.1,seed) 对rdd抽样,抽样方式是第一个参数指定的withReplacement=False即不放回,数量是第二个参数指定的0.1倍于源rdd数据数量,并使用seed作为随机种子 | |
distinct([numPartitions])) | Return a new dataset that contains the distinct elements of the source dataset. | rdd.distinct() 对rdd去除重复元素,例如[1,1,1,1,2]->[1,2] | newRDD=oldRDD.distinct() ⇒ for elem in oldRDD {if elem not in newRDD {newRDD.insert(elem)} } |
*实际上并非循环实现,而是由执行器连接DAG中的依赖关系,这样写只是为了用常规的方法理解函数效果。
(2)两个RDD间(等价于集合操作)
Transformation | Meaning | Example |
---|---|---|
union(otherDataset) | Return a new dataset that contains the union of the elements in the source dataset and the argument. 交集 | [1,2,3].union([3,4,5]) -> [1,2,3,4,5] |
intersection(otherDataset) | Return a new RDD that contains the intersection of elements in the source dataset and the argument. 并集 | [1,2,3].intersection([3,4,5]) -> [3] |
subtract(otherDataset) | Return each value in self that is not contained in other. 差集 | [1,2,3].subtract([3,4,5]) -> [1,2] |
cartesian(otherDataset) | When called on datasets of types T and U, returns a dataset of (T, U) pairs (all pairs of elements). 笛卡尔积 | [1,2].cartesian([4,5]) -> [(1, 4), (1, 5), (2, 4), (2, 5)] |
3.2.2 键值对RDD转化:理解
键值对RDD(Pair RDD)是一种特殊的RDD,这种RDD中包含的元素都是二元元组,这些二元组被视为键值对(K,V)。
使用map便可以从普通RDD创建Pair RDD,只需要将输出设置为二元组类型即可。
Spark针对Pair RDD提供了一系列专有的转化操作,这些操作围绕被视为键K的元素展开;与普通RDD一样,这样的操作也可以分为单个RDD和两个RDD(等价连接操作)。
3.2.2.1 单个Pair RDD操作的核心:combineByKey
对于单个Pair RDD,一个核心的函数是combineByKey,因为groupByKey、aggregateByKey、foldByKey和reduceByKey都是由combineByKey特化而来(即基于combineByKey实现)。
Transformation | Meaning | Argument |
---|---|---|
combineByKey(createCombiner, mergeValue, mergeCombiners) | Generic function to combine the elements for each key using a custom set of aggregation functions. Turns an RDD[(K, V)] into a result of type RDD[(K, C)], for a “combined type” C. | Users provide three functions: (1)createCombiner, which turns a V into a C (e.g., creates a one-element list) ,(2)mergeValue, to merge a V into a C (e.g., adds it to the end of a list),(3)mergeCombiners, to combine two C’s into a single one (e.g., merges the lists) |
可以看到,参数为三个函数**,分别是:
- createCombiner:在当前数据分区*第一次遇到某个键K的时候,创建一个combiner,combiner的内部初始值为createCombiner函数的返回值;createCombiner输入为值V对应的类型,输出为combiner内部类型(“combined type”)C。
- mergeValue:在当前数据分区遇到已经遇到过的键K时(说明已经有对应的combiner),将当前值更新到combiner的内部值中,具体的更新方法就是mergeValue函数给出的方法;mergeValue输入为combiner当前内部值(类型为C)和遇到的当前值V,输出为新的combiner内部值。
- mergeCombiners:所有数据分区都有一个combiner,当这些combiner都工作完成后,需要mergeCombiners函数将它们的内部值合并为一个最终的值用于代表整个RDD的计算结果(而非分区的)***;mergeCombiners输入为两个combiner的内部值,输出为同类型的合并值。
*是当前分区而不是整个RDD,数据分区的内容在3.2.3节中阐述。
**实际上还有与分区相关的参数numPartitions=None 和 partitionFunc=<function portable_hash>
***此处可能发生数据混洗,见3.2.3节。
在此基础上,构建groupByKey、aggregateByKey、foldByKey和reduceByKey:
Transformation | Meaning | “Combined Type” | createCombiner | mergeValue | mergeCombiners |
---|---|---|---|---|---|
groupByKey() | When called on a dataset of (K, V) pairs, returns a dataset of (K, Iterable<V>) pairs. | C=iterable<V> | lambda v:[v] | lambda v,c: c.append(v) | lambda c1,c2: c1+c2 |
reduceByKey(func) | Merge the values for each key using an associative and commutative reduce function. | 由参数func返回值类型决定 | lambda v:v | func | func |
aggregateByKey(zeroValue, seqFunc, combFunc) | Aggregate the values of each key, using given combine functions and a neutral “zero value”. | 由参数seqFunc返回值类型决定 | lambda v: seqFunc(zeroValue,v) | seqFunc | combFunc |
foldByKey(zeroValue, func) | Merge the values for each key using an associative function “func” and a neutral “zeroValue” | 由参数func返回值类型决定 | lambda v: func(zeroValue,v) | func | func |
其他“ByKey”型函数基本与之类似,例如sortByKey、countByKey(注意这是一个action不是transformation)、sampleByKey等,基本都可以由groupByKey、aggregateByKey、foldByKey和reduceByKey推出。因此不再赘述。
3.2.2.2 两个PairRDD间操作的核心:cogroup
对于两个Pair RDD之间的操作(类似于数据库连接)其最核心的函数就是cogroup(groupWith):
Transformation | Meaning | Example |
---|---|---|
cogroup(otherDataset) | 执行rdd1.cogroup(rdd2)时,先分别对rdd1、rdd2进行groupByKey操作,得到两个键值对列表,两个列表分别包含(K,V)和(K,W)类型元素,然后将这两个列表按照如下方式合并*:(1)如果两个列表都拥有某一个键K,则将(K,iter<V>)和(K,iter<W>)合并为(K, (iter<V>, iter<W>)) ;(2)如果有一表有的键K另一表没有,则将(K,iter<V>)和(K,None)合并为(K, (iter<V>, None)) 或 (K, (None, iter<V>)) (取决于RDD的顺序)。这个操作也被称为groupWith。 | [(1,2),(2,3),(1,4)].cogroup([(1,10),(3,11)]) -> [(1,([2,4],[10])), (2,([3],None)), (3,(None,[11]))] |
其他的Pair RDD间操作在此基础上实现(下列例子中RDD1 = [(1,2),(2,3),(1,4)], RDD2=[(1,10),(3,11)]):
Transformation | Meaning | Example | 效果逻辑等价**代码 |
---|---|---|---|
join | 内连接,对两个RDD间相同键的值的连接 | [(1,(2,10)), (1,(4,10))] | for elem in rdd1.cogroup(rdd2) { if None in elem.value {continue} else { for _ in cartesian(elem.value[0],elem.value[1]){resultRDD.insert(elem.key, _ )}}} |
righterOuterJoin | 右外连接,保证RDD2的键必须在结果中 | [(1, (2, 10)), (1, (4, 10)), (3, (None, 11))] | for elem in rdd1.cogroup(rdd2) { if None in elem.value and elem.key in rdd1.keys{continue} else { for _ in cartesian(elem.value[0],elem.value[1]){resultRDD.insert(elem.key, _ )}}} |
leftOuterJoin | 左外连接,保证RDD1的键必须在结果中 | [(1, (2, 10)), (1, (4, 10)), (2, (3, None))] | for elem in rdd1.cogroup(rdd2) { if None in elem.value and elem.key in rdd2.keys{continue} else { for _ in cartesian(elem.value[0],elem.value[1]){resultRDD.insert(elem.key, _ )}}} |
fullOuterJoin | 全外连接,RDD1和RDD2的键均在结果中 | [(1, (2, 10)), (1, (4, 10)), (2, (3, None)), (3, (None, 11))] | for elem in rdd1.cogroup(rdd2) { for _ in cartesian(elem.value[0],elem.value[1]){resultRDD.insert(elem.key, _ )}} |
subtractByKey | 去掉RDD1中的那些存在RDD2中的键的元素 | [(2,3)] | for (k,v) in rdd1 {if k in rdd2.keys {continue} else {resultRDD.insert(k,v)}} |
*此处可能发生数据混洗,见3.2.3节。
**同样的,这并不是正确的代码,只是结果与实际结果在逻辑上等价。
3.2.2.3 其他单个Pair RDD Transformation速查
Transformation | Meaning |
---|---|
sortBy(keyfunc, [ascending], [numPartitions]) | Sorts this RDD by the given keyfunc |
sortByKey([ascending], [numPartitions]) | When called on a dataset of (K, V) pairs where K implements Ordered, returns a dataset of (K, V) pairs sorted by keys in ascending or descending order, as specified in the boolean ascending argument. |
keys() | Return an RDD with the keys of each tuple. |
values() | Return an RDD with the values of each tuple. |
mapValues(func) | Pass each value in the key-value pair RDD through a map function without changing the keys; this also retains the original RDD’s partitioning. 对每个键值对中的value执行map |
flatMapValues(func) | Pass each value in the key-value pair RDD through a flatMap function without changing the keys; this also retains the original RDD’s partitioning 对每个键值对中的value执行flatMap,并将原键与所有返回的迭代器中的值组合 |
3.2.3 跨节点数据混洗(Shuffle)与数据分区(Partition)
3.2.3.1 Shuffle:举例
在分布式系统中数据存储在不同节点上(例如HDFS),在进行与键有关的操作时,会产生跨节点的数据混洗,通过混洗,将在每个节点上具有相同键的数据结合到一起。例如在combineByKey的第三阶段,即将各个分区的combiner合并为最终结果的时候,需要将所有分区的相同键的信息合并,会发生(a)相同机器不同分区数据的合并,产生少量本地IO开销(b)不同机器数据的合并,需要跨节点的数据混序,产生大量网络通信开销。再例如cogroup中比较两个group后RDD的键,则需要将二者分别发送到中间机器上进行混洗后分区,在两端都会产生大量的网络通信。
在数据混洗的过程的数据都会被缓存到磁盘上(保存路径SPARK_LOCAL_DIRS给出,并且会按照这些目录平衡分配),数据混洗的输出数据存到内存中(大小受到spark.shuffle.memoryFraction限制)。
3.2.3.2 “Partition + Persist” 组合
Shuffle的通信开销极大,因此需要数据分区partition机制减少其发生,partition通过给定的函数将具有相同的键的数据尽量放在同一个分区里;(a)可以在读入文件的时候立即进行分区并持久化缓存(persist,见3.3.3节),在后续进行有关键的处理的时候减少数据混洗;(b)可以使用RDD转化中的分区操作手动重新分区并持久化缓存,与情况a类似;(c)在运行内含partition的transformation时候,会发生分区/数据混洗,持久化缓存使得后续的与键有关的操作加快。
以上三条可以总结为:执行数据分区(无论手动执行或者执行内部含有partition的任何操作)时会发生混洗,将混洗后的数据持久化缓存下来(如果不进行持久化,则分区的好处被抵消,因为有lazy execution),后续与键有关/内含分区的操作都会加快,因为分区后相同键的数据在同一分区上。
3.2.3.3 关于partition的更多信息
(1)内部含有partition并且可以显式指定partition函数和partition数量的操作:combineBykey(以及基于其的groupByKey、aggregateByKey、reduceByKey、foldByKey)、groupBy、sortBy、sortBykey等。这些操作通过partitionFunc参数指定分区函数,默认为<function portable_hash>;通过numParitions指定分区数量。
(2)内部含有partition但不可以指定分区方式只可以指定分区数量的操作:cogroup/groupWith( 以及基于其的join、leftOuterJoin、rightOuterJoin、fullOuterJoin)、distinct等。这些操作通过numParitions指定分区数量。
(3)运行partition后能够提升效率的操作除了cogroup/groupWith( 以及基于其的join、leftOuterJoin、rightOuterJoin)和combineBykey(以及基于其的groupByKey、aggregateByKey、reduceByKey等),还有lookup。
(4)如果使用HDFS作为文件系统,在默认情况下,每个HDFS的分区会对应一个RDD分区。
(5)由于并行的基本单位是分区,分区操作还会影响程序的并行度(见5.3节),因此需要选择合适的分区数量。
3.2.3.4 分区操作:速查表
Function | Meaning |
---|---|
partitionBy(numPartitions, [partitionFunc] | Return a copy of the RDD partitioned using the specified partitioner. |
repartition(numPartitions) | 打乱数据并重新分成numPartitions个区 |
repartitionAndSortWithinPartitions([numPartitions], [partitionFunc], [ascending], [keyfunc]) | Repartition the RDD according to the given partitioner and, within each resulting partition, sort records by their keys. |
coalesce(numPartitions, shuffle=False) | shuffle指定是否打乱数据,并减少分区数量至numPartitions |
getNumPartitions() | Returns the number of partitions in RDD |
注:partitionFunc默认均为spark.HashPartitioner,可以更改为spark.RangePartitioner或者自己定义新的分区函数(传入函数作为参数即可)。
3.2.4 管道(Pipe)
Spark可以与任何支持Unix标准流读写的程序之间传递数据。使用的时候要将该程序添加到文件列表中,方便各节点下载,例程如下:
scriptPath = 'path/to/script/scriptName.x'
scriptName = 'scriptName.x'
sc.addFile(scriptPath)
scriptFile = SparkFiles.get(scriptName)
pipe_output = pipe_input.pipe(scriptFile)
3.3 RDD Action
3.3.1 通用RDD行动:速查表
(1)count系列
Action | Meaning |
---|---|
count() | 返回RDD中元素总数量 |
countByValue() | 计算RDD中各元素数量,并返回(元素,数量)键值对RDD,例如[1,1,2].countByValue->[(1,2),(2,1)] |
(2)take系列
Action | Meaning |
---|---|
take(n) | Return an array with the first n elements of the dataset. |
collect() | Return all the elements of the dataset as an array at the driver program. 等价于take(count()) |
first() | Return the first element of the dataset (等价于 take(1)). |
takeSample(withReplacement, num, [seed]) | Return an array with a random sample of num elements of the dataset 等价于collect(sample()) |
takeOrdered(num, key=None) | Get the N elements from an RDD ordered in ascending order or as specified by the optional key function. 类似于collect(sortBy(key)) |
(3)aggregate系列
Action | Meaning |
---|---|
aggregate(zeroValue, seqOp, combOp) | 使用combineByKey的框架理解,此处zeroValue指定单分区combiner的初始零值,seqOp表示combiner遇到下一个值的时候如何更新内部值,combOp表示多个分区的combiner最终如何合并 |
fold(zeroValue, op) | 简化形式的aggregate,zeroValue指定单分区combiner的初始零值,该值会被加到combiner中若干次,op指定如何更新和合并combiner值,要求op符合结合律和交换律 |
reduce(func) | 简化形式的aggregate(最常用),要求func符合结合律和交换律 Aggregate the elements of the dataset using a function func (which takes two arguments and returns one). The function should be commutative and associative so that it can be computed correctly in parallel. |
(4)foreach:RDD迭代
Action | Meaning |
---|---|
foreach(func) | Applies a function to all elements of this RDD. 等价于for elem in rdd.collect(){func(elem)} ,例如可以使用print函数打印各元素。 |
(5)统计系列(仅针对纯数值RDD)
Action | Meaning |
---|---|
mean() | 平均值 |
sum() | 总和 |
max() | 最大值 |
min() | 最小值 |
variance() | 方差 |
stdev() | 标准差 |
stats() | 上述都运行一遍 |
sampleVariance() | 采样方差 |
sampleStdev() | 采样标准差 |
3.3.2 键值对RDD行动:速查表
Action | Meaning |
---|---|
countByKey() | Count the number of elements for each key, and return the result to the master as a dictionary. |
collectAsMap() | Return the key-value pairs in this RDD to the master as a dictionary. |
lookup(key) | Return the list of values in the RDD for key key. This operation is done efficiently if the RDD has a known partitioner by only searching the partition that the key maps to. |
3.3.3 RDD缓存(Persist)
RDD persist是一种触发立即执行的action。由于lazy execution机制,每当要多次使用同一个RDD时,为了避免重新计算该RDD的所有前序依赖;或者是在刚刚完成分区/数据混洗后,RDD已经分区完成,后续操作可以从继续使用这种分区方式中获益(或避免反复shuffle)的情况下,可以考虑使用RDD缓存。典型的情况有:
(1)读入文件后,立即缓存,避免后续反复读写文件;
(2)主动执行分区后,立即缓存,后续键值对相关部分操作效率提升;
(3)完成复杂数据处理后获得阶段性的结果,并且该结果被后续操作频繁使用,考虑缓存;
(4)在被动分区后,立即缓存,与情况2类似。
另外,即便没有主动缓存,内部优化也会在一定情况下将多次使用的RDD或混洗后的数据保存下来(本身就在磁盘上)并给后续使用。
Action | Meaning |
---|---|
persist([storageLevel]) | 缓存RDD,默认存储级别*为MEMORY_ONLY |
unpersist([blocking]) | 手动取消缓存**,并将已缓存的内容清除 |
*存储级别参数storageLevel有如下几种:
storageLevel | Meaning |
---|---|
MEMORY_ONLY | 将RDD反序列化后存在JVM堆内存中。存不下的分区将不会被缓存,因此每次需要时都会即时重新计算。Store RDD as deserialized Java objects in the JVM. If the RDD does not fit in memory, some partitions will not be cached and will be recomputed on the fly each time they’re needed. |
MEMORY_AND_DISK | 将RDD反序列化后存在JVM堆内存中。存不下的分区将缓存到磁盘,需要时从磁盘读取 |
MEMORY_ONLY_SER | (Java和Scala)将RDD存储为序列化的Java对象到内存(即不进行反序列化)。这比反序列化更节省空间、节省垃圾回收时间,但读取时会占用更多CPU时间。(Java and Scala) Store RDD as serialized Java objects (one byte array per partition). This is generally more space-efficient than deserialized objects, especially when using a fast serializer, but more CPU-intensive to read. |
MEMORY_AND_DISK_SER | (Java and Scala) 将RDD存储为序列化的Java对象到内存(即不进行反序列化),存不下的分区将缓存到磁盘,比反序列化更节省空间、节省垃圾回收时间,但读取时会占用更多CPU时间。 |
DISK_ONLY | Store the RDD partitions only on disk. |
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. | Same as the levels above, but replicate each partition on two cluster nodes. |
OFF_HEAP (试验性的) | 不存到堆内存而是其他内存中 Similar to MEMORY_ONLY_SER, but store the data in off-heap memory. This requires off-heap memory to be enabled. |
**除了手动取消缓存,Spark自动监视缓存使用情况,当缓存量达到spark.storage.memoryFraction规定的限制的时候,以LRU(最近最少使用)方式自动丢弃缓存。
4 Spark高级功能
4.1 Spark SQL
Spark SQL中使用的是一种特殊的RDD,即DataFrame(曾称为SchemaRDD)。这种RDD存储的是结构化的数据,既有存储结构信息的Schema,也有由row对象组成的行数据。
Spark SQL可以选择是否引入Hive支持(不需要另外安装Apache Hive),后者提供一些Hive中的特色功能:Hive表、HiveQL、UDF等。如果引入了Hive支持,那么需要创建与当前的SparkContext关联的HiveContext,如果没有,则需要引入SQLContext(注意,这两者在2.0版本后合并到了SparkSession里):
from pyspark.sql import HiveContext # Recommended
hc = HiveContext(sc)
##### OR ######
from pyspark.sql import SQLContext
hc = SQLContext(sc)
在引入了HiveContext/SQLContext后,可以使用.sql进行SQL查询。
Spark SQL的数据源包括文件和数据库(见3.1.3和3.1.4节)。
4.2 Spark Streaming
Spark Streaming中使用的数据结构DStream是一系列持续的RDD序列。每个时间片内接收的数据(一个batch)存储到一个RDD中。运行在执行器进程中的接收器接收数据流,将该batch的RDD(缓存下来后)传送给StreamingContext下属的SparkContext处理,按照用户指定的方式执行tasks,然后(按每batch)输出结果。
DStream相关的操作分为无状态转化操作(只依赖于当前单一RDD)、有状态转化操作(依赖于当前和前序多个RDD,需要维护一个窗口或状态)、输出操作。
Spark Streaming接收文件流、actor流、Kafka、Flume等数据源。
由于一个流式数据处理程序可能不间断运行,需要检查点机制来恢复数据,因此需每5-10个batch就缓存状态到可靠的文件系统中。
4.3 MLlib
MLlib提供了一系列与机器学习相关的方法,例如Feature Extractors(选取feature)、Feature Transformers(转化数据)、Estimators(各种分类、回归、聚类、协同过滤、频繁模式挖掘的机器学习模型)、Evaluators(根据给定参数和模型给出评分)、Validators(通过evaluator给出的评分决定一系列参数和模型中最优者) 和Pipeline(将上述操作串联成流水线作业)等。
Module | Methods |
---|---|
pyspark.ml.feature | 【Feature Extractors】 TF-IDF、Word2Vec、CountVectorizer、FeatureHasher |
【Feature Transformers】Tokenizer、StopWordsRemover、n-gram、Binarizer、PCA、PolynomialExpansion、Discrete Cosine Transform (DCT)、StringIndexer、IndexToString、OneHotEncoder、VectorIndexer、Interaction、Normalizer、StandardScaler、RobustScaler、MinMaxScaler、MaxAbsScaler、Bucketizer、ElementwiseProduct、SQLTransformer、VectorAssembler、VectorSizeHint、QuantileDiscretizer、Imputer | |
【Feature Selectors】VectorSlicer、RFormula、ChiSqSelector | |
pyspark.ml.classification | 【Classfication Estimators】 Logistic regression、Decision tree classifier、Random forest classifier、Gradient-boosted tree classifier、Multilayer perceptron classifier、Linear Support Vector Machine、One-vs-Rest classifier (a.k.a. One-vs-All)、Naive Bayes、Factorization machines classifier |
pyspark.ml.regression | 【Regression Estimators】Linear regression、Generalized linear regression、Available families、Decision tree regression、Survival regression、Isotonic regression、Factorization machines regressor |
【Tree Ensembles Regression Estimators】Random forest regression、Gradient-boosted tree regression | |
pyspark.ml.clustering | 【Clustering Estimators】 K-means、Latent Dirichlet allocation (LDA)、Bisecting k-means、Gaussian Mixture Model (GMM)、Power Iteration Clustering (PIC) |
pyspark.ml.recommendation | 【Collaborative Filtering Estimators】ALS |
pyspark.ml.fpm | 【Frequent Pattern Mining Estimators】FP-Growth、PrefixSpan |
pyspark.ml.tuning | 【Validators】 CrossValidator |
pyspark.ml.evaluation module | 【Evaluator】BinaryClassificationEvaluator、RegressionEvaluator、MulticlassClassificationEvaluator、MultilabelClassificationEvaluator、ClusteringEvaluator、RankingEvaluator |
pyspark.ml.Pipeline | 【Pipeline】Pipeline、PipelineModel |
5 程序写好了
5.1 代码依赖
一般情况下,可以通过在各个节点上pip/easy_install安装python库(或Maven/sbt安装java的jar包)来满足依赖,如果不能这样做,在使用spark-submit提交代码时,需要携带应用所依赖的文件。
参数 | 含义 |
---|---|
--jars | 携带少量第三方jar包,放入应用CLASSPATH参数中 |
--files | 携带数据文件,这些文件会被分发到各节点上 |
--py-files | 携带py、egg、zip文件作为第三方python库,放入应用PYTHONPATH参数中 |
5.2 Job, Stage和Task
用户通过代码定义RDD的转化和行动操作,这些操作被记录,并生成对应的DAG,DAG中记录了RDD间的谱系/依赖关系。
(1)使用rdd.toDebugString可以查看rdd的谱系;
(2)依赖关系分为宽依赖和窄依赖,其中不需要数据混洗的称为窄依赖(例如filter、map等),需要数据混洗的则是宽依赖(例如在没有分区的情况下进行join、reduceByKey等)。注意,已经分区过后进行的键值对操作仍为窄依赖,因为不发生数据混洗;
(3)窄依赖的操作可以串联起来并流水线化执行(pipelining),这样可以将整个DAG按照必要的宽依赖分开,并缩进为若干个stage;
(4)当每次触发立即执行时,调度器就会提交一个job来运行,一个job包含若干stage,一个stage包含若干RDD和并行的计算任务task(按照分区分配task,一个task需要一个CPU核心);
(5)集群的当前执行进度可以登录http://<driver_IP>:4040/stages/查看。
总结来说,每次action对应运行一个job,一个job按是否shuffle分切为若干stage,一个stage内部pipelining执行,并且将这些计算task调度到多个节点上并行执行。
5.3 性能
5.3.1 数据分区(Partition)与任务并行度
Spark应用内对于各个task采用公平调度(Fair Scheduler)。
Spark按照数据分区来分配task,一般来说1个数据分区(对应一个task)需要分配一个CPU核心,数据分区的总数决定了程序的并行度。并行度太低(CPU核心数大于任务使用的核心数)会造成闲置资源的浪费,并行度太高(CPU核心数远小于任务使用的核心数)会导致调度开销;因此合适的分区数导致合适的并行度。可以使用coalesce
或repartition
改变分区数量(见3.2.3.4节)。
5.3.2 内存管理
Spark运行中需要占用的 内存:
(1)RDD持久化缓存,存储到JVM堆内存中,默认的spark.stroage.memoryFraction=0.6
(2)数据混洗中产生的缓存,默认的spark.shuffle.memoryFraction=0.2
(3)用户代码中申请的内存,使用1、2的剩余空间,默认占比为20%
垃圾回收的效率与内存中的对象数量相关,而不是数据量相关,因此缓存时序列化数据可以显著减少垃圾回收的时间。
5.3.3 Spark UI
Spark提供图形化界面,通过访问驱动器节点的指定端口进入到Spark UI。这个端口由spark.ui.port决定,默认为4040。
其中包含的页面有:
Page | Meaning | 特色功能 |
---|---|---|
/jobs | 列举所有正在进行/结束运行/失败的job | 查看job的stage信息、task进度、timeline*、DAG等 |
/stages | 列举出所有job的所有正在进行/结束运行/失败的stage | 查看stage的task信息、性能指标、timeline、DAG等 |
/environment | 列举所有Spark配置信息 | |
/stroage | 列举缓存的RDD信息 | |
/executors | 列举所有执行器信息 | 查看thread dump** |
* Timeline功能用来查看各个步骤耗费的时间,如果发现耗时过长的项目,可以考虑其是否(1)在处理倾斜数据、(2)需要暂停来回收内存垃圾、(3)该节点的机器出现故障、(4)缓存设置有问题、……
** Thread dump收集执行器进程的线程调用栈
参考文献
RDD编程指南: https://spark.apache.org/docs/latest/rdd-programming-guide.html
Pyspark API文档: https://spark.apache.org/docs/latest/api/python/pyspark.html
Spark SQL 编程指南:https://spark.apache.org/docs/latest/sql-programming-guide.html
Spark 机器学习指南 https://spark.apache.org/docs/latest/ml-guide.html
《Learning Spark: Lightning-Fast Data Analysis》O’Reilly