传送门:
RDD之间进行相互迭代计算(Transformation的转换),当执行开启后,新RDD的生成,代表老RDD的消失。RDD的数据是过程数据,只在处理的过程中存在。一旦处理完成,就不见了。
这个特性可以最大化的利用资源,老旧RDD没用了就从内存中清理,给后续的计算腾出内存空间。
如上图,rdd3被2次使用,第一次使用之后,其实RDD3就不存在了
第2次用的时候,只能基于RDD的血缘关系,从RDD1重新执行,构建出RDD3,供RDD5使用
上述的场景肯定要执行优化,优化就是:RDD3如果不消失,那么RDD1→RDD2→RDD3
,这个链条就不会执行2次,或者更多次。此时,用到了RDD的缓存技术。RDD的缓存技术: Spark提供了缓存API,可以让我们通过调用API,将指定的RDD数据保留在内存或者硬盘上。
缓存API:
但是,这个保存在设定上是认为不安全的。
缓存的数据在设计上是认为有丢失风险的。所以,一旦缓存丢失,可以基于RDD的血缘关系记录,重新计算这个RDD数据。缓存必须保留被缓存RDD的前置"血缘关系"。
缓存如何丢失:
➊在内存中的缓存是不安全的, 比如断电\计算任务内存不足,把缓存清理给计算让路
➊硬盘中因为硬盘损坏也是可能丢失的.
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
import time
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
rdd1 = sc.textFile('../data/input/words.txt')
rdd2 = rdd1.flatMap(lambda x: x.split(' '))
rdd3 = rdd2.map(lambda x: (x, 1))
# 缓存到内存中
rdd3.cache()
rdd4 = rdd3.reduceByKey(lambda a, b: a + b)
print(rdd4.collect())
rdd5 = rdd3.groupByKey()
rdd6 = rdd5.mapValues(lambda x : sum(x))
print(rdd6.collect())
# 主动清理缓存
rdd3.unpersist()
[('hadoop', 1), ('hello', 3), ('spark', 1), ('flink', 1)]
[('hadoop', 1), ('hello', 3), ('spark', 1), ('flink', 1)]
CheckPoint技术,也是将RDD的数据保存起来。但是,它仅支持硬盘存储。并且:
对比缓存,CheckPoint的RDD数据保存是集中收集存储。如图,CheckPoint存储RDD数据,是集中收集各个分区数据进行存储,而缓存是分散存储。
注意:
- CheckPoint是一种重量级的使用,也就是RDD的重新计算成本很高的时候,我们采用CheckPoint比较合适。或者数据量很大,用CheckPoint比较合适。如果数据量小,或者RDD重新计算是非常快,用CheckPoint没啥必要,直接缓存即可。
- Cache和CheckPoint两个API都不是Action类型。所以,想要它俩工作,必须在后面接上Action。接上Action的目的,是让RDD有数据,而不是为了让checkPoint和cache工作。
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
# 2.告知Spark,开启Checkpoint功能
sc.setCheckpointDir('hdfs://node1:8020/test/output/ckp')
rdd1 = sc.textFile('../data/input/words.txt')
rdd2 = rdd1.flatMap(lambda x: x.split(' '))
rdd3 = rdd2.map(lambda x: (x, 1))
# 调用Checkpoint API;保存数据即可
rdd3.checkpoint()
rdd4 = rdd3.reduceByKey(lambda a, b: a + b)
print(rdd4.collect())
rdd5 = rdd3.groupByKey()
rdd6 = rdd5.mapValues(lambda x : sum(x))
print(rdd6.collect())
[('hadoop', 1), ('hello', 3), ('spark', 1), ('flink', 1)]
[('hadoop', 1), ('hello', 3), ('spark', 1), ('flink', 1)]
Cache和Checkpoint区别
- Cache是轻量化保存RDD数据,可存储在内存和硬盘,是分散存储,设计上数据是不安全的(保留RDD血缘关系)
- CheckPoint是重量级保存RDD数据,是集中存储,只能存储在硬盘(HDFS)上,设计上是安全的(不保留RDD血缘关系)
Cache和CheckPoint的性能对比?
Cache性能更好,因为是分散存储,各个Executor并行执行,效率高,可以保存到内存中(占内存),更快。
- CheckPoint比较慢,因为是集中存储,涉及到网络IO,但是存储到HDFS上更加安全(多副本)
使用搜狗实验室提供【用户查询日志(SogouQ)】数据,使用Spark框架,将数据封装到RDD中进行业务数据处理分析。
数据格式:下载地址
每一列分别为访问时间、用户ID、查询词、该URL在返回结果中的排名、用户点击的顺序号、用户点击的URL
案例需求:三个需求。
#!usr/bin/env python
# -*- coding:utf-8 -*-
"""
搜索引擎日志分析
- 用户搜索的关键词分析
- 用户和关键词的组合分析
- 热门搜索时间段分析
"""
import jieba
from pyspark import SparkContext, SparkConf
from pyspark.storagelevel import StorageLevel
from utils import context_jieba, filter_words, transfer_words
if __name__ == '__main__':
# 0.构建SparkContext对象
conf = SparkConf().setAppName(' ').setMaster('local[*]')
sc = SparkContext(conf=conf)
# 1.读取数据文件
file_rdd = sc.textFile('../../data/input/SogouQ.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:用户搜索的关键词分析——主要分析热点词
# 将搜索内容取出来
context_rdd = split_rdd.map(lambda x: x[2])
# 对搜索内容进行分词,得到分词后的结果
words_rdd = context_rdd.flatMap(content_jieba)
# 对分词后的异常内容进行处理——先将不要的过滤掉,再将剩余内容替换成完成的内容
# 将关键词中的谷、帮、客进行过滤掉
filter_rdd = words_rdd.filter(filter_words)
# 将关键词进行替换
final_word_rdd = filter_rdd.map(transfer_words)
# 对单词进行分组、聚合、降序排序,找出前五个热点词
result1 = final_word_rdd.reduceByKey(lambda a, b: a + b). \
sortBy(lambda x: x[1], ascending=False, numPartitions=1).take(5)
print("需求1的结果:", result1)
需求1的结果: [('scala', 2310), ('hadoop', 2268), ('博学谷', 2002), ('传智汇', 1918), ('itheima', 1680)]
需求二:用户和关键词组合分析,代码解析:
#!usr/bin/env python
# -*- coding:utf-8 -*-
"""
搜索引擎日志分析
- 用户搜索的关键词分析
- 用户和关键词的组合分析
- 热门搜索时间段分析
"""
import jieba
from pyspark import SparkContext, SparkConf
from pyspark.storagelevel import StorageLevel
from utils import content_jieba, filter_words, transfer_words, extract_userid_and_word
if __name__ == '__main__':
# 0.构建SparkContext对象
conf = SparkConf().setAppName(' ').setMaster('local[*]')
sc = SparkContext(conf=conf)
# 1.读取数据文件
file_rdd = sc.textFile('../../data/input/SogouQ.txt')
# 2.对数据按照“\t”进行切分
split_rdd = file_rdd.map(lambda x: x.split('\t'))
# 3.split_rdd作为基础rdd,要多次使用,因此保存在硬盘中
split_rdd.persist(StorageLevel.DISK_ONLY)
# TODO:需求2:用户和关键词组合分析——每个用户的搜索热点词
# 抽取用户和关键词
user_content_rdd = split_rdd.map(lambda x: (x[1], x[2]))
# 对用户的搜索内容进行分词,分词后与用户ID再次组合
user_word_rdd = user_content_rdd.flatMap(extract_userid_and_word)
# 对内容进行分组、聚合、排序,求前5
result2 = user_word_rdd.reduceByKey(lambda a, b: a + b). \
sortBy(lambda x: x[1], ascending=False, numPartitions=1). \
take(5)
print("需求2的结果:", result2)
需求2的结果: [('6185822016522959_scala', 2016), ('41641664258866384_博学谷', 1372), ('44801909258572364_hadoop', 1260), ('7044693659960919_数据', 1120), ('7044693659960919_仓库', 1120)]
需求三:热门搜索时间段分析,代码解析:
#!usr/bin/env python
# -*- coding:utf-8 -*-
"""
搜索引擎日志分析
- 用户搜索的关键词分析
- 用户和关键词的组合分析
- 热门搜索时间段分析
"""
import jieba
from pyspark import SparkContext, SparkConf
from pyspark.storagelevel import StorageLevel
from utils import content_jieba, filter_words, transfer_words, extract_userid_and_word
if __name__ == '__main__':
# 0.构建SparkContext对象
conf = SparkConf().setAppName(' ').setMaster('local[*]')
sc = SparkContext(conf=conf)
# 1.读取数据文件
file_rdd = sc.textFile('../../data/input/SogouQ.txt')
# 2.对数据按照“\t”进行切分
split_rdd = file_rdd.map(lambda x: x.split('\t'))
# 3.split_rdd作为基础rdd,要多次使用,因此保存在硬盘中
split_rdd.persist(StorageLevel.DISK_ONLY)
# TODO:需求3:热门搜索时间段分析
# 取出所有的时间
time_rdd = split_rdd.map(lambda x: x[0])
# 对时间进行处理,只保留小时精度即可
hour_with_one_rdd = time_rdd.map(lambda x: (x.split(':')[0], 1))
# 分组、聚合、排序、求前5
# lambda a, b: a + b ==> from operator import add
result3 = hour_with_one_rdd.reduceByKey(lambda a, b: a + b). \
sortBy(lambda x: x[1], ascending=False, numPartitions=1).take(5)
print("需求3的结果:", result3)
需求3的结果: [('20', 3479), ('23', 3087), ('21', 2989), ('22', 2499), ('01', 1365)]
本地模式与集群模式相比,需要修改如下部分:
main.py
:
#!usr/bin/env python
# -*- coding:utf-8 -*-
"""
搜索引擎日志分析
- 用户搜索的关键词分析
- 用户和关键词的组合分析
- 热门搜索时间段分析
"""
import jieba
from pyspark import SparkContext, SparkConf
from pyspark.storagelevel import StorageLevel
from utils import content_jieba, filter_words, transfer_words, extract_userid_and_word
if __name__ == '__main__':
# 0.构建SparkContext对象
conf = SparkConf().setAppName('test')
sc = SparkContext(conf=conf)
# 1.读取hdfs文件
file_rdd = sc.textFile('hdfs://node1:8020/test/input/SogouQ.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:用户搜索的关键词分析——主要分析热点词
# 将搜索内容取出来
context_rdd = split_rdd.map(lambda x: x[2])
# 对搜索内容进行分词,得到分词后的结果
words_rdd = context_rdd.flatMap(content_jieba)
# 对分词后的异常内容进行处理——先将不要的过滤掉,再将剩余内容替换成完成的内容
# 将关键词中的谷、帮、客进行过滤掉
filter_rdd = words_rdd.filter(filter_words)
# 将关键词进行替换
final_word_rdd = filter_rdd.map(transfer_words)
# 对单词进行分组、聚合、降序排序,找出前五个热点词
result1 = final_word_rdd.reduceByKey(lambda a, b: a + b). \
sortBy(lambda x: x[1], ascending=False, numPartitions=1).take(5)
print("需求1的结果:", result1)
# TODO:需求2:用户和关键词组合分析——每个用户的搜索热点词
# 抽取用户和关键词
user_content_rdd = split_rdd.map(lambda x: (x[1], x[2]))
# 对用户的搜索内容进行分词,分词后与用户ID再次组合
user_word_rdd = user_content_rdd.flatMap(extract_userid_and_word)
# 对内容进行分组、聚合、排序,求前5
result2 = user_word_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))
# 分组、聚合、排序、求前5
# lambda a, b: a + b ==> from operator import add
result3 = hour_with_one_rdd.reduceByKey(lambda a, b: a + b). \
sortBy(lambda x: x[1], ascending=False, numPartitions=1).take(5)
print("需求3的结果:", result3)
utils.py
:
#!usr/bin/env python
# -*- coding:utf-8 -*-
"""
工具函数
"""
import jieba
def content_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 transfer_words(data):
"""修订某些关键词内容"""
if data == "传智播": data = "传智播客"
if data == "院校": data = "院校帮"
if data == "博学": data = "博学谷"
return (data, 1)
def extract_userid_and_word(data):
"""
将搜索内容进行分词,分词后与用户ID进行组合
:param data: 传入内容是(用户ID,搜索内容)
:return:
"""
user_id = data[0]
user_content = data[1]
# 对搜索内容进行分词
words = content_jieba(user_content)
# 对单词进行过滤与替换,并拼接
result_list = list()
for word in words:
if filter_words(word):
result_list.append((user_id + '_' + transfer_words(word)[0], 1))
return result_list
默认参数的集群提交:
[root@node1 example]# cd /tmp/pycharm_project_189/01_RDD/example/
[root@node1 example]# /export/server/spark/bin/spark-submit --master yarn --py-files utils.py main.py
22/06/24 16:33:47 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
需求1的结果: [('scala', 2310), ('hadoop', 2268), ('博学谷', 2002), ('传智汇', 1918), ('itheima', 1680)]
需求2的结果: [('6185822016522959_scala', 2016), ('41641664258866384_博学谷', 1372), ('44801909258572364_hadoop', 1260), ('7044693659960919_数据', 1120), ('7044693659960919_仓库', 1120)]
需求3的结果: [('20', 3479), ('23', 3087), ('21', 2989), ('22', 2499), ('01', 1365)]
榨干集群性能提交:
先查看集群资源有多少:
查看CPU有几核
(pyspark_env) [root@node1 example]# cat /proc/cpuinfo | grep processor | wc -l
查看内存有多大
(pyspark_env) [root@node1 example]# free -g
通过命令,计算得知,当前我集群3台服务器总共提供: 16G物理内存+6核心CPU的计算资源。
- 如何尽量提高任务计算的资源?
计算CPU核心和内存量,通过–executor-memory 指定executor内存,通过–executor-cores 指定executor的核心数,通过–num-executors 指定总executor数量
基于自身虚拟机所做的操作:
(pyspark_env) [root@node1 example]# /export/server/spark/bin/spark-submit --master yarn --py-files utils.py --executor-memory 1g --executor-cores 1 --num-executors 3 main.py
22/06/24 17:02:03 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
需求1的结果: [('scala', 2310), ('hadoop', 2268), ('博学谷', 2002), ('传智汇', 1918), ('itheima', 1680)]
需求2的结果: [('6185822016522959_scala', 2016), ('41641664258866384_博学谷', 1372), ('44801909258572364_hadoop', 1260), ('7044693659960919_数据', 1120), ('7044693659960919_仓库', 1120)]
需求3的结果: [('20', 3479), ('23', 3087), ('21', 2989), ('22', 2499), ('01', 1365)]
问题引出:有如下代码
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
# 本地list对象
stu_info_list = [(1, '张大仙', 11),
(2, '王晓晓', 13),
(3, '张甜甜', 11),
(4, '王大力', 11)]
score_info_rdd = sc.parallelize([
(1, '语文', 99),
(2, '数学', 99),
(3, '英语', 99),
(4, '编程', 99),
(1, '语文', 99),
(2, '编程', 99),
(3, '语文', 99),
(4, '英语', 99),
(1, '语文', 99),
(3, '英语', 99),
(2, '编程', 99)
])
def map_func(data):
id = data[0]
name = ''
# 匹配list对象与分布式rdd中的ID,来获得当前学生的姓名
for i in stu_info_list:
if id == i[0]:
name = i[1]
return (name, data[1], data[2])
# 4.完成用户id到用户名的映射
print(score_info_rdd.map(map_func).collect())
本地list对象(在Driver进程中),被发送到每个分区的处理线程上使用,也就是一个executor内,其实存放了2份一样的数据。executor是进程,进程内资源共享,这2份数据没有必要,造成了内存与网络IO的浪费。
解决方案:广播变量。如果将本地list对象标记为广播变量对象,那么当上述场景出现的时候,Spark只会给每个Executor来一份数据,而不是像原本那样,每一个分区的处理线程都来一份,节省内存。
如图,使用广播变量后,每个Executor只会收到一份数据集,内部的各个线程(分区)共享这一份数据集。
使用场景:本地集合对象和分布式集合对象rdd进行关联的时候,需要将本地集合对象封装为广播变量。这个通常用在本地集合对象占用内存不大的情况下。如果占用内存过大,需要分布式rdd与分布式rdd进行JOIN算子的关联。
可以节省:
- 网络IO的次数
- Executor的内存占用
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
# 2.将本地list对象标记为广播变量
# 本地list对象
stu_info_list = [(1, '张大仙', 11),
(2, '王晓晓', 13),
(3, '张甜甜', 11),
(4, '王大力', 11)]
broadcast = sc.broadcast(stu_info_list)
score_info_rdd = sc.parallelize([
(1, '语文', 99),
(2, '数学', 99),
(3, '英语', 99),
(4, '编程', 99),
(1, '语文', 99),
(2, '编程', 99),
(3, '语文', 99),
(4, '英语', 99),
(1, '语文', 99),
(3, '英语', 99),
(2, '编程', 99)
])
def map_func(data):
id = data[0]
name = ''
# 3. 使用广播变量,从broadcast对象中取出本地list对象即可
value = broadcast.value
# 匹配广播变量与分布式rdd中的ID,来获得当前学生的姓名
for i in value:
if id == i[0]:
name = i[1]
return (name, data[1], data[2])
# 4.完成用户id到用户名的映射
print(score_info_rdd.map(map_func).collect())
[('张大仙', '语文', 99), ('王晓晓', '数学', 99), ('张甜甜', '英语', 99), ('王大力', '编程', 99), ('张大仙', '语文', 99), ('王晓晓', '编程', 99), ('张甜甜', '语文', 99), ('王大力', '英语', 99), ('张大仙', '语文', 99), ('张甜甜', '英语', 99), ('王晓晓', '编程', 99)]
想要对map算子计算中的数据,进行计数累加。得到全部数据计算完后的累加结果。问题引出:代码如下
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2)
count = 0
def map_func(data):
global count
count += 1
print(count)
rdd.map(map_func).collect()
print(count)
1
2
3
4
5
1
2
3
4
5
0
两个分区,分别打印1到5。代码最后结果打印为0(由Driver进程打印)。
代码的问题在于:count来自driver对象,当在分布式的map算子中需要count对象的时候,driver会将count对象发送给每一个executor一份(复制发送),每个executor各自收到一个,在最后执行print(count)的时候,这个被打印的count依旧是driver中的那个,所以不管executor中累加到多少,都和driver这个count无关。
sc.accumulator(初始值)
累加器对象唯一和前面提到的count不同的是,这个对象可以从各个Executor中收集它们的执行结果,作用回自己身上。
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2)
# TODO:accumulator算子,构建累加器对象,这个对象可以从各个Executor中收集它们的执行结果
acmlt = sc.accumulator(0)
def map_func(data):
global acmlt
acmlt += 1
print(acmlt)
rdd.map(map_func).collect()
print(acmlt)
1
2
3
4
5
1
2
3
4
5
10
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2)
# TODO:accumulator算子,构建累加器对象,这个对象可以从各个Executor中收集它们的执行结果
acmlt = sc.accumulator(0)
def map_func(data):
global acmlt
acmlt += 1
# print(acmlt)
rdd2 = rdd.map(map_func)
rdd2.collect()
rdd3 = rdd2.map(lambda x: x)
rdd3.collect()
print(acmlt)
20
如上代码,第一次rdd2被action后,累加器值是10,然后rdd2就没有了。当rdd3构建出来的时候,是依赖rdd2的,rdd2没数据,那么rdd2就要重新生成。重新生成就导致累加器累加数据的代码再次被执行。所以代码的结果是20。
如何解决:加缓存或者checkPoint即可。
#!usr/bin/env python
# -*- coding:utf-8 -*-
from pyspark import SparkConf, SparkContext
if __name__ == '__main__':
# 1.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2)
# TODO:accumulator算子,构建累加器对象,这个对象可以从各个Executor中收集它们的执行结果
acmlt = sc.accumulator(0)
def map_func(data):
global acmlt
acmlt += 1
# print(acmlt)
rdd2 = rdd.map(map_func)
# 添加缓存
rdd2.cache()
rdd2.collect()
rdd3 = rdd2.map(lambda x: x)
rdd3.collect()
print(acmlt)
10
需求:
- 正常的单词进行单词计数
- 特殊字符统计出现有多少个
代码
#!usr/bin/env python
# -*- coding:utf-8 -*-
"""
统计:
- 正常单词进行单词计数
- 特殊字符统计出现有多少个
"""
from pyspark import SparkConf, SparkContext
import re
if __name__ == '__main__':
# 0.构建SparkContext对象
conf = SparkConf().setAppName('creat rdd').setMaster('local[*]')
sc = SparkContext(conf=conf)
# 1.读取数据文件
file_rdd = sc.textFile('../data/input/accumulator_broadcast_data.txt')
# 特殊字符的list定义
abnormal_char = [',', '.', '!', '#', '$', '%']
# 2.特殊字符list,包装成广播变量
broadcast = sc.broadcast(abnormal_char)
# 3.对特殊字符出现次数做累加,使用累加器
acmlt = sc.accumulator(0)
# 4.数据处理,先处理数据的空行
# 有内容返回True,None返回False
lines_rdd = file_rdd.filter(lambda line: line.strip())
# 5.去除前后空格
data_rdd = lines_rdd.map(lambda line: line.strip())
# 6.对数据进行切分,按照正则表达式切分,因为空格分隔符某些单词之间是两个或者多个空格
words_rdd = data_rdd.flatMap(lambda line: re.split('\s+', line))
# 7.当前words_rdd中有正常单词,也有特殊符号
# 过滤数据,保留正常单词用于做单词计数;在过滤的过程中对特殊符号做计数
def filter_func(data):
"""过滤数据,保留正常单词用于做单词计数;在过滤的过程中对特殊符号做计数"""
global acmlt
# 取出广播变量
abnormal_chars = broadcast.value
if data in abnormal_chars:
acmlt += 1
return False
else:
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)
正常单词计数结果: [('hadoop', 3), ('hive', 6), ('hdfs', 2), ('spark', 11), ('mapreduce', 4), ('sql', 2)]
特殊字符计数结果: 8
这部分内容在面试中经常被问到。
Spark的核心是根据RDD来实现的,Spark Scheduler则为Spark核心实现的重要一环,其作用就是任务调度。Spark的任务调度就是如何组织任务去处理RDD中每个分区的数据,根据RDD的依赖关系构建DAG,基于DAG划分Stage,将每个Stage中的任务发到指定节点运行。基于Spark的任务调度原理,可以合理规划资源利用,做到尽可能用最少的资源高效地完成任务计算。
以词频统计WordCount程序为例,DAG图为:
Spark官方在4040界面的DAG图为:
Action算子的作用是一个触发开关,会将action算子之前的一串rdd依赖链条执行起来。如图,我们前面写的搜索引擎日志分析案例中,三个Job的DAG为:
结论:
DAG是Spark代码的逻辑执行图,这个DAG的最终作用是为了构建物理上的Spark详细执行计划而生。所以,由于Spark是分布式(多分区)的,那么DAG和分区之间也是有关联的。
假设上述代码的全部RDD全部都在三个分区上执行,代码运行时就可以得到带有分区关系的DAG图。
Spark RDD前后之间的关系,分为:
简单的从图中来看:出现分叉就是宽依赖,否则,就是窄依赖。
宽窄依赖涉及到阶段的划分。对于Spark来说,会根据DAG按照宽依赖划分不同的DAG阶段。划分依据:从后向前,遇到宽依赖就划分出一个阶段,称之为stage。
如图,可以看到:在DAG中,基于宽依赖将DAG划分成了2个stage,在stage的内部一定都是:窄依赖。
如图,基于带有分区的DAG以及阶段划分,可以从图中得到逻辑上最优的task分配。一个task是由一个线程来具体执行。那么如上图,task1中rdd1、rdd2、rdd3的迭代计算,都是由一个task(线程完成)。如上图,task1、task2、task3,就形成了三个并行的内存计算管道,由三个线程并行工作。
注意:
- Spark默认受到全局并行度的限制,除了个别算子有特殊分区情况,大部分的算子都会遵循全局并行度的要求,来规划自己的分区数。
- Spark我们一般推荐只设置全局并行度,除了一些排序算子外,计算算子就让他默认开分区就可以了。如果中途修改分区,必然产生分叉,导致内存迭代管道变短,性能下降。
Spark的并行:在同一时间内,有多少个task在同时运行。比如设置并行度6,其实就是要6个task并行在跑。在有了6个task并行的前提下, rdd的分区就被规划成6个分区了。
可以在代码中和配置文件中以及提交程序的客户端参数中设置优先级从高到低:
全局并行度配置的参数:spark.default.parallelism
全局并行度是推荐设置,不要针对RDD改分区,可能会影响内存迭代管道的构建,或者会产生额外的Shuffle
结论:设置为CPU总核心的2~10倍。
比如集群可用CPU核心是100个,我们建议并行度是200~1000。
确保是CPU核心的整数倍即可,最小是2倍,最大一般10倍或更高(适量)均可
规划并行度,只看集群总CPU核数
Spark的任务,由Driver进行调度,这个工作包含:
如图,Spark程序的调度流程如图:
1,2,3,4是Driver的工作;5是Worker的工作
一般,将–num-executor 设置为服务器的数量
因为,一个服务器内的多个executor之间的任务交互,需要走本地回环网络IO。因此,一般一个机器开启一个executor就可以了,从而可以增加内存迭代计算的占比,缩减网络IO。应当关注的是Task任务数量与服务器的CPU数量一样