spark core相关知识点

用一个统一的数据抽象对象,来实现分布式框架中的计算功能
这个数据对象就是rdd

RDD

定义

  • 弹性分布式数据集,spark中最基本的数据抽象

  • 代表一个不可变、可分区、元素可并行计算的集合

  • Resilient:RDD中的数据可存储在内存或者硬盘中

  • Distributed: 数据是分布式的,可用于分布式计算

  • Dataset: 数据集合,用于存放数据

特性

  1. RDD是有分区的
    • 分区是RDD最小的存储单位
    • 分区是物理概念
    • 多个物理的分区组成了一个抽象的RDD
    • 可以用glom() API查看分区
  2. 计算方法会作用到每一个分区上
  3. RDD之间是有相互依赖的关系的
    • 每个新产生的RDD都需要依赖于之前的RDD
    • RDD之间是迭代计算的,会形成一个依赖链条
  4. KV型RDD可以有分区器 (可选)
    • KV型RDD,RDD中的数据是二元组,例如("hadoop",3)"hadoop"是key,3是value
    • 默认会有一个hash型的分区器,将相同key值的RDD分到同一个分区
    • 该特性是可选的,不是所有的RDD都是KV型的,例如像list的RDD
  5. RDD的分区数据读取会尽可能靠近数据源 (可选)
    • 尽可能保证数据是本地读取而不是网络读取,从而减少通讯时间
    • 尽可能确保,不是100%保证的,优先要保证并行计算能力

RDD编程

对于sparkRDD编程,其入口一定是sparkContext,sparkContext的作用就是创建出第一个RDD

创建RDD主要两种方式:

  1. 通过将本地对象转化成分布式的RDD
  2. 读取外部的文件

RDD算子

本质就是RDD上的api,本地对象(例如list)的叫做方法,RDD的叫做算子

RDD的算子分成两类,Transformation算子(转换算子),Action算子

Transformation算子

只要返回值依然是rdd的,就是trans算子

特性: 这类算子是lazy的,如果没有action算子的话,他永远不会计算,只是在构建计划

常见的transformation算子
  1. map算子
    传入一个函数对象,将RDD中的数据按照函数中的逻辑一条条进行处理,返回一个新的rdd

  2. flatmap算子
    先对rdd执行map操作,再执行解除嵌套
    解除嵌套就是将多维的对象转换成一维的

  3. reduceByKey
    针对KV型RDD,按照key进行分组,在按照聚合逻辑完成组内数据的聚合操作
    传入两个参数,返回一个值

  4. mapValues
    针对二元元组,内部的value进行map操作,只对value进行操作
    例如

rdd = sc.paralleize([('a',1),('b',11))
rdd.mapValues(lambda x : x *10).collect()

输出的结果为[('a',10),('b',110)]

  1. groupBy
    将RDD数据进行分组

action算子

只要返回值不是rdd的就是action算子

只有有了action算子,数据才开始真正被处理

常见的action算子

  1. collect
    这个要十分注意,因为RDD是分布式的,可以计算的数据量非常大,collect会将分布式的rdd计算结果收集到driver端,如果不注意的话可能会造成driver端的内存溢出

  2. foreach
    该算子也是遍历rdd中的每一个元素,对其进行func中的操作,但是要注意和map不同的是,该算子没有返回值
    该算子中的操作都是在executor端进行的,map后进行collect的操作,是将元素计算处理后再收集到driver端,由driver端进行处理。因此该算子能够降低driver端的压力。同时因为是在executor中执行,多个executor之间是并行的,能够有效提高部分逻辑的计算效率,避免了和driver的交互和统一处理

reduceByKey 和 groupByKey的区别

  1. reduceByKey的性能远大于groupByKey
    groupByKey是先进行分组,分完组后再进行自逻辑的聚合,分组的过程中会发生多次io
    而reduceByKey是先进行预分组,再进行分组,分组完成后再进行最终的聚合。
    groupBykey的每一次分组都是一次shuffle,而reduceByKey减少了shuffle的次数

对于分区操作,尽量不要尝试去增加分区数,不然可能会破坏内存迭代的管道

RDD的持久化

为了最大化利用资源,旧的rdd在进行转化成新的rdd之后就没有必要再存在了,为后续的计算节省内存空间
但是如果基于rdd1生成了rdd2和rdd3,在rdd1转化成rdd2时,rdd1就已经不存在了,生成rdd3时需要重新再生成rdd1,如果该rdd是较为靠后的rdd,需要重新遍历一遍依赖链,从最开始重新开始生成。这样会造成效率的降低,需要将rdd进行持久化

cache 缓存

可以通过调用.cache()的api,将rdd持久化
通过调用.persist()设置将rdd持久化的位置,推荐的持久化位置MEMORY_AND_DISK,对于内存较小的集群,推荐将其在硬盘上持久化DISK_ONLY
最后请将缓存主动清理掉,避免占用内存,使用.unpersist()

特性
  1. 调用.cache()后,会在每台服务器的内存或者硬盘上都创建一个副本,属于分散存储

  2. 缓存是不安全的,如果缓存丢失,会重新根据迭代链进行计算并缓存

因此对于rdd来说,一定会保留其血缘关系(迭代链),确保rdd能够再次被计算

checkpoint

checkpoint只支持使用硬盘进行存储,他被认为是安全的,在设计上认为是不会丢失的。因此不保留rdd的血缘关系

checkpoint集中收集每个分区的数据进行存储,属于集中存储,将分区中的文件收集保存到hdfs上(hdfs是一种安全的存储)

在使用是,首先要在sparkconf中调用声明cp的保存路径

sc.setCheckpointDir("hdfs://node1:8020/....")
# 如果是local可以保存在本地,其余需要保存在hdfs上

# 对于某个需要多次调用的rdd,直接调用checkpoint算子就可以了
rdd.checkpoint()
checkpoint和cache的对比
  • cp不管分区多少,风险是一致的,因为都要收集起来集中存储。而cache分区越多,风险越高,因为是分散存储的,一旦一个分区挂了就都丢失了
  • cp支持写入hdfs,高可靠的,而cache不行
  • cp不支持写入内存,但是cache可以,因此cache的性能比cp稍微好一点
  • cp被认为是安全的,因此不保留血缘关系,而cache需要保留
  • cache一般适合于轻量化的,对于较为复杂庞大的rdd最好使用cp

广播变量

当本地存在某些变量需要去和rdd进行交互的时候,会被分发到每个rdd所在的线程中使用。但是对于同一个executor中的线程而言,他们同属于一个进程,他们之间的资源是共享的,每个线程发送一个数据实际上造成了内存的浪费。

代码

# 1. 将本地变量标记成广播变量
broadcast = sc.broadcast(local_list)

# 2. 使用广播变量的时候,直接从broadcast对象中取出value,
local_val_value = broadcast.value

在使用的时候,spark分发的不再是本地变量了,而是给每个executor发送一个broadcast对象
能够有效减少网络io

但是如果本地集合对象很大的时候,driver端可能放不下,那还是需要将其封装到rdd中

累加器

对于分布式的executor,如果给一个全局变量,希望各分区中实现累加,实际上各分区内实现累加,最终到driver上的内容与rdd中executor中的数据无关,导致全局变量的异常

需要把这个累加的全局变量转换成spark的累加器类型,实现在分布式的场景下的累加

global_accum = sc.accumulator(0)

方法内的参数为初始值

  • 如果涉及到多个rdd,例如rdd1->rdd2->rdd3,rdd2->rdd4
    在生成rdd3的时候rdd2就已经不存在了,生成rdd4的时候会重新遍历生成链,再次计算rdd2

  • 如果rdd2中调用了累加器,此时会再次激活累加器,再做一次计算。最终导致结果异常。为了避免这个问题,要将多次使用到的rdd持久化

DAG

action算子会将之前的一串rdd依赖链进行执行,每次执行action都会创建一个DAG

一个action会产生一个job,一个job就会产生一个DAG

宽窄依赖

  • 窄依赖: 父RDD的一个分区,全部将数据发送给子RDD的一个分区
  • 宽依赖: 父RDD的一个分区,将数据发送给子RDD的多个分区(也就是产生了不同分区之间的数据交互)

宽依赖有个别名叫做shuffle

窄依赖

  • 父RDD的子RDD只有一个分区
  • 子RDD分区可以有多个父RDD分区

宽依赖

父RDD有多个子RDD分区

一个job(DAG)中的stage的划分标准就是看是否产生了宽依赖,在同一个stage内部,必定都是窄依赖

stage的排序是从action算子开始从后往前计数

内存迭代计算

对于一个stage中,一个分区中的计算迭代都是在一个内存计算管道(pipeline)中独立运行,独立作为一个线程存在,作为一个task

多个分区中的pipeline为多个线程,多个线程并行计算

只有出现宽依赖的时候才会出现不同executor之间的io操作

对于spark优先保证并行计算,再去保证pipeline

为避免破坏pipeline,spark尽量不要去改动分区,遵循全局的并行度要求

spark并行度

同一时间内有多少task在同时运行

有多少并行度,就会创建多少个分区

设置并行度的时候可以通过代码或者配置文件来设置,其中有一个优先级从高到底为

  • 代码 conf.set("spark.default.parallelism","100")
  • 客户端提交参数 --conf "spark.default.parallelism=100"
  • 配置文件 spark.default.parallelism 100
  • 默认参数(1,或者基于读取文件的分片数量)

推荐设置成cpu核心的2到10倍,这个核心数与单个服务器的核心数无关,只看集群的总cpu核数

如果设置并行度和cpu核心数一致,如果某个task先执行完了,就会导致某个cpu核心空闲。因此,设置更大的并行度,保证一段时间内所有cpu都在工作,一个task执行完了之后会有后续的task补上

spark的任务调度

driver负责任务调度,包括

  1. 产生逻辑DAG
  2. 产生分区DAG
  3. 划分task
  4. 将task分配给executor并监控其工作

对于一个spark程序而言,其工作调度流程为

  1. 构建driver
  2. 构建sparkContext,作为执行环境的入口对象
  3. 基于DAG scheduler,构建逻辑task分配
  4. 基于task scheduler,将逻辑task分配给executor,并监控
  5. worker被task schedur监控,听从其指令,并定期汇报

其中只有5是由worker在工作,其余都是由driver管理

对于一个服务器来说,一般就开启一个executor,线程之间的资源是不共享的,交互的时候还是要从网络进行io,虽然快点,但是没什么意义

DAG调度器

处理逻辑DAG,得到逻辑上的task划分

task调度器

基于DAG调度器划分的task,规划这些task应该在哪些物理的executor上执行,并监控其运行

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值