2.SparkCore

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=>)
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上运行,以及监控管理它们的运行

你可能感兴趣的:(spark)