1.RDD详解
1.1 为什么需要RDD
分布式计算需要:分区控制、Shuffle控制、数据存储/序列化/发送、数据计算API等
这些功能不能简单的通过Python内置的本地集合对象去完成,在分布式框架中,需要一个统一的数据抽象对象,来实现上述分布式计算所需的功能,这个抽象对象就是RDD。
1.2 什么时RDD
RDD(Resilient Distributed Dataset)叫弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算的集合
Dataset:一个数据集合,用于存放数据的
Distributed:RDD中的数据是分布式存储的,可用于分布式计算
Resilient:RDD中的数据可以存储在内存中或者磁盘中
1.3 RDD的五大特性
1.RDD是分区的
RDD的分区是RDD数据存储的最小单位
一份RDD的数据本质上是分隔成了多个分区
sc.parallelize([1,2,3,4,5,6,7,8,9], 3).glom().collect()
sc.parallelize([1,2,3,4,5,6,7,8,9], 6).glom().collect()
2.计算方法都会作用到每一个分片上
sc.parallelize([1,2,3,4,5,6,7,8,9], 3).glom().collect()
sc.parallelize([1,2,3,4,5,6,7,8,9], 3).map(lambda x: x*10).glom().collect()
3.RDD之间是有互相依赖的关系的
sc = SparkContext(conf = conf)
rdd1 = sc.textFile("t.txt")
rdd2 = rdd1.flagMap(lambda x: x.split(' '))
rdd3 = rdd2.map(lambda x: (x, 1))
rdd4 = rdd3.reduceByKey(lambda a, b: a + b)
print(rdd4.collect())
4.kv型RDD可以有分区器
默认分区器:Hash分区规则,可以手动设置一个分区器(rdd.partitionBy的方法来设置)
这个特性是可能的,因为不是所有RDD都是Key-Value型
key-vlue rdd:RDD中存储的是二元元组
5.RDD的分区规划会尽量耗尽数据所在的服务器
初始RDD(读取数据的时候)规划时,分区会尽量规划到存储数据所在的服务器。因为这样可以走本地读取,避免网络读取
2.RDD编程入门
2.1 变成执行入口SparkContext对象
Spark RDD变成的程序入口对象是SparkContext对象
只是构建出SparkContext,基于它才能执行后续的API调用和计算
本质上,SparkContext对编程来说,主要功能就是创建一个RDD出来
conf = SparkConf().setAppName(appName).setMaster(master)
sc = SparkContext(conf=conf)
2.2 RDD的创建
RDD创建主要有两种方式:通过并行化集合创建、读取外部数据源
并行化创建
并行化创建是指将本地集合转向分布式RDD
rdd = sc.parallelize(参数1, 参数2)
# 参数1 集合对象,比如list
# 参数2 分区数
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 0.构建SparkContext对象
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
# 演示通过并行化集合的方式创建RDD,本地集合 -> 分布式对象
rdd = sc.parallelize([1,2,3,4,5,6])
# parallelize方法,没有执行分区数,默认分区数是多少?根据cpu核心数定
print("默认分区数:",rdd.getNumPartitions())
rdd = sc.parallelize([1, 2, 3, 4, 5, 6])
print("分区数:", rdd.getNumPartitions())
# collect方法,将RDD中每个分区的数据都发送到Driver中,形成一个Python List对象
# collect:分布式对象 -> 本地集合
print("rdd的内容", rdd.collect())
读取文件创建
textFile API:这个API可以读取本地数据,也可以读取hdfs数据
sparkcontext.textFile(参数1, 参数2)
# 参数1 必填,文本路径 支持本地文件 支持hdfs 也支持一些比如s3协议
# 参数2 可填,表示最小分区数量
# 注意:参数2 话语权不足 spark有自己的判断,他在允许范围内参数2 有效果,超出spark允许范围则失效
wholeTextFile:读取文件的API,适用于读取一堆小文件。参数和textFile一样
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 构建SparkContext对象
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
# 通过textFile API读取数据
# 读取文本数据
file_rdd1 = sc.textFile("words.txt")
# 默认分区数和文件大小有关,读取hdfs文件则和文件block块数量有关
print("默认读取分区数:", file_rdd1.getNumPartitions())
print("file_rdd1内容:", file_rdd1.collect())
# 加载分区数参数的测试
file_rdd2 = sc.textFile("words.txt", 3)
#最小分区数 spark有自己的判断,给太大spark不理会
file_rdd3 = sc.textFile("words.txt", 100)
print("file_rdd2分区数:", file_rdd2.getNumPartitions())
print("file_rdd3分区数:", file_rdd3.getNumPartitions())
# 读取hdfs文件数据测试
hdfs_rdd = sc.textFile("hdfs://node1:8020/input/words.txt")
print("hdfs_rdd内容:", hdfs_rdd.collect())
# 读取小文件文件夹
rdd = sc.wholeTextFiles("tiny_files")
print(rdd.collect())
print(rdd.map(lambda x: x[1]).collect())
2.3 RDD 算子
算子:分布式集合对象上的API称为算子
方法\函数:本地对象API
算子:分布式对象API
算子分类:Transformation 转换算子;Action 动作算子
Transformation转化算子
定义:RDD的算子,返回值仍旧是一个RDD,称为转换算子
特性:算子是lazy懒加载的,如果没有action算子,Transformation算子是不工作的
Action算子
定义:返回不是rdd的就是action算子
2.4 常用Transformation算子
map算子
功能:map算子是RDD的数据一条条处理(处理的逻辑基于map算子中接受的处理函数),返回新的RDD
语法:
rdd.map(func)
# func : f:(T) -> U
# f: 表示是一个函数(方法)
# (T) -> U 表示的是方法的定义,T和U都是泛型,表示任意类型,U是返回值
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1,2,3,4,5,6], 3)
# 定义方法,作为算子的传入函数体
def add(data):
return data + 10
print(rdd.map(add).collect())
# 定义lambda表达式来写匿名函数
print(rdd.map(lambda data: data * 10).collect())
flatMap算子
功能:对rdd执行map操作,然后进行接触嵌套操作
解除嵌套:
# 嵌套的list
lst = [[,2,3,],[4,5,6],[7,8,9]]
# 如果解除嵌套
lst = [1,2,3,4,5,6,7,8,9]
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize(["a b c", "a c e", "e c a"])
# 按照空格切分数据后,解除嵌套
print(rdd.flatMap(lambda x: x.split(" ")).collect())
reduceByKey算子
功能:针对kv型RDD,自动按照key分组,然后根据提供的聚合逻辑,完成组内数据的聚合操作
用法:
rdd.reduceByKey(func)
# func: (V, V) -> V
# 接受2个传入参数(类型要一致),返回一个返回值,类型和传入要求一致
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([('a', 1),('a', 1),('b', 1),('b', 1),('b', 1),('b', 1)])
result = rdd.reduceByKey(lambda a, b: a + b)
print(result.collect())
# 结果:[('b',4),('a',2)]
mapValues算子
功能:针对二元元组RDD,对其内部的二元元组Value执行map操作
语法:
rdd.mapValues(func)
# func: (V) -> U
# 注意:传入的参数是二元元组的value值
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([('a', 1),('a', 11),('a', 6),('b', 3),('b', 5)])
# rdd.map(lambda x: (x[0], x[1] * 10))
# 将二元元组的所有value都乘以10进行处理
print(rdd.mapValues(lambda x: x * 10).collect())
wordcount案例回顾
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
# 1.读取文件获取数据,构建RDD
file_rdd = sc.textFile("words.txt")
# 2.通过flatMap API取出所有的单词
word_rdd = file_rdd.flatMap(lambda x: x.split(" "))
# 3.将单词转换为元组,key是单词,value是1
word_with_one_rdd = word_rdd.map(lambda word: (word, 1))
# 4.用reduceByKey 对单词进行分组并进行value的聚合
result_rdd = word_with_one_rdd.reduceByKey(lambda a, b: a + b)
# 5.同故宫collect算子,将rdd的数据收集到Driver中,打印输出
print(result_rdd.collect())
groupBy算子
功能:将rdd的数据进行分组
语法:
rdd.groupBy(func)
# func 函数
# func: (T) -> K
# 函数要求传入一个参数,返回一个返回值,类型无所谓
# 这个函数是拿到返回值后,将所有相同的返回值放入一个组中
# 分组完成后,每个组是一个二元元组,key是返回值,所有的数据放入一个迭代器对象中作为value
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1,2,3,4,5])
# 分组,将数字分层 偶数和奇数2组
rdd2 = rdd.groupBy(lambda num: 'env' if (num % 2 == 0) else 'odd')
# 将rdd2的元素的value转换成list,这样print可以输出内容
print(rdd2.map(lambda x: (x[0], list(x[1]))).collect())
#输出结果:[('even',[2, 4]),('odd',[1, 3, 5])]
Filter
功能:过滤想要的数据进行保留
语法:
rdd.filter(func)
# func: (T) -> bool 传入1个参数进来类型随意,返回True或False
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1,2,3,4,5])
# 保留奇数
print(rdd.filter(lambda x: x % 2 == 1).collect())
distinc算子
功能:对RDD数据进行去重,返回新的RDD
语法:
rdd.distinct(参数1)
# 参数1,去重分区数量,一般不用传
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1,2,3,4,5,2,3,6,4,2,4,3,])
# distinct 进行RDD数据去重
print(rdd.distinct().collect())
union算子
功能:2个rdd合并成1个rdd返回
用法:
rdd.union(other_rdd)
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd1 = sc.parallelize([1,2,3,3])
rdd2 = sc.parallelize(['a','b'])
union_rdd = rdd1.union(rdd2)
print(union_rdd.collect())
注意:不会去重,不同类型也能合并
join算子
功能:对两个RDD执行join操作(可实现sql的内外连接)
注意:join算子只用于二元元组
语法:
rdd.join(other_rdd) # 内连接
rdd.leftOuterJoin(other_rdd) # 左外
rdd.rightOuterJoin(other_rdd) # 右外
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd1 = sc.parallelize([(1001,"zhangsan"),(1002,"lisi"),(1003,"wangwu"),(1004,"zhaoliu")])
rdd2 = sc.parallelize([(1001,"销售部"),(1001,"科技部")])
# 通过join算子来进行rdd之间的关联
# 对于join算子来说,关联条件按照二元元组的key进行关联
print(rdd1.join(rdd2).collect())
# 左外连接
print(rdd1.leftOuterJoin(rdd2).collect())
# 右外连接
print(rdd1.rightOuterJoin(rdd2).collect())
intersection算子
功能:求2个rdd的交集,返回一个新的rdd
用法:
rdd.intersection(other_rdd)
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd1 = sc.parallelize([('a',1),('b',1)])
rdd2 = sc.parallelize([('a',1),('c',1)])
union_rdd = rdd1.intersection(rdd2)
print(union_rdd.collect())
# 结果:[('a',1)]
glom算子
功能:将RDD的数据,加上嵌套,这个嵌套按照分区来进行
比如RDD数据[1,2,3,4,5]有2个分区,那么被glom后,数据变成:[[1,2,3],[4,5]]
使用方法:
rdd.glom()
groupByKey算子
功能:针对KV型,自动按照key分组
用法:
rdd.groupByKey() 自动按照key分组
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([('a',1),('b',1),('a',1),('b',1),('b',1)])
grouped_rdd = rdd.groupByKey()
print(grouped_rdd.map(lambda x: (x[0], list(x[1]))).collect())
# 结果:[('b',[1,1,1],('a',[1,1])]
sortBy算子
功能:对RDD数据进行排序,基于指定的排序依据
语法:
rdd.sortBy(func, accendint=False, numpartition=1)
# func: (T) -> U 告知暗战rdd中的哪个数据进行排序,比如lambda x: x[1]表示按照rdd中第二列元素进行排序
# ascending True升序 False 降序
# numPartition 用多少分区排序
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([('a',1),('b',2),('a',3),('b',1),('b',4),('c',3),('b',5),('d',4)])
# 使用sortBy对rdd执行排序
# 按照value 数字进行排序
# 参数1 函数,表示按照哪个列进行排序
# 参数2 True 升序 False 降序
# 参数3 排序的分区数
# 注意:如果要全局有序,则排序分区数设置为1
print(rdd.sortBy(lambda x: x[1], ascending=True, numPartitions=5).collect())
# 按照key进行排序
print(rdd.sortBy(lambda x: x[0], ascending=False, numPartitions=1).collect())
sortByKey算子
功能:针对KV型RDD,按照key进行排序
语法:
sortByKey(ascending=True, numPartitions=None, keyfunc=<function RDD.<lambda>>)
ascending:升序或降序,True升序,False降序,默认升序
numPartitions:按照几个分区进行排序,如果全局有序,设置为1
keyfunc:在排序前对key进行处理,语法是:(k) -> U ,一个参数传入,返回一个值
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([('A',1),('b',2),('a',3),('b',1),
('b',4),('c',3),('B',5),('d',4),
('c',4),('g',3),('f',5),('e',4)],3)
print(rdd.sortByKey(ascending=True, numPartitions=1, keyfunc=lambda key: str(key).lower()).collect())
案例
数据示例:
{"id":1,"timestamp":"2019-05-08T01:03:.00Z","category":"平板电脑","areaName":"北京","money":"1450"}|{"id":1,"timestamp":"2019-05-08T01:03:.00Z","category":"电脑","areaName":"上海","money":"1513"}
需求:读取data文件加中的order.txt文件,提取北京的数据,组合北京和商品类别进行输出,同时对结果集进行去重,得到北京售卖的商品类别信息
from pyspark import SparkConf, SparkContext
import json
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
# 读取数据文件
file_rdd = sc.textFile("data/order.txt")
# 进行rdd数据的split 按照|符号进行,得到一个个的json数据
jsons_rdd = file_rdd.flatMap(lambda line: line.split("|"))
# 通过python自带的json库,完成json字符串到字典对象的转换
dict_rdd = jsons_rdd.map(lambda json_str: json.loads(json_str))
# 过滤数据,只保留北京的数据
beijing_rdd = dict_rdd.filter(lambda d: d['areaName'] == "北京")
# 组合北京和商品类型形成新的字符
category_rdd = beijing_rdd.map(lambda x: x['areaName'] + "_" + x['category'])
# 对结果集进行去重
result_rdd = category_rdd.distinct()
#输出
print(result_rdd.collect())
2.5 常用Action算子
countByKey算子
功能:统计key出现的次数(一般书用于KV型RDD)
代码:
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd1 = sc.textFile("words.txt")
rdd2 = rdd1.flatMap(lambda x: x.split(' '))
rdd3 = rdd2.map(lambda x: (x, 1))
# rusult 不是rdd 而是dict
result = rdd3.countByKey()
print(result)
collect算子
功能:将RDD各个分区内的数据,统一收集到Driver中,形成一个List对象
用法:
rdd.collect()
reduce算子
功能:对RDD数据集按照传入的逻辑进行聚合
语法:
rdd.reduce(func)
# func: (T, T) -> T
# 2参数传入 1个返回值, 返回值和参数要求类型一致
rdd = sc.parallelize(range(1,10))
# 将rdd的数据进行累加求和
print(rdd.reduce(lambda a, b: a + b))
fold算子
功能:和reduce一样,接受传入逻辑及逆行聚合,聚合是带初始值的
这个初始值聚合会用在:分区内聚合、分区间聚合
rdd = sc.parallelize(range(1, 10),3)
print(rdd.fold(10,lambda a,b: a + b))
# 结果:85
# 10 + 16 + 25 + 34 = 85
first算子
功能:去除RDD的第一个元素
用法:
>>> sc.parallelize([3,2,1]).first()
3
task算子
功能:取RDD的前N个元素,自核成list返回
用法:
>>> sc.parallelize([3,2,1,4,5,6]).take(5)
[3, 2, 1, 4, 5]
top算子
功能:对RDD数据集进行降序排序,取前N个
用法:
>>> sc.parallelize([3,2,1,4,5,6]).top(3)
[6, 5, 4]
count算子
功能:计算RDD有多少条数据,返回一个数字
用法:
>>> sc.parallelize([3,2,1,4,5,6]).count()
6
takeSample算子
功能:随机抽样RDD数据
用法:
takeSample(参数: True or False, 参数2: 采样数, 参数3: 随机数种子)
参数1:True表示允许取同一个数据 False表示不允许取同一个数据。和数据内容无关,是否重复表示的是同一个位置的数据
参数2:抽样个数
参数3:随机数种子
代码
rdd = sc.parallelize([5,8,4,8,9,3,5,8,5,6], 1)
print(rdd.takeSample(True, 8))
takeOrdered算子
功能:对RDD进行排序取前N个
用法:
rdd.takeOrdered(参数1, 参数2)
参数1: 要几个数据
参数2:对排序的数据进行更改,不会更改数据本身,只是在排序的时候换个样子
代码:
rdd = sc.parallelize([1,3,2,4,7,9,6], 1)
print(rdd.takeOrdered(3))
# 将数字转换为负数,那么原本整数最大的反而变成排序中最小的,固定出现在前端
print(rdd,takeOrdered(3, lambda x: -x))
foreach算子
功能:对RDD的每一个元素,执行提供的逻辑操作,但是这个方法没有返回值
用法:
rdd.foreach(func)
# func: (T) -> None
代码:
rdd = sc.parallelize([1,3,2,4,7,9,6],1)
# 对数据执行乘10操作
# 注意:函数不能给返回值,所以在里面直接打印
r = rdd.foreach(lambda x: print(x * 10))
print(r) # 这个r对象,是None 因为foreach没有返回值
saveAsTextFile算子
功能:将RDD的数据写入文本文件中
支持本地写出,hdfs等文件系统
代码:
rdd = sc.parallelize([1,3,2,4,7,9,6],3)
rdd.saveAsTextFile("hdfs://node:8020/output/1111")
注意点
foreach和saveAsTextFile是分区直接执行的,跳过Driver,由分区所在的Executor直接执行,而其他Action算子都会将结果发送至Driver
2.6 分区操作算子
mapPartitions算子
mapPartitions一次传输一整个分区的数据
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1,3,2,4,5,6,7],3)
def process(iter):
result = list()
for it in iter:
result.append(it * 10)
return result
print(rdd.mapPartitions(process).collect())
foreachPartition算子
功能:和普通foreach一样,一次处理的是整个分区数据
rdd = sc.parallelize([1,3,2,4,7,9,6],3)
def ride10(data):
result = list()
for i in data:
result.append(i * 10)
print(result)
rdd.foreachPartition(ride10)
结果:
[10, 30]
[20, 40]
[70, 90, 60]
partitionBy算子
功能:对RDD进行自定义分区操作
用法:
rdd.partitionBy(参数1, 参数2)
参数1 重新分区后有几个分区
参数2 自定义分区规则 函数传入
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([('hadoop',1),('spark',1),('flink',1),('hadoop',1),('spark',1)])
def process(k):
if 'hadoop' == k or 'hello' == k: return 0
if 'spark' == k: return 1;
return 2
# 使用partitionBy自定义分区
print(rdd.partitionBy(3, process).glom().collect())
repartition算子
功能:对RDD的分区执行重新分区(仅数量)
用法:
rdd.repartition(N)
传入N 决定新的分区数
代码:
# repartition 重新分区(数量)
rdd2 = rdd.repartition(5)
print(rdd2.glom().collect())
rdd3 = rdd.repartition(1)
print(rdd3.glom().collect())
面试题:groupByKey和reduceByKey的区别
在功能上的区别:
groupByKey仅有分组功能
reduceByKey除了特有ByKey的分组功能外,还有reduce聚合功能
性能:
reduceByKey的性能远优于groupByKey+聚合逻辑
3.RDD的持久化
3.1 RDD的数据是过程数据
RDD之间相互迭代计算,当执行开启后,新的RDD生成,老的RDD消失
RDD的数据是过程数据,只在处理的过程中存在,一旦处理完成,就不见了
3.2 缓存
RDD的缓存技术:Spark提供了缓存API,可以让我们通过调用API,将指定的RDD数据保留在内存或硬盘上
rdd.cache() # 缓存到内存中
rdd.persist(StorageLevel.MEMORY_ONLY) # 仅内存缓存
rdd.persist(StorageLevel.MEMORY_ONLY_2) # 仅内存缓存,2副本
rdd.persist(StorageLevel.DISK_ONLY) # 仅缓存硬盘上
rdd.persist(StorageLevel.DISK_ONLY_2) # 仅缓存硬盘上,2副本
rdd.persist(StorageLevel.DISK_ONLY_3) # 仅缓存硬盘上,3副本
rdd.persist(StorageLevel.MEMORY_AND_DISK) # 先放内存,不够放硬盘
rdd.persist(StorageLevel.MEMORY_AND_DISK_2) # 先放内存,不够放硬盘,2副本
rdd.persist(StorageLevel.OFF_HEAP) # 堆外内存(系统内存)
# 主动清理缓存API
rdd.unpersist()
缓存特点
缓存技术可以将过程RDD数据,持久化保存到内存或硬盘上
但是这个保存在设定上并不安全
缓存特点:保留RDD之间的血缘关系;分散存储
一旦缓存丢失,可以基于血缘关系的记录重新计算RDD数据
3.3 RDD的CheckPoint
CheckPoint技术将RDD的数据保存起来,但是它仅支持硬盘存储
它的设计被认为是安全的,不保留血缘关系
缓存和CheckPoint的对比
CheckPoint不管分区数量多少,风险是一样的,缓存分区越多,风险越高
CheckPoint支持写入HDFS,缓存不行,HDFS是高可靠存储,CheckPoint被认为是安全的
CheckPoint不支持内存,缓存可以,缓存如果写内存 性能比CheckPoint要好一些
CheckPoint因为设计是安全的,所以不保留血缘关系,缓存保留
代码:
# 设置CheckPoint第一件事,选择CP的保存路径
# 如果是LOCAL模式,可以支持本地文件系统,如果在集群运行,千万要用HDFS
sc.setCheckPointDir("hdfs://node1:8020/output/bj52chp")
# 用的时候直接调用checkpoint算子
rdd.checkpoint()
4.Spark案例练习
使用搜狗实验室提供的“用户查询日志(SogouQ)”数据,使用Spark框架,将数据分装到RDD中进行业务数据处理分析
数据包含字段:搜索时间、用户ID、搜索内容、URL返回排名、用户点击顺序、用户点击的URL
from pyspark import SparkConf, SparkContext
from pyspark.storagelevel import StorageLevel
import jieba
from operator import add
def context_jieba(data):
"""通过jieba分词工具,进行分词操作"""
seg = jieba.cut_for_search(data)
l = list()
for word in seg:
l.append(word)
return l
def filter_words(data)
"""过滤停用词"""
return data not in ["谷","帮"]
def append_word(data):
"""修订某些关键词的内容"""
if data == "博学": data = "博学谷"
if data == "院校": data = "院校帮"
return (data, 1)
def extract_user_and_word(data):
"""传入数据是元组"""
user_id = data[0]
content = data[1]
# 对content进行分词
words = context_jieba(content)
return_list = list()
for word in words:
if filter_words(word):
return_list.append(user_id + "_" + append_word(word)[0], 1)
return return_list
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
# 1.读取数据
file_rdd = sc.textFile("SougouQ.txt")
# 2.对数据进行切分 '\t'
split_rdd = file_rdd.map(lambda x: x.split("\t"))
# 3.因为要做多个需求,split_rdd作为基础rdd会被多次使用
split_rdd.persist(StorageLevel.DISK_ONLY)
# TODO: 需求1:用户搜索的关键词分析
# 主要分析热点词
# 将所有的搜索内容取出
# split_rdd.takeSample(True, 3)
context_rdd = split_rdd.map(lambda x: x[2])
# 对搜索的内容进行分词分析
words_rdd = context_rdd.flatMap(context_jieba)
# print(words_rdd.collect())
# 将分词不合理的进行组合
filtered_rdd = words_rdd.filter(filter_words)
final_words_rdd = filtered_rdd.map(append_word)
# 对单词进行分组聚合排序,取前5
result1 = final_words_rdd.reduceByKey(lambda a,b: a + b).sortBy(lambda x: x[1], ascending=False, numPartitions=1).task(5)
print("需求1结果",result1)
# TODO:需求2:用户和关键词组合分子
user_content_rdd = split_rdd.map(lambda x: (x[1], x[2]))
# 对用户的搜索内容进行分词,分词后和用户ID在此组合
user_word_with_rdd = user_content_rdd.flatMap(extract_user_and_word)
# 对内容进行分组聚合排序取前5
result2 = user_word_with_rdd.reduceByKey(lambda a, b: a + b).sortBy(lambda x: x[1], ascending=False, numPartitions=1).take(5)
print("需求2结果:", result2)
# TODO:需求3:热门搜索时间段分析
# 取出来所有的时间
time_rdd = split_rdd.map(lambda x: x[0])
# 对时间进行处理,只保留小时的精度即可
hour_with_one_rdd = time_rdd.map(lambda x: (x.split(":")[0], 1))
# 分组聚合排序
result3 = hour_with_one_rdd.reduceByKey(add).sortBy(lambda x: x[1], ascending=False, numPartitions=1).collect()
print("需求3结果:", result3)
如果要上传集群运行:master部分删除,读取的文件路径改为hdfs
查看集群资源数
查看cpu核心数:cat /proc/cpuinfo | grep processor | wc -l
查看内存有多大:free -g
5.共享变量
5.1 广播变量
如果将本地list对象标记为广播变量对象,当需要发送该数据时会给每个Executor发一份数据,而不是每个分区处理线程发,节省内存
# 1.将本地list标记成广播变量
broadcast = sc.broadcast(stu_info_list)
# 2.使用广播变量,从broadcast对象中取出本地list对象
value = broadcast.value
# 先放入broadcast内部,然后从broadcast内部取出来用,中间传输是broadcast对象
# 只要中间传输是broadcast对象,spark就会留意,只会给每个Executor发一份
5.2 累加器
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],2)
# spark提供的累加器变量,参数是初始值
acmlt = sc.accumulator(0)
def map_func(data):
global acmlt
acmlt += 1
rdd2 = rdd.map(map_func)
# 如果不缓存,rdd2被销毁后,rdd3又会执行一遍rdd2,是的累加结果变为20
rdd2.cache()
rdd2.collect()
rdd3 = rdd2.map(lambda x: x)
rdd3.collect()
print(acmlt)
5.3 综合案例
数据示例:
hadoop spark # hadoop ! %
! mapreduce
需求:
1.对正常的单词计数
2.统计特殊字符出现多少个
特殊字符包括:“,”, “.”, “!”, “#”, “$”, “%”
import re
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
conf = SparkConf().setMaster("local[*]").setAppName("test")
sc = SparkContext(conf=conf)
# 1.读取数据文件
file_rdd = sc.textFile("accumulator_broadcast_data.txt")
# 特殊字符的list定义
abnormal_char = [",", ".", "!", "#", "$", "%"]
# 2.将特殊字符list 包装成广播变量
broadcast = sc.broadcast(abnormal_char)
# 3.对特殊字符出现次数做累加,累加使用累加器
acmlt = sc.accumulator(0)
# 4.数据处理,先处理数据的空行,在python中有内容就是True, None就是False
lines_rdd = file_rdd.filter(lambda line: line.strip())
# 5.去除前后的空格
data_rdd = lines_rdd.map(lambda line: line.strip())
# 6.对数据进行切分,按照正则表达式切分,因为空格分隔符某些单词之间是两个或多个空格
# 正则表达式 \s+ 表示不确定多少个空格,至少一个空格
words_rdd = data_rdd.flatMap(lambda line: re.split("\s+",line))
# 7.当前words_rdd中有常见单词 也有特殊符号
# 现在过滤数据,保留正常单词,在过滤过程中对特殊符号做计数
def filter_func(data):
global acmlt
# 去除广播数量中存储的特殊字符list
abnormal_chars = broadcast.value
if data in abnormal_chars:
# 表示这个字符是特殊字符
acmlt += 1
return False
return True
normal_words_rdd = words_rdd.filter(filter_func)
# 8.正常单词的计数逻辑
result_rdd = normal_words_rdd.map(lambda x: (x, 1)).reduceByKey(lambda a,b: a + b)
print("正常单词计数结果:",result_rdd.collect())
print("特殊字符数量",acmlt)
6.Spark内核调度
6.1 DAG
DAG:有向无环图,根据RDD的依赖关系构建的执行流程图
Job和Action
每个Action都会产生1个DAG,并在程序运行中都会生成一个Job
DAG和分区
如果分区之间有数据交互,则得到一个还有分区关系的DAG
6.2 DAG的宽窄依赖和阶段划分
在SparkRDD前后之间的关系分为:宽依赖、窄依赖
窄依赖:父RDD的一个分区,全部将数据发给子RDD的一个分区
宽依赖:父RDD的一个分区,将数据发给子RDD的多个分区
宽依赖还有一个别名shuffle
阶段划分
对于Spark来说,会根据DAG,按照宽依赖,划分不同的DAG阶段
划分依据:从后向前,遇到宽依赖就划分出一个阶段,称为stage。stage的内部一定是窄依赖
6.3 内存迭代计算
基于带分区的DAG以及阶段划分,在一个stage内每条线由一个task线程完成,是纯内存计算,形成内存计算管道
sprak默认收到全局并行度的限制,除了个别算子有特殊分区情况,大部分的算子,都会遵循全局并行度的要求,来规划自己的分区数
6.5 Spark并行度
spark的并行:在同一时间内,有多个task在同时运行
并行度:并行能力的设置
比如并行度为6,就要6个task并行跑,rdd的分区就被规划为6个
设置并行度
优先级从高到低:代码中、客户端提交参数中、配置文件中、默认(1)
全局并行度配置的参数:
spark.default.parallelism
配置文件中:
conf/spark-defaults.conf中设置
spark.default.parallelism 100
在客户端提交参数中:
bin/spark-submit --conf "spark.default.parallelism=100"
在代码中:
conf = SparkConf()
conf.set("spark.default.parallelism", "100")
旨在代码中写,算子(不推荐)
repartition算子
coalesce算子
partitionBy算子
集群中如何规划并行度
结论:设置为CPU总核心的2-10倍
为什么至少2倍?
cpu的一个核心同一时间只能干一件事
在100个核心的情况下,设置100个并行就能让cpu 100%出力
这种设置,如果task的压力不均衡,某个task先执行完,就导致某个cpu核心空闲
目的是最大化利用集群资源
6.5 Spark的任务调度
Spark的任务,由Driber进行调度,这个工作包含:
1.逻辑DAG生成
2.分区DAG生成
3.Task划分
4.将Task分配给Executor并监控其工作
spark程序的调度流程:
1.Driver被创建
2.构建SparkContext
3.基于DAG Scheduler构建逻辑Task分配
4.基于TaskScheduler将逻辑Task分配到各个Executor上,并监控它们
5.Worker(Executor)被TaskScheduler管理监控,听从指令干活并定期汇报进度
其中1-4是Driver的工作,5是worker的工作
Driver内两个组件
DAG调度器:将逻辑的DAG图进行处理,最终得到逻辑上的Task划分
Task调度器:基于DAG Scheduler的产出,来规划这些逻辑的task,应该在哪些物理的executor上运行,以及监控管理它们的运行