Spark 学习笔记

Spark 学习笔记

Spark

Spark介绍

Spark安装
  • https://juejin.im/post/5d02365ce51d45109725fe65
Spark架构、角色

在这里插入图片描述

  • Spark架构使用了分布式计算中master-slave模型,master是集群中含有master进程的节点,slave是集群中含有worker进程的节点
  • SparkContext: Spark的主要入口点,代表对计算集群的一个连接,是整个应用的上下文,负责与ClusterManager通信,进行资源申请、任务的分配和监控等
  • Driver Program :运⾏main函数并且新建SparkContext的程序
  • Application:基于Spark的应用程序,包含了driver程序和集群上的executor
  • Cluster Manager:指的是在集群上获取资源的外部服务。目前有三种类型
  • Standalone: spark原生的资源管理,由Master负责资源的分配
  • Apache Mesos:与hadoop MR兼容性良好的一种资源调度框架
  • Hadoop Yarn: 主要是指Yarn中的ResourceManager
  • Worker Node: 集群中任何可以运行Application代码的节点,在Standalone模式中指的是通过slaves文件配置的Worker节点,在Spark on Yarn模式下就是NodeManager节点
  • Executor:是在一个worker node上为某应⽤启动的⼀个进程,该进程负责运⾏行任务,并且负责将数据存在内存或者磁盘上。每个应⽤都有各自独立的executor
  • Task :被送到某个executor上的工作单元
Spark任务执行流程

在这里插入图片描述

  • RDD:弹性分布式数据集,是spark 的基本运算单元,通过scala集合转化读取数据集生成或者由其他RDD进过算子操作得到
  • Job:可以被拆分成Task并行计算的单元,一般为Spark Action触发的一次执行作业
  • Stage:每个Job会被拆分成很多组Task,每组任务被称为Stage,也可称TaskSet,该属于经常在日志中
    看到

详细的流程为:

在这里插入图片描述

  • Application启动之后, 会在本地启动一个Driver进程,用于控制整个流程(假设我们使用的Standalone模式)
  • 初始化SparkContext,构建出DAGScheduler、TaskScheduler,以SparkContext为程序运行的总入口
  • 在初始化TaskSechduler的时候,它会向资源管理器(Standalone中是Master)注册Application,Master收到消息后使用资源调度算法在Spark集群的Worker上启动Executor并进行资源的分配,最后将Executor注册到TaskScheduler
  • 资源管理器分配Executor资源并启动StandaloneExecutorBackend,Executor运行情况将随着心跳发送到资源管理器上,到这里准备工作基本完成了
  • 根据我们编写的业务,如通过sc.textFile(“file”)加载数据源,将数据转化为RDD
  • DAGScheduer 先按照Action将程序划分为一至多个job(每一个job对应一个DAG),之后DAGScheduer根据是否进行shuffer将job划分为多个Stage,每个Stage过程都是Taskset , DAG将Taskset交给TaskScheduler(由Work中的Executor去执行)
  • Executor向SparkContext申请Task
  • Task Scheduler将Task发放给Executor运行,同时SparkContext将应用程序代码发放给Executor;
    Task在Executor上运行,运行完毕释放所有资源
Spark World Count
# coding:utf-8
from __future__ import print_function

import sys
from operator import add

# SparkSession:是一个对Spark的编程入口,取代了原本的SQLContext与HiveContext,方便调用Dataset和DataFrame API
# SparkSession可用于创建DataFrame,将DataFrame注册为表,在表上执行SQL,缓存表和读取parquet文件。
from pyspark.sql import SparkSession

if __name__ == "__main__":
    # appName 为 Spark 应用设定一个应用名,改名会显示在 Spark Web UI 上
    # 假如SparkSession 已经存在就取得已存在的SparkSession,否则创建一个新的。
    spark = SparkSession\
        .builder\
        .appName("PythonWordCount")\
        .getOrCreate()
    # 读取传入的文件内容,并写入一个新的RDD实例lines中,此条语句所做工作有些多,不适合初学者,可以截成两条语句以便理解。
    # map是一种转换函数,将原来RDD的每个数据项通过map中的用户自定义函数f映射转变为一个新的元素。原始RDD中的数据项与新RDD中的数据项是一一对应的关系。
    file = "file:///home/dyc/opt/soft/spark/README.md"
    lines = spark.read.text(file).rdd.map(lambda r: r[0])
    # flatMap与map类似,但每个元素输入项都可以被映射到0个或多个的输出项,最终将结果”扁平化“后输出
    counts = lines.flatMap(lambda x: x.split(' ')) \
                  .map(lambda x: (x, 1)) \
                  .reduceByKey(add)
    # collect() 在驱动程序中将数据集的所有元素作为数组返回。 这在返回足够小的数据子集的过滤器或其他操作之后通常是有用的。由于collect 是将整个RDD汇聚到一台机子上,所以通常需要预估返回数据集的大小以免溢出
    output = counts.collect()
    for (word, count) in output:
        print("%s: %i" % (word, count))

    spark.stop()

Spark RDD

什么是 RDD

RDD(Resilient Distributed Dataset) 叫着 弹性分布式数据集 ,是Spark 中最基本的抽象,它代表一个不可变、可分区、里面元素可以并行计算的集合

RDD 的属性
  • 一组分片(Partition),即数据集的基本组成单位
  • 对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度
  • RDD 之间互相存在依赖关系
  • 一个Partitioner ,即 RDD 的分片函数
  • 一个列表,存储存取每个Partition 的优先位置
创建 RDD
从内存
 pyspark
Python 3.6.8 (default, Oct  7 2019, 12:59:55) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
20/01/13 00:25:14 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 2.3.4
      /_/

Using Python version 3.6.8 (default, Oct  7 2019 12:59:55)
SparkSession available as 'spark'.
>>> kv = [('a',7), ('a', 2), ('b', 2), ('b',4), ('c',1), ('c',2), ('c',3), ('c',4)]
>>> rdd2 = sc.parallelize(kv)
>>> rdd3 = rdd2.groupByKey()
>>> rdd3.collect()
[('a', <pyspark.resultiterable.ResultIterable object at 0x7eff82f93390>), ('b', <pyspark.resultiterable.ResultIterable object at 0x7eff82f93438>), ('c', <pyspark.resultiterable.ResultIterable object at 0x7eff82f93470>)]
从文件
# coding:utf-8

from pyspark import SparkConf, SparkContext

if __name__ == '__main__':
    # 连接集群local,本应用名称为Demo
    conf = SparkConf().setMaster('local').setAppName('Demo')
    sc = SparkContext(conf=conf)
    # 统计文件中包含mape的行数,并打印第一行
    logFile = "file:///home/dyc/opt/soft/spark/README.md"  
    lines = sc.textFile(logFile)
    plines = lines.filter(lambda lines: 'Spark' in lines)
    print(plines.count())
    print(plines.first())

注意: 当Spark进行批处理任务遇到当个文件过大时,如果Spark执行器设置的内存太小会出现java.lang.OutOfMemoryError: Java heap space错误导致任务失败,我们可以在RDD创建之后通过repartition方法设置合理的分区数量,将每一个分区的数据量减小。

 def repartition(self, numPartitions):
        """
         Return a new RDD that has exactly numPartitions partitions.

         Can increase or decrease the level of parallelism in this RDD.
         Internally, this uses a shuffle to redistribute data.
         If you are decreasing the number of partitions in this RDD, consider
         using `coalesce`, which can avoid performing a shuffle.

         >>> rdd = sc.parallelize([1,2,3,4,5,6,7], 4)
         >>> sorted(rdd.glom().collect())
         [[1], [2, 3], [4, 5], [6, 7]]
         >>> len(rdd.repartition(2).glom().collect())
         2
         >>> len(rdd.repartition(10).glom().collect())
         10
        """
        return self.coalesce(numPartitions, shuffle=True)
RDD 分区
RDD 的编程 API
  • Transformation

这种 RDD 中的所有转换都是延迟加载的,也就是说,他们并不会直接就计算结果。相反的,他们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个返回结果的 Driver 的动作时,这些操作才会真正的运行。这种设计会让Spark 更加有效率的运行。
常用的 Transformation 操作:

  • map(func):返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成
from pyspark import SparkContext
sc = SparkContext("local", "Map app")
words = sc.parallelize (
   ["scala", 
   "java", 
   "hadoop", 
   "spark", 
   "akka",
   "spark vs hadoop", 
   "pyspark",
   "pyspark and spark"]
)
words_map = words.map(lambda x: (x, 1))
mapping = words_map.collect()
print (mapping)
[('scala', 1), ('java', 1), ('hadoop', 1), ('spark', 1), ('akka', 1), ('spark vs hadoop', 1), ('pyspark', 1), ('pyspark and spark', 1)]
  • filter(func):返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成
from pyspark import SparkContext
sc = SparkContext("local", "Filter app")
words = sc.parallelize (
   ["scala", 
   "java", 
   "hadoop", 
   "spark", 
   "akka",
   "spark vs hadoop", 
   "pyspark",
   "pyspark and spark"]
)
words_filter = words.filter(lambda x: 'spark' in x)
filtered = words_filter.collect()
print (filtered)
['spark', 'spark vs hadoop', 'pyspark', 'pyspark and spark']
  • flatMap(func):类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素
from pyspark import SparkContext
sc = SparkContext("local", "Map app")
words = sc.parallelize (
   ["scala", 
   "java", 
   "hadoop", 
   "spark", 
   "akka",
   "spark vs hadoop", 
   "pyspark",
   "pyspark and spark"]
)
words_map = words.flapMap(lambda x: (x, 1))
mapping = words_map.collect()
print (mapping)
['scala', 1, 'java', 1, 'hadoop', 1, 'spark', 1, 'akka', 1, 'spark vs hadoop', 1, 'pyspark', 1, 'pyspark and spark', 1]
  • mapPartitions(func):类似于map,但独立地在RDD的每一个分片上运行
from pyspark import SparkContext
sc = SparkContext("local", "Map app")
words = sc.parallelize (
   ["scala", 
   "java", 
   "hadoop", 
   "spark", 
   "akka",
   "spark vs hadoop", 
   "pyspark",
   "pyspark and spark"]
)
words_map = words.mapPartitions(lambda x: (x, 1))
words_map.foreach(print)
<itertools.chain object at 0x7f7045a9d3c8>
<itertools.chain object at 0x7f7045a9d3c8>
1
1
<itertools.chain object at 0x7f7045a9d3c8>
1
<itertools.chain object at 0x7f7045a9d3c8>
1
<itertools.chain object at 0x7f7045a9d3c8>
1
<itertools.chain object at 0x7f7045a9d3c8>
1
<itertools.chain object at 0x7f7045a9d3c8>
1
<itertools.chain object at 0x7f7045a9d3c8>
1

  • mapPartitionsWithIndex(func):类似于mapPartitions,但func带有一个整数参数表示分片的索引值
>>> rdd = sc.parallelize([1, 2, 3, 4], 4)
>>> def f(splitIndex, iterator): yield splitIndex
>>> rdd.mapPartitionsWithIndex(f).sum()
6
  • sample(withReplacement, fraction, seed):根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子
>>> rdd = sc.parallelize(range(100), 4)
>>> n = rdd.sample(False, 0.1, 81)
>>> n.collect()
[4, 26, 39, 41, 42, 52, 63, 76, 80, 86, 97]

  • union(otherDataset):对源RDD和参数RDD求并集后返回一个新的RDD
>>> rdd = sc.parallelize([1, 1, 2, 3])
>>> rdd.union(rdd).collect()
[1, 1, 2, 3, 1, 1, 2, 3]
  • intersection(otherDataset):对源RDD和参数RDD求交集后返回一个新的RDD
>>> rdd1 = sc.parallelize([1, 10, 2, 3, 4, 5])
>>> rdd2 = sc.parallelize([1, 6, 2, 3, 7, 8])
>>> rdd1.intersection(rdd2).collect()
[1, 2, 3]
  • groupByKey([numTasks]):在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD
>>> rdd = sc.parallelize([("a", 1), ("b", 1), ("a", 1)])
>>> sorted(rdd.groupByKey().mapValues(len).collect())
[('a', 2), ('b', 1)]
>>> sorted(rdd.groupByKey().mapValues(list).collect())
[('a', [1, 1]), ('b', [1])]
  • reduceByKey(func, [numTasks]):在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置
>>> from operator import add
>>> rdd = sc.parallelize([("a", 1), ("b", 1), ("a", 1)])
>>> sorted(rdd.reduceByKey(add).collect())
[('a', 2), ('b', 1)]

  • 以下还有很多RDD的Transformation操作,可以查看RDD API doc
转换含义
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])先按分区聚合 再总的聚合每次要跟初始值交流 例如:aggregateByKey(0)(+,+) 对k/y的RDD进行操作
sortByKey([ascending], [numTasks])在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
sortBy(func,[ascending], [numTasks])与sortByKey类似,但是更灵活
join(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
cogroup(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
cartesian(otherDataset)笛卡尔积
pipe(command, [envVars])调用外部程序
coalesce(numPartitions)重新分区 第一个参数是要分多少区,第二个参数是否shuffle 默认false ;少分区变多分区 true ; 多分区变少分区 false
repartition(numPartitions)重新分区 必须shuffle 参数是要分多少区少变多
repartitionAndSortWithinPartitions(partitioner)重新分区+排序 比先分区再排序效率高 对K/V的RDD进行操作
  • Action

以下是Spark支持的一些常用action算子。详细请参考 RDD API doc

Action算子作用
reduce(func)将RDD中元素按func进行聚合(func是一个 (T,T) => T 的映射函数,其中T为源RDD元素类型,并且func需要满足 交换律 和 结合律 以便支持并行计算
collect()将数据集中所有元素以数组形式返回驱动器(driver)程序。通常用于,在RDD进行了filter或其他过滤操作后,将一个足够小的数据子集返回到驱动器内存中
count()返回数据集中元素个数
first()返回数据集中首个元素(类似于 take(1))
take(n)返回数据集中前 n 个元素
takeSample(withReplacement,num, [seed])返回数据集的随机采样子集,最多包含 num 个元素
takeOrdered(n, [ordering])按元素排序(可以通过 ordering 自定义排序规则)后,返回前 n 个元素
saveAsTextFile(path)将数据集中元素保存到指定目录下的文本文件中(或者多个文本文件),支持本地文件系统、HDFS 或者其他任何Hadoop支持的文件系统。保存过程中,Spark会调用每个元素的toString方法,并将结果保存成文件中的一行
countByKey()只适用于包含键值对(K, V)的RDD,并返回一个哈希表,包含 (K, Int) 对,表示每个key的个数
foreach(func)在RDD的每个元素上运行 func 函数。通常被用于累加操作
RDD 依赖关系

RDD 和它依赖的 父 RDD(可能有多个) 的关系有两种不同的类型,即 窄依赖(narrow dependency)和宽依赖(wide dependency)
在这里插入图片描述

  • 窄依赖:窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个分区使用。可以比喻为独生子女
  • 宽依赖:宽依赖是多个 RDD 的Partition 会依赖同一个父 RDD 的 Partition
DAG 的生成

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据

在这里插入图片描述

RDD 的持久化
RDD 缓存

Spark中最重要的功能之一是跨操作在内存中持久化(或缓存)数据集。当您持久保存RDD时,每个节点都会存储它在内存中计算的任何分区,并在该数据集(或从中派生的数据集)的其他操作中重用它们。这使得未来的行动更快(通常超过10倍)。缓存是迭代算法和快速交互使用的关键工具。
您可以使用persist()或cache()方法标记要保留的RDD 。第一次在动作中计算它,它将保留在节点的内存中。Spark的缓存是容错的 - 如果丢失了RDD的任何分区,它将使用最初创建它的转换自动重新计算。

  • 什么时候我们需要持久化?要求的计算速度快、 集群的资源要足够大、cache 的数据会多次触发Action
  • 建议:先进行数据过滤,然后将缩小范围后的数据再cache 到内存中.
如何使用
  • 使用 rdd.persist()或者rdd.cache()
cache 和 persist 区别

cache实际上是调用了self.persist(StorageLevel.MEMORY_ONLY),由此可见,cache只有一种缓存级别就是StorageLevel.MEMORY_ONLY, persist可以有多种选择,默认为StorageLevel.MEMORY_ONLY

见源码:

    def cache(self):
        """
        Persist this RDD with the default storage level (C{MEMORY_ONLY}).
        """
        self.is_cached = True
        self.persist(StorageLevel.MEMORY_ONLY)
        return self


[docs]    def persist(self, storageLevel=StorageLevel.MEMORY_ONLY):
        """
        Set this RDD's storage level to persist its values across operations
        after the first time it is computed. This can only be used to assign
        a new storage level if the RDD does not have a storage level set yet.
        If no storage level is specified defaults to (C{MEMORY_ONLY}).

        >>> rdd = sc.parallelize(["b", "a", "c"])
        >>> rdd.persist().is_cached
        True
        """
        self.is_cached = True
        javaStorageLevel = self.ctx._getJavaStorageLevel(storageLevel)
        self._jrdd.persist(javaStorageLevel)
        return self
persist 级别
  • DISK_ONLY:磁盘
  • DISK_ONLY_2:磁盘;双副本
  • MEMORY_ONLY: 内存;反序列化;把RDD作为反序列化的方式存储,假如RDD的内容存不下,剩余的分区在* 以后需要时会重新计算,不会刷到磁盘上。
  • MEMORY_ONLY_2:内存;反序列化;双副本
  • MEMORY_ONLY_SER:内存;序列化;这种序列化方式,每一个partition以字节数据存储,好处是能带来更好的空间存储,但CPU耗费高
  • MEMORY_ONLY_SER_2 : 内存;序列化;双副本
  • MEMORY_AND_DISK:内存 + 磁盘;反序列化;双副本;RDD以反序列化的方式存内存,假如RDD的内容存不下,剩余的会存到磁盘
  • MEMORY_AND_DISK_2 : 内存 + 磁盘;反序列化;双副本
  • MEMORY_AND_DISK_SER:内存 + 磁盘;序列化
  • MEMORY_AND_DISK_SER_2:内存 + 磁盘;序列化;双副本
  • 序列化能有效减少存储空间,默认MEMORY_ONLY

具体的定义:

_all__ = ["StorageLevel"]


[docs]class StorageLevel(object):

    """
    Flags for controlling the storage of an RDD. Each StorageLevel records whether to use memory,
    whether to drop the RDD to disk if it falls out of memory, whether to keep the data in memory
    in a JAVA-specific serialized format, and whether to replicate the RDD partitions on multiple
    nodes. Also contains static constants for some commonly used storage levels, MEMORY_ONLY.
    Since the data is always serialized on the Python side, all the constants use the serialized
    formats.
    """

    def __init__(self, useDisk, useMemory, useOffHeap, deserialized, replication=1):
        self.useDisk = useDisk
        self.useMemory = useMemory
        self.useOffHeap = useOffHeap
        self.deserialized = deserialized
        self.replication = replication

    def __repr__(self):
        return "StorageLevel(%s, %s, %s, %s, %s)" % (
            self.useDisk, self.useMemory, self.useOffHeap, self.deserialized, self.replication)

    def __str__(self):
        result = ""
        result += "Disk " if self.useDisk else ""
        result += "Memory " if self.useMemory else ""
        result += "OffHeap " if self.useOffHeap else ""
        result += "Deserialized " if self.deserialized else "Serialized "
        result += "%sx Replicated" % self.replication
        return result


StorageLevel.DISK_ONLY = StorageLevel(True, False, False, False)
StorageLevel.DISK_ONLY_2 = StorageLevel(True, False, False, False, 2)
StorageLevel.MEMORY_ONLY = StorageLevel(False, True, False, False)
StorageLevel.MEMORY_ONLY_2 = StorageLevel(False, True, False, False, 2)
StorageLevel.MEMORY_AND_DISK = StorageLevel(True, True, False, False)
StorageLevel.MEMORY_AND_DISK_2 = StorageLevel(True, True, False, False, 2)
StorageLevel.OFF_HEAP = StorageLevel(True, True, True, False, 1)

"""
.. note:: The following four storage level constants are deprecated in 2.0, since the records
    will always be serialized in Python.
"""
StorageLevel.MEMORY_ONLY_SER = StorageLevel.MEMORY_ONLY
""".. note:: Deprecated in 2.0, use ``StorageLevel.MEMORY_ONLY`` instead."""
StorageLevel.MEMORY_ONLY_SER_2 = StorageLevel.MEMORY_ONLY_2
""".. note:: Deprecated in 2.0, use ``StorageLevel.MEMORY_ONLY_2`` instead."""
StorageLevel.MEMORY_AND_DISK_SER = StorageLevel.MEMORY_AND_DISK
""".. note:: Deprecated in 2.0, use ``StorageLevel.MEMORY_AND_DISK`` instead."""
StorageLevel.MEMORY_AND_DISK_SER_2 = StorageLevel.MEMORY_AND_DISK_2
""".. note:: Deprecated in 2.0, use ``StorageLevel.MEMORY_AND_DISK_2`` instead."""

我们可以看到: def __init__(self, useDisk, useMemory, useOffHeap, deserialized, replication=1), 中指明了是否使用磁盘,是否使用内存,是否使用堆外内存,是否序列化,副本数量

有关Spark内存使用,可以看下面几篇文章

RDD分区partition,分区器partitioner
  • 分区partition

partition是RDD的最小数据处理单元,可以看作是一个数据块,每个partition有个编号index。
一个partition被一个map task处理

>>> rdd = sc.parallelize([1, 2, 3, 4], 2)
>>> rdd.getNumPartitions()
2
  • 分区器partitioner
    • MR任务的map阶段的处理结果会进行分片(也可以叫分区,这个分区不同于上面的分区),分片的数量就是reduce task的数量。
    • 具体怎么分片由分区器partitioner决定,spark中默认定义了两种partitioner:
      • 哈希分区器(Hash Partitioner):hash分区器会根据key-value的键值key的hashcode进行分区,速度快,但是可能产生数据偏移,造成每个分区中数据量不均衡
      • 范围分区器(Range Partitioner):range分区器会对现有rdd中的key-value数据进行抽样,尽量找出均衡分割点,一定程度上解决了数据偏移问题,力求分区后的每个分区内数据量均衡,但是速度相对慢
    • partitioner分区详情
      • 在对父RDD执行完Map阶段任务后和在执行Reduce阶段任务前,会对Map阶段中间结果进行分区。
        分区由父RDD的partitioner确定,主要包括两部分工作
      • 确定分区数量(也就是reduce task数量),也是子RDD的partition数量,决定将Map阶段中间结果的每个key-value对分到哪个分区上

Spark 累加器和广播变量

  • 累加器
    累加器(accumulator)
    累加器是仅仅被相关操作累加的变量,因此可以在并行中被有效地支持。它可以被用来实现计数器和总和。
    累加器通过对一个初始化了的变量v调用SparkContext.accumulator(v)来创建。在集群上运行的任务可以通过add或者”+=”方法在累加器上进行累加操作。但是,它们不能读取它的值。只有驱动程序能够读取它的值,通过累加器的value方法

  • 广播变量

Spark提供的Broadcast Variable,是只读的。并且在每个节点上只会有一份副本,而不会为每个task都拷贝一份副本。因此其最大作用,就是减少变量到各个节点的网络传输消耗,以及在各个节点上的内存消耗。此外,spark自己内部也使用了高效的广播算法来减少网络消耗

Spark SQL

Spark SQL是Spark用来处理结构化数据的一个模块,它提供了两个编程抽象分别叫做DataFrame和DataSet

DataFrame

DataFrame与RDD相同之处,都是不可变分布式弹性数据集。不同之处在于,DataFrame的数据集都是按指定列存储,即结构化数据。类似于传统数据库中的表。 DataFrame的设计是为了让大数据处理起来更容易。DataFrame允许开发者把结构化数据集导入DataFrame,并做了higher-level的抽象; DataFrame提供特定领域的语言(DSL)API来操作你的数据集

最简单的理解我们可以认为DataFrame就是Spark中的数据表(类比传统数据库),DataFrame的结构如下:
DataFrame(表)= Schema(表结构) + Data(表数据)

总结:DataFrame(表)是Spark SQL对结构化数据的抽象。可以将DataFrame看做RDD。

小例子

# coding:utf-8
from pyspark import SparkConf, SparkContext
from pyspark.sql import HiveContext,Row,SparkSession

if __name__ == '__main__':
    spark = SparkSession \
        .builder \
        .appName("Python Spark SQL basic example") \
        .config("spark.some.config.option", "some-value") \
        .getOrCreate()

    logFile = "file:///home/dyc/opt/soft/spark/project/file.json"  
    df = spark.read.json(logFile)
    # 
    df.show()
    df.createOrReplaceTempView("people")
    sqlDF = spark.sql("SELECT * FROM people")
    # 与df.show()相同
    sqlDF.show()


RDD 转换成 DataFrame
from pyspark.sql import SparkSession
from pyspark.sql.types import *

spark = SparkSession \
    .builder \
    .appName("Python Spark SQL basic example") \
    .config("spark.some.config.option", "some-value") \
    .getOrCreate()

sc = spark.sparkContext

# Load a text file and convert each line to a Row.
lines = sc.textFile("file:///home/dyc/opt/soft/spark/examples/src/main/resources/people.txt")
parts = lines.map(lambda l: l.split(","))
# Each line is converted to a tuple. It's a RDD
people = parts.map(lambda p: (p[0], p[1].strip()))
# The schema is encoded in a string.
schemaString = "name age"
fields = [StructField(field_name, StringType(), True) for field_name in schemaString.split()]
schema = StructType(fields)
# Apply the schema to the RDD.
schemaPeople = spark.createDataFrame(people, schema)
# Creates a temporary view using the DataFrame
schemaPeople.createOrReplaceTempView("people")
# SQL can be run over DataFrames that have been registered as a table.
results = spark.sql("SELECT name FROM people")
results.show()

Datasets

简单的说来,Dataset 提供了对 RDD 的 sql 查询。它可以从 JVM objects 中构建,然后使用转换算子操作,例如 map, flatMap, filter

  • 注意:pyspark 不支持Dataset

SparkStreaming

什么是Spark Streaming

首先,什么是流(streaming)?数据流是连续到达的无穷序列。流处理将不断流动的输入数据分成独立的单元进行处理。流处理是对流数据的低延迟处理和分析。Spark Streaming是Spark API核心的扩展,可实现实时数据的快速扩展,高吞吐量,高容错处理。Spark Streaming适用于大量数据的快速处理。实时处理用例包括:网站监控,网络监控欺诈识别网页点击广告物联网传感器

Spark Streaming支持如HDFS目录,TCP套接字,Kafka,Flume,Twitter等数据源。数据流可以用Spark 的核心API,DataFrames SQL,或机器学习的API进行处理,并且可以被保存到HDFS,databases或Hadoop OutputFormat提供的任何文件系统中去。

Spark Straming如何工作

Spark Streaming将数据流每X秒分作一个集合,称为Dstreams,它在内部是一系列RDD。您的Spark应用程序使用Spark API处理RDD,并且批量返回RDD操作的结果

在内部实现上,DStream 由连续的序列化 RDD 来表示。每个 RDD 含有一段时间间隔内的数据,如下图所示

在这里插入图片描述

对数据的操作也是按照 RDD 为单位来进行的,如下图所示

在这里插入图片描述

Spark Streaming 优缺点

与传统流式框架相比,Spark Streaming 最大的不同点在于它对待数据是粗粒度的处理方式,即一次处理一小批数据,而其他框架往往采用细粒度的处理模式,即依次处理一条数据。Spark Streaming 这样的设计实现既为其带来了显而易见的优点,又引入了不可避免的缺点。

优点

  1. Spark Streaming 内部的实现和调度方式高度依赖 Spark 的 DAG 调度器和 RDD,这就决定了 Spark Streaming 的设计初衷必须是粗粒度方式的,同时,由于 Spark 内部调度器足够快速和高效,可以快速地处理小批量数据,这就获得准实时的特性

  2. Spark Streaming 的粗粒度执行方式使其确保“处理且仅处理一次”的特性,同时也可以更方便地实现容错恢复机制

  3. 由于 Spark Streaming 的 DStream 本质是 RDD 在流式数据上的抽象,因此基于 RDD 的各种操作也有相应的基于 DStream 的版本,这样就大大降低了用户对于新框架的学习成本,在了解 Spark 的情况下用户将很容易使用 Spark Streaming

  4. 由于 DStream 是在 RDD 上的抽象,那么也就更容易与 RDD 进行交互操作,在需要将流式数据和批处理数据结合进行分析的情况下,将会变得非常方便

缺点

  1. Spark Streaming 的粗粒度处理方式也造成了不可避免的延迟。在细粒度处理方式下,理想情况下每一条记录都会被实时处理,而在 Spark Streaming 中,数据需要汇总到一定的量后再一次性处理,这就增加了数据处理的延迟,这种延迟是由框架的设计引入的,并不是由网络或其他情况造成的

总而言之,Spark Streaming 为我们提供了一种崭新的流式处理框架,相信未来随着 Spark Streaming 会在易用性、稳定性以及其他方面有很大的提升。

示例应用程序

示例应用程序使用pyspark消费kafak json 数据

数据投递程序
# encoding:utf-8

from kafka import KafkaProducer
import json
import time

tp='spark_streaming_test_topic'
producer = KafkaProducer(bootstrap_servers='localhost:9092',value_serializer=lambda v: json.dumps(v).encode('utf-8'))
for i in range(1000):
     print i
     time.sleep(1)
     # 投递的数据是JSON
     producer.send(tp, key=bytes(i), value={"key":i})
数据消费程序
# encoding:utf-8

from pyspark import SparkConf,SparkContext
from pyspark.streaming import StreamingContext
from pyspark.streaming.kafka import KafkaUtils

import json

def main():
    master = "local[*]"
    appName = "streaming_test"
    brokers="127.0.0.1:9092"
    topic='spark_streaming_test_topic'
    conf = SparkConf().setAppName(appName).set('spark.io.compression.codec','snappy')
    sc = SparkContext(master, conf=conf)
    # 构建 Streaming Context 对象
    ssc = StreamingContext(sc, 2)
    # 创建 Kafka InputDStream
    kafkaStreams = KafkaUtils.createDirectStream(ssc,[topic],kafkaParams={"metadata.broker.list": brokers})
    # 操作 DStream
    result =kafkaStreams.map(lambda line: json.loads(line[1])).map(lambda line: ("key",line.get("key",1)))
    result_count = result.reduceByKey(lambda x,y: x+y)
    result_count.pprint()
    # 启动 Spark Streaming
    ssc.start()
    ssc.awaitTermination()  


if __name__=="__main__":
    main()
任务提交到Spark集群
bin/spark-submit --jars spark-streaming-kafka-0-8-assembly_2.11-2.3.4.jar strema.py
结果输出
-------------------------------------------
Time: 2020-01-16 00:03:48
-------------------------------------------
('key', 7)

参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CoLiuRs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值