Spark核心编程

RDD 详解

为什么需要 RDD

分布式计算需要什么条件?

分区控制:将数据分区,分别处理
shuffle控制:实现不同设备间的关联数据通信
数据存储\序列化\发送
数据计算API

这些功能我们不能通过Python内置的本地集合对象实现(那不成自己写框架了嘛,费事)
因此我们需要有一个统一的数据抽象对象,来实现上述分布式计算所需的功能,这个就是 RDD

什么是 RDD

弹性分布式数据集(Resilient Distributed Dataset),Spark中最基本的数据抽象,代表一个不可变、可分区、所含元素可并行计算的集合。
dataset:数据集合,存数据
distributed:分布式存储,RDD的数据是跨机器存储的(跨进程)
resilient:弹性,数据可以存在内存或硬盘中,且可以动态扩容或缩容

RDD 五大特性

RDD数据结构有五个特性,其中前三个每个RDD都具备,后两个特征可能有但不确定

  1. 有分区
    分区是RDD存储的最小单位。RDD是逻辑概念,而分区是物理概念,一个RDD由多个分区构成
    在这里插入图片描述

  2. RDD的 方法作用在每个分区
    元素变大10倍的操作会作用在三个分区上
    在这里插入图片描述

  3. RDD之间有单向依赖关系
    RDD总是由一个RDD迭代到另一个RDD,对一个RDD的每次运算都会生成新的RDD
    RDD1->RDD2->RDD3->RDD4,构建成为DAG(有向无环图)
    当有分区数据丢失的时候,Spark会通过依赖关系重新计算,算出丢失的数据,而不是对RDD所有的分区进行重新计算
    在这里插入图片描述

  4. KV型RDD有分区器
    KV型RDD:存储的数据是二元元组的RDD,如(“hadoop”, 3)
    KV型RDD在分区时默认按Hash规则分区,key的哈希值相同的元组划分到同一个分区
    也可以手动设置分区器(rdd.partitionBy的方法来设置)
    在这里插入图片描述

  5. RDD分区规划会尽量靠近 数据 所在地
    RDD在规划时,分区会尽量规划到存储数据的服务器上
    这样就可以走本地存储,避免网络通信
    移动数据不如移动计算

WordCount分析(RDD视角)

在这里插入图片描述

总结

如何理解RDD?
RDD是一个抽象的概念,
存储上来说,是一组分布式分区的集合,分区的数量和存储位置是弹性的(可动态扩容,可内存可硬盘)
计算上来说,RDD是分布式计算的载体,计算会作用到RDD中的每个分区上

RDD的五大特点?
分区、计算雨露均沾、不同阶段的RDD单向依赖、KV型RDD有分区器、分区靠近数据

RDD 编程入门

程序执行入口SparkContext

无论何种语言,Spark RDD 编程的程序入口对象就是SparkContext,基于它才能执行后续的API调用和计算
本质上来说,SparkContext的主要功能就是创建第一个RDD出来
在这里插入图片描述

RDD的创建

  1. 并行化创建
    本地集合转为分布式RDD

    	rdd = sparkcontext.parallelize(集合对象,分区数)  # 集合对象,如list等
    
  2. 读取文件创建
    读取 本地HDFS 数据创建RDD对象

    	rdd = sparkcontext.textfile(文件路径,分区数)  # 分区数在Spark允许的范围内才有效
    	
    	rdd = sparkcontext.wholeTextfile(文件夹路径,分区数)  
    	适用于读取一堆小文件
    	collect后结果为一个list,其中元素为二元元组,key为文件路径,value为文件内容
    

RDD 算子

算子:分布式集合对象上的API,即作用在分布式对象上的方法,为了区分本地对象上的API(方法)

分类:转换算子和动作算子

  • 转换算子(Transformation)
    return RDD 的算子,这类算子是 懒加载 的(如果没有action算子,转换算子是不工作的)
  • 动作算子(Action)
    返回值不是RDD的算子

转换算子相当于构建计划,动作算子为开关,只有动作算子出现计划才会被执行

转换算子

算子参数功能语法
map函数名将RDD的数据一条条处理(根据接收的处理函数),将处理函数的结果封装到新的RDD中返回rdd.map(func)参数函数的参数类型不限,返回值类型不限
flatMap函数名对RDD进行map操作,然后进行解除嵌套rdd.flatMap(func)参数函数的要求同map
reduceByKey函数名针对KV型RDD,先按照key分组,然后根据参数函数完成组内数据 (value)聚合参数函数接收两个参数,返回一个值,两个参数和返回值的类型要相同;参数函数只管聚合,不管分组;value两两叠加式聚合,如对1,2,3,4相加,计算顺序是1+2,3+3,6+4
groupBy函数名将RDD的数据进行分组分组依据是将RDD中的每条数据输入参数函数得到返回值,将返回值相同的对应数据分到一组;每个组是一个二元组,key为参数函数返回值,value是同组数据的迭代器
filter函数名过滤出想要的数据参数函数的返回值需为bool,true的保留,false的丢弃
distinct去重分区数量对RDD数据去重,返回新RDD算子参数一般缺省;RDD数据类型任意,数字字符都行
unionrdd合并2个RDD为1个RDD返回rdd.union(other_rdd)只合并,不去重,合并时不管数据类型
joinrdd对两个RDD执行join操作(可实现SQL的内/外连接)rdd.join(other_rdd) rdd.leftOuterJoin(other_rdd)join算子只能作用于二元元组的RDD,默认按照key进行关联,若要通过value关联需要用map调换kv的位置
intersectionrdd求两个RDD的交集,返回新RDDrdd.intersection(other_rdd)
glom-将RDD数据按照分区加上嵌套rdd.glom()
groupByKey-针对KV型RDD,自动按照key将value分组分组后的数据迭代器中只保留value
sortBy函数名; ascending(bool); numPartitions(int)对RDD数据进行排序rdd.sortBy(func, ascending=False, numPartitions=3)参数函数的作用是指定排序的依据列;ascending升序降序;numPartitions表示排序的分区数,要保证数据全局有序要设置为1
sortByKeykeyfunc; ascending; numPartitions针对KV型RDD,按key进行排序rdd.sortByKey(ascending=True, numPartitions=None, keyfunc=< function>)keyfunc在排序前对key进行处理

解除嵌套:[ [1, 2], [3, 4] ] -->[1,2,3,4],对分区内的字符串split后collect会得到嵌套结果
flatMap若只想解嵌套不Map,可以传一个lambda x: x

案例
在这里插入图片描述

# -*- coding = utf-8 -*-
from pyspark import SparkConf, SparkContext
import json
if __name__ == '__main__':
    conf = SparkConf().setAppName("Transformation_operators_example").setMaster("local[*]")
    sc = SparkContext(conf=conf)
    # 将数据按|分割,得到一组json数据(字符串)
    # 通过Python自带的json库将每个json字符串转为字典
    # 根据字典的值过滤出包含北京的数据
    # 组合 北京 和 商品类型 形成新的字符串
    # 去重
    rdd = sc.textFile("../data/input/order.text")\
        .flatMap(lambda line : line.split("|"))\
        .map(lambda json_str: json.loads(json_str))\
        .filter(lambda d: d['areaName'] == '北京')\
        .map(lambda x: x['areaName'] + '_' + x['category'])\
        .distinct()
    print(rdd.collect())

动作算子

算子参数功能语法
countByKey-统计key出现的次数rdd.countByKey()一般适用于kv型RDD,返回值为字典{(key:count)}
collect将RDD各分区中的数据收集到Driver中,形成一个List对象要拉取到Driver中,结果不能过大,否则会撑爆Driver内存
reduce函数名将RDD数据集按传入方法的逻辑进行聚合rdd.reduce(lambda a,b:a+b)返回值类型不确定,可以使int或str等等
foldinit_value, 函数名接收传入逻辑进行聚合,但是聚合带有初始值rdd.fold(10, lambda a,b:a+b)初始值会同时作用在分区内和分区间
first取出RDD的第一个元素返回值类型为rdd中的元素类型
takeint取出RDD的前n个元素rdd.take(5)返回 List
topint对RDD降序排序,并取前n个rdd.top(5)返回 List
count返回RDD中的数据数量
takeSamplebool, 采样数,随机数种子随机抽样RDD数据bool为True表示允许取同一个位置的数据,可以重复抽样;随机种子可以省略
takeOrderedint,函数名对RDD按参数函数处理后的值排序,然后取前n个值对应的原始数据默认降序,若要升序可以用参数函数将数据取负
foreach函数名功能和map一样rdd.foreach(lambda x: print(x*10))该方法没有返回值;foreach的执行结果由Executor直接输出,而不像collect等将结果汇集到driver后输出,效率高,如在lambda函数中执行数据库插入操作
saveAsTextFile将RDD的数据写入文本文件中支持本地写出和HDFS,一个分区中的数据写到一个文件中,由Executor执行

分区操作算子

算子参数功能语法
mapPartitions函数名按分区,根据参数函数的逻辑,处理RDD中的数据参数函数的参数是一个分区的数据(迭代器),返回值也是一个迭代器,map是单个元素;减少了网络IO的次数,性能优于map
foreachPartition函数foreach的分区计算版本性能会优于foreach
partitionByint,函数名对RDD进行自定义分区int为新分区数;参数函数对key做运算,返回值需为int类型(分区编号)
repartitionint对rdd的分区重新分区参数为新分区数量;一般不要修改分区,会影响并行计算(内存迭代的管道数量) ;一般不要增加分区(可能会导致shuffle)

groupByKey 和 reduceByKey 的区别
功能上:
groupByKey 只能 分组reduceByKey分组+聚合 一体化
性能上:
reduceByKey的性能要优于 groupByKey + reduce
groupByKey + reduce只能先分组,后聚合,分组时要对每条数据进行IO,消耗很大
reduceByKey 可以先预聚合,然后对预聚合的结果进行IO(分组),再最终聚合,减少被shuffle的数据量,提升性能
数据量越大,reduceByKey的优势就越高

总结

RDD创建方法:
本地集合并行化、读取文件(TextFile\WholeTextFile)

RDD分区数怎么查看
rdd.getNumPartitions()

Transformation 和 Action 的区别
转换算子必返回RDD,动作算子必不返回RDD
转换算子懒加载,不遇到动作算子不执行

哪两个Action算子的结果不经过Driver直接Executor输出
foreach 和 saveAsTextFile

reduceByKey 和 groupByKey 的区别
功能:reduceByKey 分组+聚合,groupByKey 聚合
性能:reduceByKey 分组前预聚合,再shuffle,传输IO小性能强

mapPartitions 和 foreachPartition的区别
mapPartitions有返回值,foreachPartition无返回值
mapPartitions是转换算子,foreachPartition是动作算子
二者都是以分区为单位处理数据,比普通算子性能强

分区操作的注意点
尽量不要增加分区,否则可能破坏内存迭代的计算管道

RDD 持久化

RDD 的数据是过程数据

RDD之间是有血缘关系的,新 RDD 的生成,代表着老 RDD 的消失
一旦处理完成,RDD的数据就被清除了
这个特性可以最大化资源利用率,从内存中清除老 RDD,为后续计算腾出内存空间
在这里插入图片描述

当要多次使用同一个RDD时,第2次使用时只能基于血缘关系重新构建该RDD,这性能损耗就很厉害了,怎么解决?RDD 持久化技术!(缓存 和 CheckPoint)

RDD 缓存

Spark 提供了缓存API,用于将 RDD 数据保存在内存或硬盘
缓存API有两种
rdd.cache():缓存到内存中
rdd.persist(StoreageLevel):根据参数设置的级别将RDD数据存储到内存或硬盘中 from pyspark.storagelevel import StorageLevel导入参数类
在这里插入图片描述
rdd.unpersist():清理缓存

调用API后,每个Executor存储其所在服务器上的RDD分区数据到该Executor所在服务器的内存或硬盘上(分散存储),并且被缓存的RDD的前置血缘关系也会被存储(防止内存数据丢失)

RDD CheckPoint

CheckPoint 也是存储RDD的数据,但是它仅支持硬盘存储
再设计上CheckPoint被认为是安全的,因此它不保留RDD血缘关系

CheckPoint 存储RDD数据时是将每个RDD分区中的数据进行收集后存储(如存到HDFS中)
在这里插入图片描述

CheckPoint VS 缓存

CheckPoint缓存
存储方式集中存储(存整个RDD)分散存储(以分区为单位存)
风险与分区数量无关分区越多,风险越大
存储位置支持HDFS和本地硬盘内存或硬盘
性能略差(集中存储涉及网络IO)好一点(能写内存,各Executor并行)
血缘关系不保留保留

RDD 案例练习

在这里插入图片描述
需求:
用户搜索关键词分析
用户和关键词组合分析
热门搜索时间段分析
在这里插入图片描述

RDD 共享变量

广播变量

本地list分布式RDD 对象有了 关联(如要将RDD对象内的部分数据用本地list中的部分数据替换),Driver 就会向托管每个分区的 Executor 发送 序列化的 本地list 对象,但若每个Executor 托管着多个分区,Executor 本质上是一个进程,进程内资源共享,向该进程内的多个分区发送 本地list 就会造成 多余的网络IO内存浪费

怎么解决?广播变量!
将本地对象封装到广播变量当中,使用时从广播变量中取出本地变量即可
对于同一个Executor托管的分区,Driver只会向他们发送一份本地数据(一个广播变量),减少网络IO,节省内存

使用方法
在这里插入图片描述

将本地list转为RDD也可以实现功能,但是RDD关联RDD需要通过join算子,就有可能产生shuffle,性能不如本地list关联RDD
在这里插入图片描述

累加器

分布式场景下,当我们想要统计RDD中的某类数据的数量,可能会在map算子的参数函数中定义一个自增计数器变量count,用于统计被处理过的数据数量
那么问题就来了,非RDD代码在 Driver中执行,Driver定义了count,对分区执行map操作时要用到count,Driver会将count(只是数据)发给每个Executor,每个Executor内对count的累加操作只影响本Executor内的count,对Driver中的count无效,就无法实现分布式累加了

怎么办?使用Spark提供的累加器!累加器对象可以从各Executor中收集他们的执行结果并汇总,实现分布式累加

使用方法

acmlt = sc.accumulator(0)  创建累加器变量,参数为累加器初始值

注意
累加器的值和对应的RDD的创建相关,当一个RDD被多次生成时,累加器中的值也会多次累加,导致结果错误,可以通过RDD持久化(缓存或CheckPoint)解决

总结

广播变量解决什么问题?
分布式集合RDD 和 本地集合进行关联使用时,降低内存占用,减少网络IO,提高性能

累加器解决什么问题?
全局累加

Spark 内核调度

DAG

有向无环图,描述了 一组单向依赖的RDD逻辑执行流程
在这里插入图片描述
Job 和 Action
在这里插入图片描述
Action算子会将该算子之前的一串rdd依赖链条执行起来
也就是说,一个Action会产生一个逻辑DAG,会在程序运行时产生一个Job

即:一个Application中,可以包含多个Job,每个Job对应着一个DAG,且每个Job都是由一个Action产生的

DAG 和 分区

DAG 的最终目的是:构建物理上的Spark详细执行计划

带有分区关系的DAG只有在代码运行起来之后才能画出来,因为只有运行起来后才能确定分区数量等
在这里插入图片描述

DAG的宽窄依赖和阶段划分

Spark中RDD的血缘依赖关系可以分为两类

  • 窄依赖:父RDD的一个分区,全部将数据发给子RDD的一个分区
  • 宽依赖:父RDD的一个分区,将数据发给子RDD的多个分区,也叫 shuffle

射出多个箭头的就是宽依赖

阶段划分
Spark中Job会被划分为不同的阶段(Stage),划分依据是:遇到宽依赖就划分出一个阶段
Stage内部一定都是窄依赖,窄依赖的一组RDD分区可以整整齐齐的运行
在这里插入图片描述

内存迭代计算

RDD的转换怎么实现?
不同RDD之间的转换用不同的线程执行(线程若属于不同Executor还需要网络IO进行交互)
不可行×

在RDD转换链中,具有窄依赖的一组RDD的转换由一个线程完成(线程内无需通过IO交互)
可行√

上述一组窄依赖RDD的转换被规划为一个Task(线程完成),是纯内存计算,构成内存计算管道

横向来看,一个分区只会被一个Task处理,
竖向来看,一个 RDD 中的每个分区都会被不同的 Task 处理

Spark 默认收到全局并行度的限制,除个别算子有特殊分区需求(如sort要保证全局排序要设为1),一般不建议在算子上再设置并行度,这样有可能破坏内存计算管道,产生额外的 shuffle(分区数不1:1就会宽依赖了)

1. Spark如何做内存计算?DAG的作用?Stage阶段划分的作用?
Spark会产生DAG
DAG图会基于分区和宽窄依赖关系划分Stage
一个Stage内都是窄依赖,如果一组窄依赖的RDD内形成1:1的分区数量关系,就可以产生很多内存迭代计算管道
这些管道就是一个个具体的执行Task
一个Task是一个具体的线程,任务跑在一个线程内,就是走内存计算

DAG 是为了内存计算
Stage划分 是为了构建内存计算管道

2. Spark 为什么比 MapReduce 快?

  • Spark 算子丰富。MapReduce算子很少(Map 和 Reduce),处理复杂任务时往往需要多个MapReduce串联,这就需要通过磁盘交互数据,导致速度慢
  • Spark 可以尽可能多的内存迭代计算。算子之间形成DAG,基于依赖划分阶段后,阶段内形成内存迭代管道,速度快,MapReduce中的Map和Reduce依旧通过磁盘交互

Spark 并行度

并行度:同一时间内同时运行的 Task 数量
并行度会影响到 RDD 的分区设定
如设置并行度 6,有 6 个 Task 并行的前提下,RDD 就被规划成 6 个分区了。先有并行度,才有分区规划

并行度设置位置:
优先级降序
代码 > 提交程序的客户端参数中 > 配置文件
默认并行度为 1

全局并行度设置方式:
在这里插入图片描述
集群中如何规划并行度?
设置为集群内CPU总核心数的2~10倍,确保是核心数的整数倍,最少2倍,不能过大,过大调度难度会很大
CPU 的一个核心一次只能干一件事,若Task的压力不均衡,某个 Task 先执行完了,就会导致部分 CPU核心 的空闲,所以多设置并行度可以保证 Task 运行完后有新 Task 补上,不让 CPU 闲下来,最大程度利用集群的资源

Spark 任务调度

Spark 的任务,由 Driver 进行调度,主要包括

  • 逻辑DAG产生
  • 分区DAG产生
  • Task划分
  • 将Task分配给Executor并监控其工作

在这里插入图片描述
Spark 程序的调度流程如上图:

  1. 构建Driver
  2. 构建SparkContext(执行环境入口对象)
  3. 将Job代码提交给 DAG Scheduler(DAG调度器),构建分区DAG图,和基于此图的Task分配
  4. 基于 Task Scheduler(Task调度器)将Task分配到各个Executor上干活,并监控他们

Driver内的两大组件:
DAG 调度器:根据代码逻辑生成逻辑DAG,基于分区关系生成分区DAG,基于分区DAG得到逻辑上的Task划分(即每个Task做什么,Task之间的交互关系)
Task 调度器:基于逻辑 Task 划分,来规划这些 Task 应该在哪些物理Executor上运行,并监控管理

Spark 概念名词大全(挖坑)

TermMeaning
Application用户代码被提交到Spark运行时就会形成一个Application,由一个Driver和多个Executor构成
Driver program管理main方法的入口,程序运行的调度者和管理者,负责构建SparkContext
Cluster manager外部服务,用于管理集群资源(Master角色,如Standalone manager 和 Yarn 中的 resource Manager)
Deploy mode

Spark 层级运行关系
一个 Spark环境,可以运行 多个Application(一个运行中的代码)
Application 内可以有 多个 Job
每个 Job 由一个 Action算子 产生,且每个 Job 有自己的 DAG图
一个 Job 的 DAG,根据宽窄依赖划分成 不同Stage
每个 Stage 基于分区数量形成 多个内存迭代管道
每个内存管道形成一个 Task(DAG调度器划分Job成Stage,Stage被分为具体的Task任务,)

总结

DAG是什么有啥用?
有向无环图,描述任务执行流程,主要作用是协助DAG调度器构建Task分配用作任务管理

内存迭代 / 阶段划分?
基于DAG中RDD的宽窄依赖划分阶段,阶段内部是窄依赖可以构建内存迭代的管道

DAG 调度器?
构建Task分配,用以任务管理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值