Spark知识小结

Spark知识小结

Spark中的几个常用转换算子的区别及用法

groupByKey(),reduceByKey(),aggregateByKey(),combineByKey()

  • groupByKey()

    该函数是在key-value的pairs上进行transform的,返回的rdd是一个pairs类型,此时的values是一个迭代对象,函数根据具有相同key的value进行分组,返回相同key下values的迭代对象。

  • reduceByKey()

    相对于groupByKey()而言,reduceByKey()优化了在相同partition中对相同key进行reduce,然后再在不同partition中进行reduce。输入参数也不一样,参数可以决定我们返回值的类型,通过自定义的参数Func,可以灵活的配置我们将以怎样的方式组合value。

  • aggregateByKey()

    该函数相对reduceByKey()更加灵活,比groupByKey()更加高效,因为reduceByKey()中只有combFunc,只有指定value合并的方法。aggregateByKey()函数有三个参数,zeroValue是一个初始值或者累加值;seqFunc是将累加值和相同key的values进行操作,第一个参数是累加值,第二个参数是rdd中传来的values;combFunc和reduceByKey()一样,定义如何合并相同key的values,它减少了我们每次创建数组的过程,减少了没必要的数据创建开销,同时可以返回和传入类型不同的value。

  • combineByKey()

    该函数和aggregateByKey()一样,可以返回和输入类型不同的value。执行逻辑:它会遍历rdd中的每一个元素,因此,每个元素的key要么没遇到,要么和之前出现过的某个元素的key相同。如果这是第一个元素,combineByKey()会使用一个叫做createCombiner()的函数来创建那个键对应的累加器初始值,这个过程会在每个分区的第一个元素出现时发生,如果这是一个在处理当前分区之前已经遇到过的键,它会使用mergeValues()方法将该键的累加器对应的当前值与这个新值进行合并,因为对于每一个分区都是单独处理的,因此对于同一个键在全局可能会有多个累加器,如果不同分区有两个或者多个键对应的累加器,那么就使用mergeCombiners()函数将各个分区的结果进行合并。

  • 总结:如果想要在分组后进行聚合操作,使用reduceByKey()或者aggregateByKey()会更加高效,因为groupByKey()在group的过程中,不会对每一个partition内的key进行合并,也不会在map阶段进行合并,而如果在一个分区中有相同的key,直接先合并,然后再传输到下一个rdd中,此时需要传输的数据量就会小很多,效率自然高。

Spark的Shuffle过程
  • 说明

    Shuffle操作是指在Spark操作中调用一些特殊的算子才会有的一种操作,会导致大量的数据在不同的节点之间进行传输,由此可见Shuffle过程是Spark中最复杂、最消耗性能的一种操作。

  • 举例

    reduceByKey()算子会将一个RDD中的每一个key对应的value都聚合成一个values,然后生成一个新的RDD,新RDD的元素就是<key,values>的格式,每一个key对应一个聚合而成的values。这里最大的问题在于,对于上一个RDD来说,并不是一个key对应的所有value都在一个partition中,更不可能key的所有value都在一个节点上,对于这种情况,就必须在集群中将各个节点上同一个key对应的value统一传输到一个节点上进行聚合操作,此过程会发生大量网络IO。

  • 过程

    • Shuffle Write过程

      首先,在对一个key对应的value进行聚合时,上一个Stage的MapTask必须保证将自己处理的当前分区中key相同的数据写入到同一个分区文件中,可能会有多个不同的分区文件。

    • Shuffle Read过程

      然后,下一个Stage的ReduceTask必须从上一个Stage的所有Task所在节点的分区文件中找到属于自己的分区文件,接着将属于自己的分区文件数据拉取过来,这样就可以保证每一个key对应的所有value都汇聚到一个节点上进行处理和聚合,此过程称为Shuffle。

  • 分区排序问题

    默认情况下,Shuffle操作不会对每个分区中的数据进行排序,如果想要对每个分区中的数据进行排序,可以选用以下三种方法:

    • 使用mapPartitions算子将每个分区的数据拉取出来进行排序

    • 使用repartitionAndSortWithinPartition算子在重新分区的过程中对分区内的数据进行排序

    • 使用sortByKey算子对所有分区的数据进行全局排序

      以上三种方法,mapPartitions算子代价比较小,因为不需要进行额外的Shuffle操作,sortByKey算子和repartitionAndSortWithinPartition算子可能需要进行额外的Shuffle操作,性能不是很高。

  • 引起Shuffle的算子

    • bykey类的算子:groupByKey、reduceByKey、aggregateByKey、CombineByKey、sortByKey
    • repartition类的算子:repartitionAndSortWithinPartition、partitionBy、coalesce(需要指定是否要Shuffle)
    • join类的算子:join(先groupByKey,后join不会发生Shuffle)、cogroup(尽量避免Shuffle操作)
  • 小结

    Shuffle操作是Spark中唯一最消耗性能的过程,因此也就成了最需要进行性能调优的地方,最需要解决线上报错的地方,也就是唯一可能出现数据倾斜的地方,为了实时Shuffle操作,Spark才有Stage的概念。在发生Shuffle操作的算子中,需要进行Stage划分的Shuffle操作的前半部分属于上一个Stage的范围,通常称之为MapTask,Shuffle操作的后半部分属于下一个Stage的范围,通常称为ReduceTask,其中,MapTask负责数据的组织,ReduceTask负责数据的聚合,也就是在上一个Stage的Task所在的节点上,将属于自己处理的各个分区文件拉取过来进行聚合。MapTask会将数据先保存在内存中,如果内存不够的话,就溢写到磁盘文件中保存,ReduceTask会读取各个节点上属于自己的分区磁盘文件到自己节点的内存中进行聚合。由此可见,Shuffle操作会消耗大量内存,因为无论是网络传输数据之前还是之后,都会使用大量内存中的数据结构来进行聚合操作,在聚合过程中,如果内存不足的话,只能溢写到磁盘文件中去,此过程会发生大量的网络IO,降低性能。此外,Shuffle操作过程中会产生大量的中间文件,也就是Map端产生的大量分区文件,这些文件会一直保留着,直到RDD不再被使用,被垃圾回收器回收后,才会去清理那些中间文件。所以,Spark性能的消耗主要体现在:内存的消耗、网络IO、磁盘IO。

Spark的性能优化
  • Spark的优化

    • 使用foreachPartitions代替foreach(一次函数调用处理一个partition内的所有数据)
    • 设置num-executors参数(Spark作业需要用多少个Executor进程来执行)
    • 设置executor-memory参数(每个Executor进程的内存)
    • 设置executor-cores参数(每个Executor进程的CPU的core数量)
    • 设置driver-memory参数(Driver进程的内存)
    • 设置spark.default.parallelism参数
    • 设置spark.storage.memoryFraction参数
    • 设置spark.shuffle.memoryFraction参数
  • Spark对磁盘的要求

    • 设置独立的日志分区
    • Spark磁盘临时文件自动清理
    • 通过spark.worker.cleaner.appDataTtl来设置清理的时间
  • spark-submit命令示例:

    ./bin/spark-submit \

    –master yarn-cluster \

    –num-executors 1 \

    –executor-memory 512M \

    –executor-cores 2 \

    –driver-memory 512M \

    –conf spark.default.parallelism 2 \

    –conf spark.storage.memoryFraction 0.2 \

    –conf spark.shuffle.memoryFraction 0.1

Spark数据倾斜调优
  • 调优概述

    有的时候,可能会遇到大数据计算中的一个最棘手的问题–数据倾斜,此时Spark作业的性能会比期望差很多。数据倾斜调优,就是使用各种技术方案解决不同类型的数据倾斜问题,以保证Spark作业的性能。

  • 数据倾斜发生时的现象

    • 绝大多数Task执行的都非常快,但是个别Task执行极慢。比如,总共有1000个Task,997个都在1分钟内完成了,而剩余3个却要一个多小时完成,这种情况很常见。

    • 原本正常运行的Spark作业,某天突然出现OOM(内存溢出),观察异常栈,由业务代码所致,很少见

  • 数据倾斜发生的原理

    在进行Shuffle的时候,各个节点上相同的key被拉取到某个节点上的一个Task来处理。

  • 如何定位导致数据倾斜的代码

    数据倾斜只会发生在Shuffle过程中,可能导致数据倾斜的一些算子:groupByKey()、reduceByKey()、aggregateByKey()、join、distinct、cogroup、repartition等

  • 某个Task执行特别慢的情况

    首先确定数据倾斜发生在第几个Stage中,无论使用yarn-client模式提交还是yarn-cluster模式提交,都可以在Spark Web UI上查看当前这个Stage各个Task分配的数据量,然后进一步确定是不是Task分配的不均匀导致了数据倾斜。

  • 某个Task莫名其妙出现OOM

    直接查看yarn-client模式下本地log中的异常栈或者是yarn-cluster模式下log中的异常栈,定位可能引发OOM异常的代码,结合Spark Web UI查看报错的那个Stage的各个Task的运行时间以及分配的数据量,从而确定是否是由于数据倾斜才导致了内存溢出。

  • 查看导致数据倾斜的key的分布情况

    • 如果是Spark SQL中的groupByKey、join语句导致的数据倾斜,直接查SQL中使用的表的key分布情况
    • 如果是Spark RDD执行Shuffle算子导致的数据倾斜,那么可以在Spark作业中加入查看key分布的代码,比如,RDD.countByKey(),然后将统计出来的各个key出现的次数,collect/take到客户端打印,即可看到key的分布情况。
  • 数据倾斜的解决方案

    • 使用Hive ETL预处理数据
    • 过滤少数导致数据倾斜的key
    • 提高Shuffle操作的并行度
    • 两阶段聚合(局部聚合 + 全局聚合)
    • 将reduce join 转化为map join
    • 采样倾斜key并拆分join操作
    • 使用随机前缀+扩容RDD进行join
    • 多种方案组合使用
  • Shuffle调优

Spark的TopN问题
  • 问题:如果实时展示热门文章,比如近8小时点击量最多的前100篇文章,如果是你来开发这个功能,你会具体怎么做?

    • 数据接收

      1M/s的点击并发量,需要使用分布式架构。客户端可能为了减轻服务器的压力而选择延迟合并点击请求进行批量发送。服务器采用多台机器多进程部署来接收点击请求,接收到的请求在进行参数解析后,被发送到存储单元。为了减轻存储的压力,每个进程可能会使用小窗口聚合数据,每隔一小段时间将窗口的数据聚合起来一起发送给存储单元。

    • 数据存储

      点击量是很重要的数据,用户的兴趣偏好就靠它来分析。这里使用Kafka是一个不错的选择,它的ZeroCopy机制适合处理并发量很高的请求,完全满足需求。Kafka接收数据,然后一边做实时统计,一边将数据持久化存储到HDFS。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GCkAt0VR-1590288876296)(…/图片/1575768827699.jpg)]

    • 分布式TopN算法

      用户很多,数据量很大,用户表根据哈希被分成了1024张子表。用户表里有一个score字段,表示这个用户的积分数。如果是单表,一个SQL即可搞定,现在是多个子表,此时需要在每个子表上进行一次TopN查询,然后聚合结果再做一次TopN查询。子表查询可以多线程并行执行,聚合效率比较高。

    • 滑动窗口

      8小时的滑动窗口,意味着新的数据将源源不断地进来,旧的数据在时刻被淘汰。严格来说,每条数据要严格的过期,差一秒都不行,到过期时间即被淘汰。但是从业务层面来讲,排行榜没有必要做到如此的精准,偏差几分钟是可以被允许的。业务上的折中给服务器的资源优化带来了机遇,在此可以对时间片进行切分,一分钟一个槽来进行计数,如此操作不是特别实时,会有大约一分钟的短暂的时间窗口误差,不过依然满足实际业务需求。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lp6aSQlN-1590288876300)(…/图片/1575769073430.jpg)]

    • 定时任务

      每个子节点都会有一个定时任务去负责维持统计窗口,过期失效的统计数据,计算局部的TopN热帖。每个节点都有一个局部的TopN热帖,还需要一个主节点来汇总这些局部热帖,然后计算全局热帖。主节点没必要特别实时,可以定期从子节点上拉取TopN数据,也可以让子节点主动汇报。

    • 散列

      头条的文章数量至少也在数十万,如果每个节点都对所有的文章进行点击数统计,似乎也会占用不少内存,聚合和排序也会有不小的计算量,最好的法子是每个节点只负责一部分文章的统计,如此可以明显节省计算资源。这里可以将Kafka的分区数设置为子节点的数量,让每个子节点负责消费一个分区的数据,在Kafka生产端,对点击记录的帖子ID进行散列处理,保证相同文章ID的点击流进入相同的分区,最终流向同一个统计子节点。

    • 消费者宕机

      当节点多时,节点挂掉的概率也会增大,比如电源可能掉电、人为操作失误等。如果没有做任何防范措施,当一个节点挂掉时,该节点上近8个小时窗口的统计数据将会丢失,该节点管理的局部热帖就丧失了进入全局热帖的机会。为此,我们可以定时做一下checkpoint,将当前的状态持久化到本地文件或者数据库中,因为每个节点管理的文章不会太多,所以需要序列化的内容也不会太大,当节点重启时,从持久化的checkpoint中将之前的状态恢复出来,然后继续进行消费和统计。

    • 点击去重

      一篇优秀的文章,如果它不是很短的情况,一般会吸引读者反复阅读很多次,这个计数如果完全去重不太合理,如果被人恶意反复点击明显也不好。此时,可以先从客户端着手,过滤一部分无效点击流。同一篇文章在太短时间内被当前用户反复点击,这种情况很容易被发现,如果间隔时间比较长,那就是读者的回味点击属于文章的正向反馈,应该记录下来。然后再从服务器端着手,需要对用户的行为进行状态化(增加存储压力),还需要防止用户的防刷行为(一门大型课题)。如果缺少防刷控制,一个头条号可以通过这种漏洞来使得自己的文章非法获得大量的点击量,进入热门文章列表,打上热门标签,被海量的用户看到,获得较大的经济收益,即使这篇文章内容本身的吸引力并不足够。

Spark任务的提交执行过程
  • 名词解释

    • ResourceManager

      负责将集群的资源分配给各个应用使用,分配和调度的基本单位是Container(容器),其中封装了集群资源(CPU、内存、磁盘等),每个任务只能在Container中运行,并且只能使用Container中的资源

    • NodeManager

      一个计算节点,负责启动Application所需的Container,监控资源的使用情况,并将资源的使用情况上报给ResourceManager

    • ApplicationMaster

      运行在Container中,主要负责向ResourceManager申请ApplicationMaster所需的Container,获取Container,并跟踪这些Container的运行状态和执行进度,执行完成后通知ResourceManager注销ApplicationMaster

  • spark-on-standalone(单机版)

    • Spark集群启动后,Worker向Master注册信息
    • spark-submit命令提交程序后,Driver和Application也会向Master注册信息
    • 初始化SparkContext对象时,创建DAGScheduler和TaskScheduler
    • Driver将Application信息注册给Master后,Master根据Application信息去Worker 节点启动Executor
    • Executor内部创建运行Task的线程池,然后将启动的Executor信息反向注册给Driver
    • DAGScheduler负责将Spark作业转化为Stage的DAG,根据宽窄依赖切分Stage,然后将Stage封装成TaskSet的形式发送给TaskManager,同时还会处理由于Shuffle数据丢失导致的失败
    • TaskManager维护所有的TaskSet,分发Task给各个计算节点的Executor(根据数据本地化策略分发Task),监控Task的运行状态,负责重试失败的Task
    • 任务运行完成后,SparkContext向Master申请注销自己,并释放资源
  • yarn-client模式(集群版)

    • client向ResourceManager申请启动ApplicationMaster,与此同时,在SparkContext初始化过程中创建DAGScheduler和TaskScheduler
    • ResourceManager接收到请求后,在一个NodeManager中启动第一个Container运行ApplicationMaster
    • Driver中的SparkContext初始化完成后与ApplicationMaster建立通讯,ApplicationMaster根据任务的规模向ResourceManager申请Application的资源
    • 一旦ApplicationMaster申请到资源,便与对应的NodeManager进行通讯,启动Executor,并把Executor信息反向注册给Driver
    • Driver分发Task,监控Executor的运行状态,负责重试失败的Task
    • 任务运行完成后,client向ResourceManager申请注销,同时销毁自己
  • yarn-cluster模式(集群版)

    ​ 当用户向yarn提交应用程序后,yarn将分两阶段运行应用程序:

    • 第一个阶段,将Spark的Driver作为一个ApplicationMaster在yarn中启动
    • 第二阶段,ApplicationMaster向ResourceManager申请资源,并启动Executor来运行Task,同时监控Task的整个运行流程并重试失败的Task。
Spark的常见问题
  • 详述Spark RDD(Spark Core)

    RDD简介

    RDD是Resilient Distributed Dataset的缩写,即弹性分布式数据集,是Spark的最基本抽象,是对分布式内存的抽象使用,实现了像操作本地集合一样来操作分布式数据集的抽象。RDD是Spark最核心的概念,表示已被分区、不可改变和可以并行操作的数据集合,不同的数据集格式对应不同的RDD实现,此外,RDD必须是可以序列化的。每次对RDD数据集的操作结果可以缓存到内存,后续操作可以直接从内存中读取,节省了进行MapReduce过程的磁盘IO操作时间,这对于迭代计算比较常见的机器学习算法、交互式数据挖掘来说,极大提升了效率。

    RDD特点

    1. 创建:稳定存储的数据(Hive、HBase、HDFS);通过parallelize或makeRDD方式;其他RDD的转换
    2. 操作:丰富的转换算子、动作算子和缓存算子。只有动作算子被触发时,运算才会真正被触发
    3. 只读:状态不可变,不能修改
    4. 分区:支持对key进行分区,保存到多个节点上,还原时只会计算丢失分区的数据,不会影响整个系统
    5. 路径:RDD之间的依赖关系,称为RDD的血统(Lineage),即RDD有充足的信息关于它是如何产生的
    6. 持久化:支持将会被重用RDD的缓存(缓存到内存或者溢写到磁盘)
    7. 延迟计算:延迟计算RDD,使其能够将转换管道化(pipeline transforation)

    RDD的优势

    1. RDD只能从持久存储或者转换操作获得,相对于分布式共享内存(DSM)可以实现高效容错,对于丢失的部分数据,可以根据它的血缘关系重新计算出来,而不需要做特定的checkpoint
    2. RDD的不变性,可以实现类似于MapReduce的推测执行
    3. RDD的数据分区特性,可以通过数据的本地性来提高性能
    4. RDD都是可序列化的,在内存不足时,可以自动降级为磁盘存储,将RDD存储在磁盘上,有损性能,但是不会差于MapReduce计算引擎

    RDD的内部属性

    ​ 通过RDD的内部属性,用户可以获取相应的元数据信息,从而支持更复杂的算法或优化

    1. 分区列表:通过分区列表可以找到一个RDD中包含的所有分区及其所在地址
    2. 分片函数:通过自定义函数实现对每个数据块进行RDD运算
    3. 对父RDD的依赖列表:依赖分为宽依赖和窄依赖,并非所有的RDD都有依赖
    4. 可选:K-V结构的RDD根据哈希来分区,类似于MapReduce中的Partitioner接口,控制key具体分到哪个reduce
    5. 可选:每一个分片的优先计算位置(存储的是一个表,可以将处理的分区本地化)
  • 详述Spark中的宽、窄依赖

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLqIkwV0-1590288876305)(…/博客/1577184586562.jpg)]

    宽依赖(Wide Dependencies)和窄依赖(Narrow Dependencies)。宽依赖是指父RDD的每个分区可以被多个子RDD分区所依赖,例如groupByKey()、reduceByKey()等操作,而窄依赖是指父RDD的每个分区只能被子RDD的一个分区所使用,例如map()、filter()、union()等操作。如果父RDD的一个分区被一个子RDD分区所使用就是窄依赖,否则是宽依赖。如此划分有两个作用,首先,窄依赖支持在一个节点上管道化执行,其次窄依赖支持故障还原,对于窄依赖,只有丢失的父RDD的分区需要重新计算,而对于宽依赖,Spark会在持有各个父分区的节点上,将中间数据持久化来简化故障还原,就像MapReduce会持久化map的输出一样。特别说明,对于join操作有两种情况,如果join操作使用的每个分区仅仅和已知的分区进行join,此时的join操作就是窄依赖;其他情况就是宽依赖;因为是确定的分区数量的依赖关系,所以就是窄依赖,得出一个推论:窄依赖不仅包含一对一的窄依赖,还包含一对固定个数的窄依赖。

  • Spark如何划分Stage

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v8ZqYG2t-1590288876308)(…/博客/1577184340484.jpg)]

    Stage划分的依据就是宽依赖,什么时候产生宽依赖呢,例如执行groupByKey()、reduceByKey()等算子。

    1. 从后往前推理,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到Stage中
    2. 每个Stage中的Task的数量由该Stage中最后一个RDD的分区数量决定
    3. 最后一个Stage中的任务类型是ResultTask,前面所有其他的Stage中的任务类型都是ShuffleMapTask
    4. 代表当前Stage的算子一定是该Stage的最后一个计算步骤
  • Spark中的常用算子有哪些

    1. 转换算子:map()、flatMap()、filter()、mapPartitions()、groupByKey()、reduceByKey()等

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g7K9RWbf-1590288876311)(…/博客/1577105503182.jpg)]

    2. 动作算子:count()、collect()、reduce()、first()、take()、foreach()等

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1XmmV3O1-1590288876312)(…/博客/1577105507903.jpg)]

    3. 缓存算子:cache()、persist()、unpersist()

  • Spark任务的提交流程

    Spark on Yarn:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dpw7175v-1590288876314)(…/博客/1577184764727.jpg)]

    Yarn-Client模式:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzcvlYz3-1590288876315)(…/博客/1577184781877.jpg)]

    Yarn-Cluster模式:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hv32iw2e-1590288876316)(…/博客/1577184784556.jpg)]

    整个任务的提交流程主要就是收集ApplicationMaster的上下文,比如ApplicationMaster的启动命令、资源文件、环境变量等,然后和yarn建立连接,通过yarn-client提交ApplicationMaster到yarn上运行,之后,不断向yarn轮询任务的状态直到任务运行结束。

  • Spark如何从HBase读取数据

    package com.baizhi.test
     
    import org.apache.hadoop.hbase.{HBaseConfiguration, HTableDescriptor, TableName}
    import org.apache.hadoop.hbase.client.HBaseAdmin
    import org.apache.hadoop.hbase.mapreduce.TableInputFormat
    import org.apache.spark._
    import org.apache.hadoop.hbase.client.HTable
    import org.apache.hadoop.hbase.client.Put
    import org.apache.hadoop.hbase.util.Bytes
    import org.apache.hadoop.hbase.io.ImmutableBytesWritable
    import org.apache.hadoop.hbase.mapreduce.TableOutputFormat
    import org.apache.hadoop.mapred.JobConf
    import org.apache.hadoop.io._
     
    object TestFromHBase { //从HBase读取数据并转化为RDD
      def main(args: Array[String]): Unit = {
        
        val sparkConf = new SparkConf().setAppName("HBaseTest").setMaster("local")
        val sc = new SparkContext(sparkConf)
        
        val tablename = "account"
        val conf = HBaseConfiguration.create()
        //设置zooKeeper集群地址,可以通过将hbase-site.xml导入classpath,但是建议在程序里这样设置
        conf.set("hbase.zookeeper.quorum","slave1,slave2,slave3")
        //设置zookeeper连接端口,默认2181
        conf.set("hbase.zookeeper.property.clientPort", "2181")
        conf.set(TableInputFormat.INPUT_TABLE, tablename)
     
        // 如果表不存在则创建表
        val admin = new HBaseAdmin(conf)
        if (!admin.isTableAvailable(tablename)) {
          val tableDesc = new HTableDescriptor(TableName.valueOf(tablename))
          admin.createTable(tableDesc)
        }
     
        //读取数据并转化成rdd
        val hBaseRDD = sc.newAPIHadoopRDD(conf, classOf[TableInputFormat],
          classOf[org.apache.hadoop.hbase.io.ImmutableBytesWritable],
          classOf[org.apache.hadoop.hbase.client.Result])
     
        val count = hBaseRDD.count()
        println(count)
        hBaseRDD.foreach{case (_,result) =>{
          //获取行键
          val key = Bytes.toString(result.getRow)
          //通过列族和列名获取列
          val name = Bytes.toString(result.getValue("cf".getBytes,"name".getBytes))
          val age = Bytes.toInt(result.getValue("cf".getBytes,"age".getBytes))
          println("Row key:"+key+" Name:"+name+" Age:"+age)
        }}
     
        sc.stop()
        admin.close()
      }
    }
    
  • Spark如何将数据写入HBase

    package com.baizhi.test
    
    import java.io.{IOException, PrintWriter, StringReader, StringWriter}
    import java.util.Base64
    import com.wonders.TXmltmp
    import org.apache.hadoop.hbase.HBaseConfiguration
    import org.apache.hadoop.hbase.client.{HTable, Put}
    import org.apache.hadoop.hbase.mapred.TableInputFormat
    import org.apache.hadoop.hbase.util.Bytes
    import org.apache.spark.{SparkConf, SparkContext}
    import org.xml.sax.{InputSource, SAXException}
    
    object TestToHBase { //将数据写出到BHase
      def main(args: Array[String]): Unit = {
          
        val saprkConf = new SparkConf().setAppName("Text")
        val sc = new SparkContext(saprkConf)
        
        //val dataText = "/user/hdfs/test/rdd_1000000.dat"
        val rdd = sc.textFile(args(0))
        val data = rdd.map(_.split("\\|\\|")).map{x=>(x(0),x(1),x(2))}
        val result = data.foreachPartition{x => {
          val conf= HBaseConfiguration.create()
          conf.set(TableInputFormat.COLUMN_LIST,"hbaseTest");
          conf.set("hbase.zookeeper.quorum","qsmaster,qsslave1,qsslave2");
          conf.set("hbase.zookeeper.property.clientPort","2181");
          //conf.addResource("/home/hadoop/data/lib/hbase-site.xml");
          val table = new HTable(conf,"hbaseTest");
          table.setAutoFlush(false,false);
          table.setWriteBufferSize(5*1024*1024);
          x.foreach{y => {
            try {
            val tmp = new TXmltmp
            val j1 = new String( Base64.getDecoder.decode(y ._1) )
            val j2 = new String( Base64.getDecoder.decode(y ._2))
            val xml = tmp.load(j1, j2)
    
            import javax.xml.parsers.DocumentBuilderFactory
            val foctory = DocumentBuilderFactory.newInstance
            val builder = foctory.newDocumentBuilder
            val buil = builder.parse(new InputSource( new StringReader(xml)))
            var put= new Put(Bytes.toBytes(y._3));
            put.addColumn(Bytes.toBytes("cf2"), Bytes.toBytes("age"), Bytes.toBytes(xml))
            table.put(put);table.flushCommits
              }
          catch {
            case ex: SAXException=>
            case ex: IOException=>
              println("found a unknown exception"+ ex)
              val sw:StringWriter = new StringWriter()
              val pw:PrintWriter = new PrintWriter(sw)
              ex.printStackTrace(pw)
              val error = sw.getBuffer
              sw.close()
              pw.close()
              var put= new Put(Bytes.toBytes(y._3));
            put.addColumn(Bytes.toBytes("cf2"),
                          Bytes.toBytes("name"),
                          Bytes.toBytes(error.toString))
              table.put(put);table.flushCommits}
          }}
        }}
          sc.stop()}
      }
    
  • Spark Streaming获取Kafka数据的两种消费模式以及三种消费语义

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TnA8eVm1-1590288876318)(…/博客/1577184784580.jpg)]

    • 两种消费模式
      一、基于Receiver的方式

         Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的,然后Spark Streaming启动的job会去处理那些数据。
         
         然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复,但是效率底下,并且容易导致executor内存溢出,不推荐使用。
      

      注意点:

      1. Kafka中Topic的partition与Spark中的RDD的partition是没有关系的。所以,增加kafka中Topic的分区数,只会增加Receiver的个数,就是读取Topic的线程数量,并不会增加Spark处理数据的并行度。

      2. 如果基于容错的文件系统,比如HDFS,启用了预写日志机制,接收到的数据都会被复制一份到预写日志中。因此,在KafkaUtils.createStream()中,需要指定它的持久化级别。

      3. 设置的持久化级别是StorageLevel.MEMORY_AND_DISK_SER。

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Cqd55Dm-1590288876320)(…/博客/1577109591610.jpg)]

      代码演示:

      val kafkaStream = {
       
        val sparkStreamingConsumerGroup = "spark-streaming-consumer-group"
       
        val kafkaParams = Map(
          "zookeeper.connect" -> "zookeeper1:2181",
          "group.id" -> "spark-streaming-test",
          "zookeeper.connection.timeout.ms" -> "1000")
       
        // topic名称
        val topic_name = "input-topic"
       
        // kafka的topic分区数
        val num_of_topic_partitions = 5
       
        // 启动与kafka相对应的分区数的receiver去接收数据
        val streams = (1 to num_of_topic_partitions) map { _ =>
            KafkaUtils.createStream(ssc, kafkaParams, Map(topic_name -> 1),StorageLevel.MEMORY_ONLY_SER).map(_._2)
        }
       
        // 将所有分区的数据联合
        val unifiedStream = ssc.union(streams)
       
        // 重分区(即spark并行度)
        val sparkProcessingParallelism = 1  
        unifiedStream.repartition(sparkProcessingParallelism)
      

    }

    
    二、基于Direct的方式
    
    ```tex
     Spark 1.3中引入的,从而能够确保更加健壮的机制。替代掉使用Receiver来接收数据后,这种方式会周期性地查询Kafka,来获得每个topic+partition的最新的offset,从而定义每个batch的offset的范围。当处理数据的job启动时,就会使用Kafka的简单consumer api来获取Kafka指定offset范围的数据。
    ```
    
    优点
    
       1. 简化并行读取:如果要读取多个partition,不需要创建多个输入DStream然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间有一个一对一的映射关系。
    
       2. 高性能:如果要保证零数据丢失,在基于Receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制,会对数据复制一份,而这里又会复制一份到WAL中。而基于Direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。
    
       3. 一次且仅一次的事务机制:
                       基于Receiver的方式,是使用Kafka的高级API来在Zookeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和Zookeeper之间可能不同步。
                        基于Direct的方式,使用Kafka的简单API,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。
    
          [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T0IB2h0m-1590288876322)(../博客/1577109595865.jpg)]
    
    代码演示:
    
    ~~~scala
    // topic名称
    val topics_name = Set("teststreaming")  
     
    // kafka地址
    val brokers = "localhost1:9092,localhost2:9092,localhost3:9092"  
     
    // 连接参数
    val kafkaParams = Map[String, String]("metadata.broker.list" -> brokers, "serializer.class" -> "kafka.serializer.StringEncoder")  
     
    // Create a direct stream  
    val kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics_name )   
     
    // 处理数据
    val events = kafkaStream.flatMap(line => {  
         Some(line.toString())  
    })
    
    • 三种消费语义

      消费者注册到Kafka的方式:

          subscribe:这种方式在新增topic或者partition或者消费者增加或者消费者减少的时候,会进行消费者组内消费者的再平衡。
      
          assign:这种方式注册的消费者不会进行rebalance。
      
      1. At-most-once 最多消费一次,可能会导致数据丢失
        (offset已经提交了,数据还没处理完,消费者挂了)
      
      1. At-least-once 最少消费一次,可能会导致重复消费
       (数据处理完了,offset还没提交之前,消费者挂了)
      
      1. Exactly-once 恰好消费一次,保证数据不丢失不重复消费
       (offset和数据同时处理完毕)
      
         需要保证offset提交和数据存储在同一个事务里面,即存储到同一个库,例如MySQL等等。不同库难以实现该语义。
      

      区别:
      Spark Streaming基于receiver的方式都实现了至少一次语义的消费,保证了数据不丢失,保证先输出结果,然后再提交offset到Zookeeper。

        sparkstreaming direct api的方式,offset的控制就比较灵活,加上他去了receiver大大提升了效率,受到了广泛应用。但是缺点还是有的,那就是offset,需要手动维护,常见的方式是将offset提交到zk,然而这种方式由于输出结果和offset中间还是存在步骤差,会导致数据多次处理,结果多次输出,那么要求我们保证多次输出结果不影响我们的业务。
      
  • checkpoint的使用场景以及使用方法

    • 场景

      对于一个复杂的RDD,如果担心某些关键的、后面会重复使用的RDD,可能因为节点故障,导致持久化数据的丢失,就可以针对该RDD启动checkpoint机制,实现容错和高可用。

    • 使用

      在SparkContext中调用setCheckpointDir()方法,设置一个容错的文件系统目录(比如:HDFS),然后对RDD调用checkpoint()方法,之后再在RDD所处的job运行结束之后,会启动一个独立的job来将checkpoint过的RDD的数据写入之前设置的文件系统进行持久化操作。

    • 注意

      在进行checkpoint之前,最好先对RDD执行持久化操作(比如:persist(StorageLevel.DISK_ONLY)),如果持久化了,就不用重新计算;否则如果没有持久化RDD,还设置了checkpoint,那么本来job都结束了,但是由于中间的RDD没有持久化,此时checkpoint job想要将RDD写入外部文件系统,还得将RDD之前的所有RDD全部重新计算一次,再进行checkpoint,然后从持久化的RDD磁盘文件读取数据。

  • checkpoint和持久化的区别

    • Lineage是否发生改变

      持久化只是将数据保存在BlockManager中,RDD的血缘关系不会发生变化

      checkpoint完成后,RDD已经没有之前的血缘关系了,即血缘关系发生变化

    • 丢失数据的可能性

      持久化的数据丢失的可能性比较大

      checkpoint的数据通常保存在容错且高可用的分布式文件系统中,丢失的可能性比较小

  • cache()和persist()的区别以及persist()的存储级别有哪些

    • 区别

      cache()只有一个默认的缓存级别(MEMORY_ONLY),而persist()可以根据情况设置其它的缓存级别

    • 存储级别

      MEMORY_ONLY : 将 RDD 以反序列化 Java 对象的形式存储在 JVM 中。如果内存空间不够,部分数据分区将不再缓存,在每次需要用到这些数据时重新进行计算。这是默认的级别。
      MEMORY_AND_DISK : 将 RDD 以反序列化 Java 对象的形式存储在 JVM 中。如果内存空间不够,将未缓存的数据分区存储到磁盘,在需要使用这些分区时从磁盘读取。
      MEMORY_ONLY_SER : 将 RDD 以序列化的 Java 对象的形式进行存储(每个分区为一个 byte 数组)。这种方式会比反序列化对象的方式节省很多空间,尤其是在使用 fast serializer时会节省更多的空间,但是在读取时会增加 CPU 的计算负担。
      MEMORY_AND_DISK_SER : 类似于 MEMORY_ONLY_SER ,但是溢出的分区会存储到磁盘,而不是在用到它们时重新计算。
      DISK_ONLY : 只在磁盘上缓存 RDD。
      MEMORY_ONLY_2,MEMORY_AND_DISK_2,等等 : 与上面的级别功能相同,只不过每个分区在集群中两个节点上建立副本。
      OFF_HEAP(实验中): 类似于 MEMORY_ONLY_SER ,但是将数据存储在 off-heap memory,这需要启动 off-heap 内存。
      
  • Spark中map()和flatMap()算子的区别

    map()算子会对每一条输入进行指定的操作,然后为每一条输入返回一个对象,而flatMap()算子则是两个操作的集合,先映射后扁平化(先进行映射操作,返回一个个对象,然后将所有对象合并为一个对象)。

  • Spark中partitonBy()和repartition()算子的区别

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LyeMCfzG-1590288876323)(…/博客/1577182026348.jpg)]

    • 相同点

      partitonBy()和repartition()算子都是对数据进行重新分区,默认都是使用HashPartitioner。

    • 异同点

      partitonBy()只能用于PairRDD,当它们都作用于PairRDD时,结果却不一样,一般partitonBy()的结果才是我们期望的,而repartition()底层使用一个随机数当作key,并非原来的key。

  • Spark RDD、DataSet和DataFrame的定义

    • Spark RDD

      RDD代表弹性分布式数据集,它是记录的只读分区集合,RDD是Spark的基本数据结构,它允许程序以容错的方式在大规模集群上基于内存执行计算。

    • DataFrame

      与RDD有所不同,数据组以列的形式组织起来,类似于关系型数据库的二维表,它是一个不可变的分布式数据集合,Spark中的DataFrame允许开发人员将数据结构加到分布式数据集合上,从而实现更高级级别的抽象。

    • DataSet

      Apache Spark 中的DataSet API是DataFrame API的扩展,它提供了类型安全(type-safe),面向对象的编程接口,DataFrame利用Catalyst Optimizer可以让用户通过类似于SQL的表达式对数据进行查询。

  • Spark RDD、DataSet和DataFrame的区别

    • Spark版本

      • RDD – 自Spark 1.0起
      • DataFrame – 自Spark 1.3起
      • DataSet – 自Spark 1.6起
    • 数据表示形式

      • RDD
        RDD是分布在集群中许多机器上的数据元素的分布式集合。 RDD是一组表示数据的Java或Scala对象。
      • DataFrame
        DataFrame是命名列构成的分布式数据集合。 它在概念上类似于关系型数据库中的表。
      • Dataset
        它是DataFrame API的扩展,提供RDD API的类型安全,面向对象的编程接口以及Catalyst查询优化器的性能优势和DataFrame API的堆外存储机制的功能。
    • 数据格式

      • RDD
        它可以轻松有效地处理结构化和非结构化的数据。 和Dataframe和Dataset一样,RDD不会推断出所获取的数据的结构类型,需要用户来指定它。
      • DataFrame
        仅适用于结构化和半结构化数据。 它的数据以命名列的形式组织起来。
      • DataSet
        它也可以有效地处理结构化和非结构化数据。 它表示行(row)的JVM对象或行对象集合形式的数据。 它通过编码器以表格形式(tabular forms)表示。
    • 编译时类型安全

      • RDD
        RDD提供了一种熟悉的面向对象编程风格,具有编译时类型安全性。
      • DataFrame
        如果您尝试访问表中不存在的列,则持编译错误。 它仅在运行时检测属性错误。
      • DataSet
        DataSet可以在编译时检查类型, 它提供编译时类型安全性。
        [TO-DO 什么是编译时的类型安全]
    • 序列化

      • RDD
        每当Spark需要在集群内分发数据或将数据写入磁盘时,它就会使用Java序列化。序列化单个Java和Scala对象的开销很昂贵,并且需要在节点之间发送数据和结构。
      • DataFrame
        Spark DataFrame可以将数据序列化为二进制格式的堆外存储(在内存中),然后直接在此堆内存上执行许多转换。无需使用java序列化来编码数据。它提供了一个Tungsten物理执行后端,来管理内存并动态生成字节码以进行表达式评估。
      • DataSet
        在序列化数据时,Spark中的数据集API具有编码器的概念,该编码器处理JVM对象与表格表示之间的转换。它使用spark内部Tungsten二进制格式存储表格表示。数据集允许对序列化数据执行操作并改善内存使用。它允许按需访问单个属性,而不会消灭整个对象。
    • 垃圾回收

      • RDD
        创建和销毁单个对象会导致垃圾回收。
      • DataFrame
        避免在为数据集中的每一行构造单个对象时引起的垃圾回收。
      • DataSet
        因为序列化是通过Tungsten进行的,它使用了off heap数据序列化,不需要垃圾回收器来销毁对象。
    • 效率/内存使用

      • RDD
        在java和scala对象上单独执行序列化时,效率会降低,这需要花费大量时间。
      • DataFrame
        使用off heap内存进行序列化可以减少开销。 它动态生成字节代码,以便可以对该序列化数据执行许多操作。 无需对小型操作进行反序列化。
      • DataSet
        它允许对序列化数据执行操作并改善内存使用。 因此,它可以允许按需访问单个属性,而无需反序列化整个对象。
    • 编程语言支持

      • RDD
        RDD提供Java,Scala,Python和R语言的API。 因此,此功能为开发人员提供了灵活性。
      • DataFrame
        DataFrame同样也提供Java,Scala,Python和R语言的API。
      • DataSet
        Dataset的一些API目前仅支持Scala和Java,对Python和R语言的API在陆续开发中。
    • 聚合操作(Aggregation)

      • RDD
        RDD API执行简单的分组和聚合操作的速度较慢。
      • DataFrame
        DataFrame API非常易于使用。 探索性分析更快,在大型数据集上创建汇总统计数据。
      • DataSet
        在Dataset中,对大量数据集执行聚合操作的速度更快。
    • 结论

      • 当我们需要对数据集进行底层的转换和操作时, 可以选择使用RDD。
      • 当我们需要高级抽象时,可以使用DataFrame API和Dataset API。
      • 对于非结构化数据,例如媒体流或文本流,同样可以使用DataFrame和Dataset API。
      • 我们可以使用DataFrame和Dataset 中的高级的方法。 例如,filter, map, aggregation, sum, SQL queries以及通过列访问数据等。
        如果您不关心在按名称或列处理或访问数据属性时强加架构(例如列式格式)。
        另外,如果我们想要在编译时更高程度的类型安全性。
    • 小结

      RDD提供更底层功能, DataFrame和Dataset则允许创建一些自定义的结构,拥有高级的特定操作,节省空间并高速执行。为了确保我们的代码能够尽可能的利用Tungsten优化带来的好处,推荐使用Scala的 Dataset API(而不是RDD API)。Dataset即拥有DataFrame带来的relational transformation的便捷,也拥有RDD中的functional transformation的优势。

  • Spark的定时计划怎么使用shell脚本编写以及如何修改定时计划

    • Linux环境下使用定时器

      # 1.安装crontab
      yum -y install vixie-cron
      yum -y install crontabs
      
      # 2.启停命令
      service crond start
      service crond stop
      service crond restart
      service crond reload
      service crond status
      
      # 3.查看所有定时器任务
      crontab -l
      # 4.编辑定时任务
      crontab -e
      
      # 5.添加定时器
      # crontab的时间表达式
      # 分(0-59) 时(0-23) 日(1-31) 月(1-12) 星期(1-7) command
      
      crontab -e
      0 2 * * * sh /home/spark/test.sh
      
      # (/表示频率)
      */10 * * * * sh /home/spark/test.sh
      
      # (,表示并列)
      15,45 * * * * sh /home/spark/test.sh
      
      # (-表示范围)
      15,45 8-11 * * * sh /home/spark/test.sh
      
    • Linux环境下如何编写Perl脚本

      # 1.安装perl
      yum -y install gcc gcc-c++ make autoconf libtool
      
      # 2.编写perl脚本
      vi tets.pl
      # 内容
      #!/usr/bin/perl
      use strict
      print "Hello World!\n"
      
      # 3.添加可执行权限
      chmod 764 tets.pl
      
      # 4.执行tets.pl文件
      ./tets.pl
      
    • 在Java程序中调用Linux命令

      Runtime rt = Runtime.getRuntime();
      String[] cmd = { "/bin/sh", "-c", "cd ~" };
      Process proc = rt.exec(cmd);
      proc.waitFor();
      proc.destroy();
      
    • 实例演示

      • 首先编写执行Spark任务的Perl脚本:getappinfo.pl
      #!/usr/bin/perl
       
      use strict;
       
      # 获取上一天的日期
      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time - 3600 * 24);
      # $year是从1900开始计数的,所以$year需要加上1900;
      $year += 1900;
      # $mon是从0开始计数的,所以$mon需要加上1;
      $mon += 1;
       
      print "$year-$mon-$mday-$hour-$min-$sec, wday: $wday, yday: $yday, isdst: $isdst\n";
       
      sub exec_spark
      {
          my $dst_date = sprintf("%d%02d%02d", $year, $mon, $mday);
          my $spark_generateapp = "nohup /data/install/spark-2.0.0-bin-hadoop2.7/bin/spark-submit  --master spark://hxf:7077  --executor-memory 30G --executor-cores 24  --conf spark.default.parallelism=300 --class com.analysis.main.GenAppInfo  /home/hadoop/jar/analysis.jar $dst_date > /home/hadoop/logs/genAppInfo.log &";
          print "$spark_generateapp\n";
       
          return system($spark_generateapp);
      }
       
      if (!exec_spark())
      {
          print "done\n";
          exit(0);
      }
      
      • 添加定时器任务:每天的0点30分执行getappinfo.pl
      crontab -e
      30 0 * * * /data/tools/getappinfo.pl
      
      • 脚本中的Spark程序如下:
      package com.analysis.main
       
      import org.apache.spark.SparkConf
      import org.apache.spark.sql.SparkSession
       
      object TestCrontab {
       
        // args -> 20170101
        def main(args: Array[String]) {
       
          if (args.length == 0) {
            System.err.println("参数异常")
            System.exit(1)
          }
       
          val year = args(0).substring(0, 4)
          val month = args(0).substring(4, 6)
          val day = args(0).substring(6, 8)
       
          //设置序列化器为KryoSerializer,也可以在配置文件中进行配置
          System.setProperty("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
       
          // 设置应用名称,新建Spark环境
          val sparkConf = new SparkConf().setAppName("GenerateAppInfo_" + args(0))
          val spark = SparkSession
            .builder()
            .config(sparkConf)
            .enableHiveSupport()
            .getOrCreate()
          println("Start " + "GenerateAppInfo_" + args(0))
       
          import spark.sql
       
          sql("use arrival")
          val sqlStr = "select opttime, firstimei, secondimei, thirdimei, applist, year, month, day from base_arrival where year=" + year + " and month=" + month + " and day=" + day
          sql(sqlStr).show()
       
          // 跑GenAppInfoNew
          val rt = Runtime.getRuntime()
          val cmd = Array("/bin/sh", "-c", "/data/tools/getappinfo_new.pl")
          try {
            val proc = rt.exec(cmd)
            proc.waitFor()
            proc.destroy()
            println("执行提取appinfo_new任务")
          } catch {
            case e: Exception => println("执行提取appinfo_new任务失败:" + e.getMessage())
          }
        }
      }
      
  • 说明

    这个程序首先从Hive中查询数据并展示出来,然后再调用Linux的shell执行另一个Perl脚本getappinfo_new.pl,我们可以在这个脚本中写入其他操作,完结。

  • Spark Streaming直接连接Kafka应该如何维护偏移量,如果Kafka宕机了,偏移量又该怎么来维护

    如果要做到消息端到端的Exactly Once消费,就需要事务性的处理offset和实际操作的输出。经典的做法是让offset和操作输出保存在同一个地方,会更简洁和通用。比如,consumer把最新的offset和加工后的数据一起写到HBase中,这样就可以保证数据的输出和offset的更新要么都成功,要么都失败,间接实现事务性,最终做到消息的端到端的精确一次消费。

    代码演示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2xBviXwJ-1590288876325)(…/博客/5432088-70f48dd262d24624.webp)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K9ghPf0x-1590288876327)(…/博客/5432088-8a137c1893af4c27.webp)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LRXfrL2Y-1590288876328)(…/博客/5432088-a4a8e2b9601947ce.webp)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z3DvRycc-1590288876331)(…/博客/5432088-c2813fc651ccab15.webp)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQwyvNe0-1590288876332)(…/博客/5432088-32ec53faba26d4f6.webp)]

    官方提供的思路就是,将JavaInputDStream转换为OffsetRange对象,该对象具有Topic对应的分区的所有信息,每次batch处理完,Spark Streaming都会自动更新该对象,所以你只需要找个合适的地方保存该对象(比如HBase、HDFS),就可以愉快的操纵offset了。

  • reduceByKey()和groupByKey()的区别,给定一个场景,什么情况下能使用reduceByKey()算子,而不能使用

    groupByKey()算子

    相同点:第一点:都作用于RDD;第二点:都是根据key来分组聚合;第三点:默认分区数量不变,但是可以通过参数重新指定

    不同点:groupByKey()默认没有聚合函数,得到的返回值类型是RDD[K,Iterable[V]];reduceByKey()必须传递聚合函数,得到的返回值类型是RDD[K,聚合后的V];reduceByKey()=groupByKey().map()

    区别:reduceByKey()会进行分区内聚合,然后再进行网络传输,而groupByKey()不会进行局部聚合操作

    结论:如果这两个算子都可以使用,建议使用reduceByKey()算子操作

    洗牌过程:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TpV8Lj6R-1590288876334)(…/博客/5959612-ab43f6b77f20a8ee.webp)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i8DZKOJp-1590288876337)(…/博客/5959612-15d2fc1ea81bd120.webp)]

    代码演示:

    package com.baizhi.test
    
    import org.apache.spark.rdd.RDD
    import org.apache.spark.{Partitioner, SparkConf, SparkContext}
    
    object ReduceByKeyDemo {
      def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setAppName("ReduceByClass")
        
        conf.setMaster("local[*]")//设置为本地运行
        
        //设置程序的入口
        val sc: SparkContext = new SparkContext(conf)
        
        //创建RDD
        val rdd1: RDD[Int] = sc.makeRDD(List(1,2,3,4,5))
        
        //调用groupBy方法
        val groupedRdd1: RDD[(String, Iterable[Int])] = rdd1.groupBy(t => t.toString)
    
        val rdd2: RDD[(String, Int)] = sc.makeRDD(List(("rb", 1000), ("baby", 990),
          ("yangmi", 980), ("bingbing", 5000), ("bingbing", 1000), ("baby", 2000)), 3)
      
        val groupByKeyRDD2: RDD[(String, Iterable[Int])] = rdd2.groupByKey(2)
        
        val groupByKeyRDD1: RDD[(String, Iterable[Int])] = rdd2.groupByKey()
        //调用groupByKey + mapValues 就相当于 reduceByKey方法
        val result: RDD[(String, Int)] = groupByKeyRDD1.mapValues(_.sum)
    
        val reduceByKeyRDD: RDD[(String, Int)] = rdd2.reduceByKey(_+_)
        val rdd6: RDD[(String, Int)] = rdd2.reduceByKey(_ + _)
        
        // 指定生成的rdd的分区的数量
        val rdd7: RDD[(String, Int)] = rdd2.reduceByKey(_ + _, 10)
    
        val rdd5: RDD[(String, List[Int])] = sc.makeRDD(List(("a", List(1, 3)), ("b", List(2, 4))))
        rdd5.reduceByKey(_ ++ _)
          
        sc.stop()
          
      }
    }
    

    注意:如果想查看执行结果的话,可以调用foreachPartition(println)方法,然后再toString,可以直接看到返回结果。

    • 了解:
      • combineByKey 组合数据,但是组合之后的数据类型与输入时值的类型不一样
      • foldByKey合并每一个key的所有值,在级联函数和“零值”中使用

990),
(“yangmi”, 980), (“bingbing”, 5000), (“bingbing”, 1000), (“baby”, 2000)), 3)

  val groupByKeyRDD2: RDD[(String, Iterable[Int])] = rdd2.groupByKey(2)
  
  val groupByKeyRDD1: RDD[(String, Iterable[Int])] = rdd2.groupByKey()
  //调用groupByKey + mapValues 就相当于 reduceByKey方法
  val result: RDD[(String, Int)] = groupByKeyRDD1.mapValues(_.sum)

  val reduceByKeyRDD: RDD[(String, Int)] = rdd2.reduceByKey(_+_)
  val rdd6: RDD[(String, Int)] = rdd2.reduceByKey(_ + _)
  
  // 指定生成的rdd的分区的数量
  val rdd7: RDD[(String, Int)] = rdd2.reduceByKey(_ + _, 10)

  val rdd5: RDD[(String, List[Int])] = sc.makeRDD(List(("a", List(1, 3)), ("b", List(2, 4))))
  rdd5.reduceByKey(_ ++ _)
    
  sc.stop()
    
}

}


 注意:如果想查看执行结果的话,可以调用foreachPartition(println)方法,然后再toString,可以直接看到返回结果。

+ 了解:
  +  combineByKey 组合数据,但是组合之后的数据类型与输入时值的类型不一样
  +  foldByKey合并每一个key的所有值,在级联函数和“零值”中使用

##### 续
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值