spark数据分析导论
spark是什么?
spark是一个用来实现快速而通用的集群计算平台。主要的特点就是在内存中进行计算。
spark的软件栈
spark Core:实现了spark的基本功能,包括任务调度、内存管理、错误恢复与存储系统交互等模块以及对RDD(弹性分布式数据集)的定义及相关的API操作。
spark Sql:是spark用来操作结构化数据的程序包,支持Apache Hive 版本的HQL来进行数据查询。
**spark streaming:**是spark提供的对实时数据进行流式计算的组件。
**MLlib:**提供了常见的机器学习功能的程序库,提供了一些算法的实现:分类、回归、据类以及模型评估等。
**GraphX:**提供了操作图的程序库,可以进行并行的图计算。
RDD编程
RDD概述
**RDD:**弹性分布式数据集,其实就是分布式的元素集合。spark自动将RDD的数据分发到集群上,并将操作并行化执行。
RDD操作分为转化操作和行动操作,转化操作会由一个RDD生成一个新的RDD,行动操作会对RDD计算出一个结果,并把结果返回到驱动器程序中。转化操作是惰性的,需要行动进行触发,举一个例子:
lines = sc.textFile("README.md") #创建RDD,但是不会加载数据到内存中
python_lines = lines.filter(lambda line: "python" in line) # 转化成新的RDD
python_lines.first() # 并发执行计算
执行first()行动算子时,spark会只将含有python单词的第一行数据拉取到内存中,而不是全部拉取,默认情况下,Spark的RDD会在每次对他们进行行动操作时重新计算,如果想要在多个行动中使用同一个RDD的话,需要对RDD进行持久化操作。
创建RDD
两种方式:
- 取读外部数据
- 在驱动器程序中对一个集合进行并行化
lines = sc.parallelize(["pandas","I like python"]) # 开发时用的不多,因为需要将所有集合数据拉到一个节点上
RDD操作
两种操作:
- 转化操作:惰性操作,返回的一定是新的RDD。
- 行动操作:非惰性操作,返回的一定是其他类型的数据类型,这也是区别是否为行动操作的重要指标
errorsRDD = inputRDD.filter(lambda x: "error" in x)
warningsRDD = inputRDD.filter(lambda x: "warning" in x)
badlinesRDD = errorsRDD.union(warningsRDD) # 连接操作
上述代码中,spark会使用谱系图来记录这些不同RDD之间的依赖关系,当持久化的RDD缺失数据时,spark也是使用谱系图来进行RDD的恢复的。
转化操作时惰性的,所以在进行转化操作时,操作程序不会立即执行,所有我们最好不要把RDD看作存储某些特定数据的数据集合,我们可以把它看成存储记录如何计算数据的指令列表。
转化操作算子
map():接收一个函数,把这个函数用于RDD中的每个元素,将函数的返回结果作为结果RDD中的元素。
nums = sc.parallelize([1,2,3,4])
squared = nums.map(lambda x: x*x) # squared: 1,4,9,16
filter():接收一个函数,将RDD中满足该函数的元素放入新的RDD中返回。
words = sc.parallelize(["python","java","C","C++"])
squared = words.map(lambda x: "C" in x) # squared: "C","C++"
flatMap():接收一个函数,应用于RDD中的每个元素并将返回的所有元素构成新的RDD.
lines = sc.parallelize(["python R beautiful","java is good","C","C++ well"])
words = lines.flatMap(lambda x: x.split(" ")) # 'python', 'R', 'beautiful', 'java', 'is', 'good', 'C', 'C++', 'well'
distinct():去重
sample():对RDD进行采样
union():生成一个包含两个RDD中所有元素的RDD
intersection():求两个RDD共同的元素的RDD
subtract():移除一个RDD中的内容
cartesian():与另一个RDD的笛卡尔积
num1 = sc.parallelize([1,2,3])
num2 = sc.parallelize([3,4,5])
num_all = num1.union(num2) # 1,2,3,3,4,5
num_dis = num_all.distinct() # 1,2,3,4,5
num_samp = num_all.sample(False,0.6) # 不确定的2个数
num_inter = num1.intersection(num2) # 3
num_sub = num1.subtract(num2) # 1,2
num_car = num1.cartesian(num2) # (1,3),(1,4)...(3,5)
行动操作算子(常用)
reduce():接收一个函数,操作两个相同类型的元素,并返回一个同样类型的元素。
collect():返回RDD中所有元素,需要将集群上的数据拉取到单节点上。
count():返回RDD中元素的个数
countByValue():各个元素在RDD中出现的次数
take(num):从RDD中返回num个元素
top(num):从RDD中返回最前面的num个元素
aggregate(zeroValue)(seq0p,comb0p):与reduce类似,不同之处在于返回不同类型的数据。理解起来很难,可以参考此链接文章
RDD持久化
RDD是惰性求值的,当重复的使用同一个RDD时,每次都会重算RDD以及它的所有依赖,消耗很大,需要对RDD进行持久化
RDD.persist()
RDD.cache()
参数末尾加上“_2”表示持久化数据分为两份
当缓存的数据过多,内存存不下时,spark会采用最近最少使用的缓存策略从内存中清除数据,若仅是内存缓存级别,会直接清除,下次使用,重新计算,若是磁盘级别,会将移除的数据存入磁盘中。
键值对RDD操作
spark中包括键值对类型的RDD叫做键值对RDD
以键值对集合{(1,2),(3,4),(3,6)}为例
数据读取与保存
文本文件: 每一行都是RDD的一个元素
input = sc.textFile("file:///home/README.md")
result.saveAsTextFile(outputFile)
json:读取json数据时,常常需要将其所在文件读入,然后使用json解析器来对json进行解析。
此外还支持csv、sequenceFile、对象文件、hdsf、hbase、hive等数据的读取与写入。
集群环境下的spark
在分布式环境下,spark集群采用的是主/从结构,有一个节点负责中央协调(driver节点),调度各个分布式的工作节点(执行器)。
驱动器节点:spark驱动器就是执行程序中的main()方法的进程。两大职责:
- 驱动器程序负责把用户程序转化为多个物理执行的单元(task任务),其实相当与由操作指令和逻辑组成了一个有向无环图(DAG),然后按照这个DAG执行物理计划。
- 调度执行器节点,根据DAG,驱动器程序会在各个执行器进程间协调任务的调度。驱动器会根据当前的执行点集合,尝试把所有任务基于数据所在位置分配给合适的执行器进程,从而减少网络消耗。
执行器节点:spark执行器是一种工作进程,负责在spark作业中运行任务,任务间相互独立。两大职责:
- 负责运行组成spark应用的任务,并将结果返回给驱动器进程。
- 他们通过自身的块管理器为用户程序中要求缓存的RDD提供内存式存储。
小结:
- 用户通过spark2-submit脚本提交应用。
- spark2-submit脚本启动驱动器程序,调用用户定义的main()方法。
- 驱动器程序与集群管理器通信,申请资源以启动执行器节点。
- 集群管理器为驱动器程序启动执行器节点。
- 驱动器进程执行用户应用中的操作。根据程序中所定义的对RDD的转化操作和行动操作,驱动器节点把工作以任务的形式发送到执行器进程。
- 任务在执行器程序中进行计算并保存结果。
- 如果驱动器程序的main()方法退出,或者调用了sparkContext.stop(),驱动器程序会终止执行器进程,并且通过集群管理器释放资源。
spark2-submit部署应用
bin/spark2-submit [optinos] <app jar | python file> [app options]
spark调优与调试
每个sparkContext对象都需要一个sparkConf的实例,sparkConf的三个创建方式。
- spark默认配置项。
- 代码层次创建的对象
conf = new SparkConf() conf.set("spark.app.name", "my spark app")
- 使用spark-submit传递的参数。
实际配置生效优先级:2 > 3 > 1
并行度优化
RDD会被分为一系列的分区,每个分区都是整个数据的子集,当spark调度并运行任务时,spark会为每个分区中的数据创建出一个任务,因此调整分区的数量提升并行度来提高程序的效率。两种调整分区的方法:
- 在数据混洗操作时,使用参数的方式为混洗后的RDD指定并行度。
- 对已有的RDD进行重新分区来获得更多或者更少的分区数
RDD.repartition(num)
序列化格式
当Spark需要通过网络传输数据,或是将数据溢写到磁盘上时,spark需要把数据序列化为二进制格式,默认情况,spark会使用java内建的序列化库,在这一点上,我们可以使用其他的更好的序列化工具库,如Kryo
内存管理
spark中内存主要有以下几种用途:
- RDD存储:当进行RDD持久化时,RDD的分区会被缓存到内存中。
- 数据清洗与聚合的缓存区:当进行数据混洗操作时,spark会创建一些缓冲区来存储输出数据或聚合操作的中间结果,可以通过spark.shuffle.memoryFraction参数来限定缓存区内存占总内存的比例。
- 用户代码:用户代码中定义的函数,数组及对象等会占用大部分内存。
默认情况下,spark的内存分配方案:RDD存储:60%;缓存区:20%;用户代码:20%。
内存优化的三种方案:
- 根据需要调整各个内存占比。
- 进行RDD持久化时,选择最优的缓存等级,若内存占用较大,可以采用MEMORY_AND_DISK(溢出后缓存到磁盘上)的等级。
- 当内存占用较大时,缓存序列化后的对象,而不是要直接缓存,虽然先序列化会浪费一部分时间,但是也比需要时从新根据谱系图重新计算要快。
Spark SQL介绍
spark SQL 是用来操作结构化和半结构化数据的接口,提供以下三大功能:
- 可以从各种结构化数据源中读取数据。
- 不仅支持在spark程序内使用sql语句进行数据查询,也支持外部工具中通过标准数据库连接器(JDBC/ODBC)进行查询。
- spark SQL 支持SQL与常规的python/java/scala代码高度整合。
### 下面介绍一下写pyspark SQL脚本的一些语法
from pyspark.sql import SparkSession # 引入pyspark sql 包
import pyspark.sql.functions as F # 导入pyspark sql 中相关函数
from pyspark.sql.window import Window # 导入开窗函数,数据清洗时需要用到
import argparse # 导入获取执行python脚本运行参数的包
from pyspark.sql.types import * # 导入使用自定义函数udf时,用的返回类型
# 获取脚本执行时需要传递的参数
paraser = argparse.ArgumentParser()
paraser.add_argument('-date','--stat_dt') # 添加参数命--stat_dt 或 -date的参数
args = paraser.parse_parse_args()
stat_dt = args.stat_dt # 得到stat_dt参数值
# 定义一个函数,作为udf函数参数使用
def get_test_data(name):
if name == "python":
return 1
else:
return 0
#定义udf函数
udf_get_test_data = F.udf(get_test_data,IntegerType()) #注意函数名不要(),后面参数为函数返回值 # 的类型
# 获取一个sparkSession对象
spark = SparkSession.builder.appName("my test app")
.config("hive.exec.dynamic.partition","true") \ # 设置开启动态分区
.config("hive.exec.dynamic.partition.mode","nonstrict") \ # 全部动态分区
.enableHiveSupport() \
.getOrCreate()
# 获取hive中数据
data = spark.sql("select cust_id,name,price,stat_dt from test_table") # 获取hive上相关字段数据
data = data.filter(data.stat_dt == stat_dt) # 过滤其他数据
data = data.withColumn("class", udf_get_test_data(F.col("name"))) # 添加一列
data = data.withColunm("price_sum", F.sum("price")).over(Window.partition("cust_id")) # 算出每个客户的总价值,使用groupBy会丢失数据
data = data.withColumn("index", F.row_number().over(Window.partitionBy("cust_id").orderBy("price"))) # 未每个客户按照价格排序
data = data.filter(data.index = F.lit("1")) # 获取每个客户价格最低的数据
data = data.drop("stat_dt") # 删除分区列
data.createOrReplaceTempView("data") # 注册为临时表
spark.sql("alter table standard.test_table_result drop if exists partition(stat_dt='%s')" %(stat_dt)) # 删除原有分区数据,支持重跑
spark.sql("insert into standard.test_table_result partition(stat_dt='%s')" %(stat_dt)) # 插入结果数据
spark.stop() # 停止程序