RDD详解
为什么需要RDD
分布式计算需要:
- 分区控制
- shuffle控制(不同分区进行数据传输)
- 数据存储、序列化、发送
- 数据计算API
- 其他一系列功能
这些功能不能简单地通过python内置的本地集合对象(list、dict等)来实现,在分布式框架中,需要有一个统一的数据抽象对象,来实现上述分布式计算所需功能。
上述抽象对象:RDD
什么是RDD
Resilient Distributed Dataset,弹性分布式数据集,spark中最基本的数据抽象,代表一个不可变、可分区、里面元素可并行计算的集合
Resilient:数据可存放在内存或磁盘中
Distributed:数据是分布式存储的,可用于分布式计算(数据跨机器存储、跨进程存储;本地集合:数据全部在一个进程内部)
Dataset:数据集合,用来存放数据
RDD 5大特性
RDD数据结构内部有5大特性,前3个特性每个RDD都具备,后2个特性可选
- RDD有分区
- 计算方法作用到每一个分区(分片)之上
- RDD之间相互依赖
- KV型RDD可以有分区器
- RDD分区数据的读取尽量靠近数据所在地(数据不动,数据在哪,就在哪个机器上计算)
RDD有分区
分区是RDD数据存储的最小单位
一份RDD数据,本质上是分隔成了多个分区
RDD方法会作用在所有分区上
map方法体现在了每一个分区上
glom方法,将一个分区的数据转换为相同类别的内存数组进行处理,分区不变
RDD之间有依赖关系
KV型RDD可以有分区器
KV型RDD:RDD内存储的数据是二元元组,如(“hadoop”,3)
默认分区器:Hash分区规则,可以通过rdd.partitionBy方法手动设置一个分区器
这个特性只是可能的,因为并不是所有RDD都是KV型
RDD分区尽量靠近数据所在服务器
初始RDD(读取数据的时候)规划的时候,分区尽量规划到存储数据所在的服务器上
因此,尽量走本地读取,避免网络读取
本地读取:Executor所在服务器,也是一个DataNode,同时这个DataNode上有它要读的数据,因此可以直接读取硬盘,而不需要走网络
网络读取:读取数据时,需要经历网络传输才能读取到
因此,spark会在确保并行计算能力的前提下,尽量本地读取(非100%)
wordCount中RDD
总结
如何正确理解RDD
弹性分布式数据集,分布式计算的实现载体(数据抽象)
RDD 5大特性
- 可分区
- 方法应用到每一个分区
- RDD相互依赖
- KV型RDD可以有分区器
- RDD构建尽量靠近数据
RDD编程入门
程序执行入口–SparkContext对象
Spark RDD编程的程序入口对象是SparkContext对象(不论何种语言)
只有构建出SparkContext,基于sc才能执行后续的API调用和计算
本质上sc对编程来说,主要功能就是创建出第一个RDD出来
RDD创建
RDD创建的两种方式:
- 通过并行化集合创建(本地对象 转 分布式RDD)
- 读取外部数据源(读取文件)
并行化创建
读取文件创建
通过textFile API 读取本地数据或hdfs数据
sc.textFile(para1, para2)
para1必填,文件路径
para2可选,最小分区数(spark有自己的判断,允许范围内,para2有效,允许范围外,para2无效)
wholeTextFile
读取文件的API,适用于读取一堆小文件
RDD算子
算子
分布式集合对象上的API,称为算子
本地对象的API叫方法、函数
算子分类
- Transformation:转换算子
- Action:动作(行动)算子
transformation转换算子
定义:返回值仍然是一个RDD,称为转换算子
特性:lazy懒加载,如果没有action算子,则transformation算子不工作
action动作算子
定义:返回值不是RDD的就是action算子
区分
transformation转换算子相当于构建执行计划,action动作算子相当于执行计划
常用transformation转换算子
map算子
将RDD的数据一条一条处理(处理逻辑:基于map算子中接受的处理函数),返回新的RDD
rdd.map((func)
# func: f(T)->U
flatMap算子
对RDD执行map操作,然后进行解除嵌套操作
# 嵌套list
lst = [[1,2,3],[4,5,6],[7,8,9]]
# 解除嵌套
lst = [1,2,3,4,5,6,7,8,9]
flatMap传入参数和map一致,就是给map逻辑用的,在map逻辑上解除嵌套
reduceByKey算子
针对KV型RDD,自动按照key分组,根据提供的聚合逻辑,完成组内数据(value)的聚合操作
rdd.reducebyKey(func)
# func: (V,V)->V
# 接受两个类型一致的传入参数,返回值类型也和传入一致
key相同的分为一组,再根据value聚合
mapValues算子
针对二元元组RDD,对其元组内部的value执行map操作
rdd.mapValues(func)
# func: (V)->V
# 只针对value进行处理
groupBy算子
对RDD数据进行分组
rdd.groupBy(func)
# func: (T)->K
# 拿到返回值后,将相同返回值放入一个组中
# 分组完成后,每个组是一个二元元组,key是返回值,同组的数据放入迭代器中作为value
[('b',[('b',1), ('b', 1), ('b',1)])('a',[('a',1),('a',1)])]
key是返回值,value是同组的数据
filter算子
过滤想要的数据,进行保留
rdd.filter(func)
# func: (T)->bool
# 传入的参数任意类型,返回值必须是布尔型(真True、假False)
distinct算子
对RDD进行去重,返回新RDD
rdd.distinct(参数) # 参数为去重分区数量,按几个分区进行去重,一般不传
union算子
将2个RDD合并为1个RDD并返回
rdd.union(other_rdd)
只合并(不同类型的RDD也可以合并),不去重
join算子
对2个RDD进行join(可实现SQL的内、外连接)
join算子只能用于二元元组,按key进行关联
rdd.join(other_rdd) # 内连接
rdd.leftOuterJoin(other_rdd) # 左外连接
rdd.rightOuterJoin(other_rdd) # 右外连接
intersection算子
求2个RDD的交集,返回新RDD
rdd.intersection(other_rdd)
glom算子
将RDD数据加上嵌套,嵌套按分区进行
rdd.glom()
[[1,2,3,4],[5,6,7,8,9]]
groupByKey算子
针对KV型RDD,自动按照key分组
rdd.groupByKey()
分组后只保留同组的value
sortBy算子
对RDD数据进行排序,基于指定的排序依据
rdd.sortBy(func, ascending = False, numPartitions=1)
# func: (T)->U:按RDD中哪个数据进行排序,如lambda x:x[1]表示按照RDD中第二列元素进行排序
# ascending = True 按升序排序,False 按降序排序
# numPartitions 用多少分区排序
sortByKey算子
针对KV型RDD,按key进行排序
sortByKey(ascending=True,numPartitions=None,keyfunc=<function RDD.<lambda>>)
常用算子案例
将案例提交到YARN集群中运行
pycharm中直接执行
import os
os.environ["HADOOP_CONF_DIR"]="/export/server/hadoop/etc/hadoop"
rdd = sc.textFile("hdfs://node1:8020/input/order.text") # 集群中文件须使用hdfs文件
# 如果运行的文件依赖其他py文件,可以通过设置属性来指定依赖代码
conf.set("spark.submit.pyFiles","defs.py") # conf.set(key,value)
通过服务器上spark-submit提交到集群运行
–py-files可以指定以来的其他py代码,支持单py文件或zip压缩包
/export/server/spark/bin/spark-submit --master yarn --py-Files ./defs.py ./main.py
常用action算子
countByKey算子
统计key出现的次数,一般适用于KV型RDD
collect算子
将RDD各个分区内的数据统一到driver中,形成一个list对象
rdd.collect()
- 将RDD各个分区的数据都拉取到driver
- 拉取前应该了解到数据集不大
- 太大的数据集会撑爆内存
reduce算子
对RDD数据集按照传入逻辑进行聚合
rdd.reduce(func)
# func : (T,T)->T
rdd.parallelize(range(1,10))
print(rdd.reduce(lambda a,b:a+b))
fold算子[很少用]
和reduce一样,接受传入逻辑进行聚合;聚合带有初始值
- 分区内聚合
- 分区间聚合
[[1,2,3], [4,5,6], [7,8,9]]
初始值为10
分区1聚合后得到10+1+2+3 = 16
分区2聚合后得到10+4+5+6 = 25
分区3聚合后得到10+7+8+9 = 34
rdd = sc.parallelize(range(1,10),3)
print(rdd.fold(10,lambda a,b:a+b))
最终聚合结果为10 + 16 +25 +34 =85
first算子
取出RDD的第一个元素
rdd.parallelize([3,2,1]).first()
3
take算子
取RDD的前N个元素,组成list返回
rdd.parallelize([1,2,3,4,5,6,7,8,9]).take(5)
[1,2,3,4,5]
top算子
对RDD数据集进行降序排序,取前N个
rdd.parallelize([1,2,3,4,5,6,7,8,9]).top(5)
[9,8,7,6,5]
count算子
计算RDD有多少条数据,返回值是一个数字
rdd.parallelize([1,2,3,4,5,6]).count()
6
takeSample算子
随机抽样RDD的数据
takeSample(bool,takeSampleNum,seed)
- bool为真:允许取同一个数据;bool为假,不允许取同一个数据
- 采样(抽样)数量
- 随机种子(一般不给,spark会自动给随即种子)
rdd = sc.parallelize([1,1,1,1,1,1,1,1],1)
print(rdd.takeSample(True,8))
takeOrdered算子
对RDD排序取前N个
rdd.takeOrdered(para1, para2)
# para1 取N个数据
# para2 对排序数据进行更改(不更改数据本身,只修改到临时排序的数值)
# 默认升序,需要到倒序可以使用para2重排序
foreach算子
对RDD的每一个元素,执行提供的逻辑操作(同map),不过没有返回值
rdd.foreach(func)
saveAsTextFile算子
将RDD数据接入到文本文件中
支持本地写出hdfs等文件系统
小注意
action算子中:
- foreach
- saveAsTextFile
这两个算子是分区(Executor)直接执行,跳过Driver
其余action算子都会将结果发送给Driver
mapPartitions算子
map算子每次处理一个元素
mapPartitions算子一次处理一个分区的数据,作为一个迭代器(一次性list)对象传入过来(一次处理一个迭代器)
foreachPartition算子
类似foreach,一次处理一整个分区
foreachPartition就是个没有返回值的mapPartitions
partitionBy算子
对RDD进行自定义分区操作(默认hash分区)
rdd.partitionBy(para1, para2)
para1 重新分区后的分区数
para2 自定义分区规则,返回值为int,分区编号[0,分区数-1]
repartition算子
对RDD的分区执行重新分区(仅数量上)
rdd.repartition(N)
分区须谨慎:一般情况下spark代码中除了全局排序设置为1个分区外,通常所有api中的分区相关代码都不太理会
分区影响:
- 影响并行计算(内存迭代的并行管道数量)
- 分区的增加极可能导致shuffle
repartition api最好不用,用也是减少分区(方便全局排序)
groupByKey算子和reduceByKey算子区别
如果对数据执行 分组+聚合 ,reduceByKey性能>>>groupByKey+聚合逻辑
groupByKey只可以分组;reduceByKey除了可以分组,还可以聚合
使用groupByKey就是先分组,后聚合;reduceByKey先预聚合,再分区,再聚合,减少IO
总结
RDD创建的几种方式
- 并行化集合的方式(本地集合转分布式集合)
- 读取数据的方式(TextFile、WholeTextFile)
RDD查看分区数
调用getNumPartitions api查看分区数(返回int)
transformation和action算子的区别
transformation转换算子的返回值还是RDD,action算子的返回值不是RDD
transformation转换算子懒加载,碰到action算子才会执行
哪两个action算子不经过Driver直接执行输出
foreach和saveAsTextFile,直接由executor执行输出
groupByKey和reduceByKey区别
groupByKey算子只有分组功能,没有聚合逻辑,需自行补充聚合逻辑
reduceByKey算子支持分组聚合,先预聚合、再分组、再聚合,效率更高,传输IO小
mapPartitions和foreachPartition的区别
mapPartitions是一个转换算子,有返回值
foreachPartition是一个action算子,没有返回值
两个算子都是一次处理一整个分区的数据
分区操作的注意项
尽量不要增加分区(分区划分好后),可能破坏内存迭代的计算管道
RDD持久化
RDD过程数据
RDD之间进行相互迭代的计算(Transformation转换),执行开始后,新RDD产生,旧RDD消失
RDD的数据是过程数据,处理过程中存在,处理完成则清除
最大化利用资源,没用的RDD就会从内存中被清除,给后续的计算腾出空间
RDD3有两次使用,但第一次使用后,RDD3就被清除了;第二次用的时候的RDD3从RDD1重新执行构建出来
RDD缓存
针对某个或某些需要重复使用的RDD进行持久化优化,缓解整条链的重建
RDD缓存技术:spark提供缓存api,通过调用api,将指定的RDD数据保留再内存或硬盘上
- 缓存技术可以将过程RDD数据持久化到内存或者硬盘上
- 这个保存在设定上认为不安全
设计上认为有丢失的风险
缓存的特点:保留RDD之间的血缘(依赖)关系
缓存丢失:
- 缓存不安全,断电、计算任务不足等情况下,清理缓存给计算让路
- 硬盘有可能损坏
RDD checkpoint
checkpoint技术也是将RDD数据保存起来,但是仅支持硬盘存储
- 设计上认为是安全的(设计上认为不会丢失)
- 不保留血缘关系
RDD数据被checkpoint保存到HDFS中
缓存与checkpoint对比:
- checkpoint不管分区数量多少,风险一样;缓存分区越多,风险越高
- checkpoint支持写入hdfs,缓存不行,hdfs是高可靠存储,checkpoint认为更安全
- checkpoint不支持写入内存,缓存可以,缓存如果写入内存中,性能更好
- checkpoint因为设计上认为安全,所以不保留血缘关系;缓存设计上认为不安全,所以保留血缘关系
缓存:轻量级的、临时的、即使重新计算也不太费事的
checkoint:体量大的、计算费事的
总结
cache和checkpoint的区别
cache轻量化保存RDD数据,可存储在内存和一泡尿,是分散存储,设计上认为不安全,因此保留血缘
checkpoint重量级保存RDD数据,集中存储,只能存储硬盘上(hdfs),设计上认为是安全的,因此不保留血缘
cache和checkpoint性能对比
cache分散存储,各个executor并行执行,效率高,可以保存到内存中,更快,性能更好
checkpoint集中存储,涉及到网络IO,比较慢,但是存储到HDFS上更加安全
spark案例联系
搜索引擎日志分析
数据提供:搜狗实验室
网址:http://www.sogou.com/labs/resource/q.php
数据格式:搜索时间 用户ID 搜索内容 URL返回排名 用户点击顺序 用户点击的URL
提交到集群运行
总结
分词库
jieba
为什么所有服务器都要装jieba
yarn以集群运行,executor可以在所有服务器上运行,因此每台服务器都要有jieba库支撑
尽量提高任务计算资源
计算CPU核心和内存量
通过–executor-memory指定executor内存;
通过–executor-cores指定executor核心数
通过–num-executors指定总executor数量