大数据常见面试题
- 一、Hadoop生态
- 二、Spark技术栈
- 2.1 scala
- 2.2 Spark Core
- 2.2.1 Spark与MapReduce Shuffle的异同
- 2.2.2 spark如何处理数据倾斜
- 2.2.3 spark rdd的宽窄依赖
- 2.2.4 groupByKey/reduceByKey/combineByKey
- 2.2.5 spark 优化 重点 **************spark 优化spark 优化spark 优化
- 2.2.6 spark有几种部署模式
- 2.2.7 spark 架构 ------------------driver------executor---------stage------task--------------------
- 2.2.8 spark 的提交流程 **********************************spark的提交流程
- 2.2.9 spark提交作业参数
- 2.2.10 spark和hadoop都是并行计算,有什么相同和区别
- 2.2.11 spark中rdd概念和特征
- 2.2.12 spark如何处理内存溢出?-----------------
- 2.2.13 Spark中的 checkpoint ---------------------checkpoint----checkpoint----checkpoint
- 2.2.14 spark的shuffle方式
- 2.2.15 spark中广播变量的作用
- 2.2.16 Spark Join 的优化经验
- 2.3 spark sql
- 2.4 SparkStreaming
- 三、核心组件
- 3.1 flume
- 3.2 kafka
- 3.2.1 kafka和传统队列消息的区别
- 3.2.2 kafka的应用场景
- 3.2.3 kafka在高并发的情况下,如何避免数据丢失
- 3.2.4 kafka 到 spark streaming 怎么保证数据完整性,怎么保证不重复消费
- 3.2.5 kafka的消费者高阶和低阶API有什么区别
- 3.2.6 kafka怎么保证数据消费一次且仅消费一次
- 3.2.7 kafka保证数据一致性和可靠性
- 3.2.8 spark实时作业宕掉,kafka指定的topic数据堆积怎么办?
- 3.2.9 acks
- 3.2.10 kafka读写流程
- 3.2.11 kafka为什么只让leader进行读写
- 3.2.12 为了避免磁盘被沾满,kafka会周期性的删除旧消息,请问删除策略有哪些?控制粒度到什么程度?请具体描述一下?
- 3.2.13 kafka 数据高可用的原理
- 3.2.14 kafka offset存放在哪儿?
- 3.2.15 如何保证kafka的消息有序
- 3.2.16 kafka的分区数
- 3.2.17 kafka消费者的分区分配策略
- 3.2.18 kafka中数据量计算
- 3.2.19 kafka 消息数据积压,kafka消费能力不足怎么处理?
- 3.2.20 kafka监控
- 3.2.21 kafka高吞吐的实现
- 五、大数据算法题
- 六、算法理论
一、Hadoop生态
1.1 HDFS
1.1.1 hdfs读写流程
写数据流程:
读数据流程:
读文件
下载:
1.请求下载第一个块,
2.NameNode检查路径是否存在,不存在抛异常,存在返回DataNode列表
3.返回DataNode列表[DN1,DN2,DN3]
4.建立连接,下载第一个数据块
注:
1下载后面的数据块重复1~4步骤,client对数据块进行连接,形成一个完整的文件.
2.在传输的过程中,也是以packet形式进行传输,默认大小64KB
1.1.2 NameNode、DataNode的作用
- NameNode
1.集群的管理制度,Hdfs的元数据节点
2.管理Hdfs的文件目录树,管理数据块(Block)映射信息及副本信息,一个文件对应的块的名字以及块被存储到哪里,一个文件备份的多少都是由NameNode来管理
3.处理客户端的读写请求 - DataNode
slave,实际存储块的节点
执行数据块的读/写操作
1.3 hive
1.3.1 hive中内部表、外部表的区别
内部表不会被 external 修饰,而被 external 修饰的就是外部表
区别:
- 内部表数据由Hive自身管理,外部表数据由HDFS管理;
- 内部表数据存储的位置是hive.metastore.warehouse.dir(默认:/user/hive/warehouse),外部表数据的存储位置由自己制定(如果没有指定LOCATION,Hive将在HDFS上的/user/hive/warehouse文件夹下以外部表的表名创建一个文件夹,并将属于这个表的数据存放在这里);
- 删除内部表会直接删除元数据(metadata)及存储数据;删除外部表仅仅会删除元数据,HDFS上的文件并不会被删除;
- 对内部表的修改会将修改直接同步给元数据,而对外部表的表结构和分区进行修改,则需要修复(MSCK REPAIR TABLE table_name;)
1.3.2 hive中的动态和静态分区
静态分区与动态分区的主要区别在于静态分区是手动指定,而动态分区是通过数据来进行判断。
- 静态分区:
在编译期间指定分区名
支持load和insert两种插入方式
适用于分区数较少,分区名可以明确确定的数据 - 动态分区:
根据分区字段的实际值,动态进行分区
在sql执行的时候进行分区
需要开启动态分区设置
只支持insert方式
动态分区可以通过下面的设置来打开:
//设置为true表示开启动态分区功能(默认为false)
set hive.exec.dynamic.partition=true;
//设置为nonstrict,表示允许所有分区都是动态的(默认为strict)
set hive.exec.dynamic.partition.mode=nonstrict;
动态分区:
//注意输入字段的最后面必须是动态分区字段。需要在select中查出分区的字段
insert [overwrite] table tbl_name partition(pt, if_online)
select field1, field2, ..., pt, if_online
from tbl
where xxx;
静态分区:
//不需要在select中查出分区的字段,已指定值
insert [overwrite] table tbl_name partition(pt=20121023, if_online=1)
select field1, field2, ..., fieldn
from tbl
where xxx;
注意分区细节:
(1)尽量不要用动态分区,因为动态分区的时候,将会为每一个分区分配reducer数量,当分区数量多的时候,reducer数量将会增加,对服务器是一种灾难。
(2)动态分区和静态分区的区别,静态分区不管有没有数据都将会创建该分区,动态分区是有结果集将创建,否则不创建。
(3)设置hive动态分区的严格模式:
hive提供我们一个严格模式:为了阻止用户不小心提交恶意hql
hive.mapred.mode=nostrict : strict
如果该模式值为strict,将会阻止以下三种查询:
(1)对分区表查询,where中过滤字段不是分区字段。
(2)笛卡尔积join查询,join查询语句,不带on条件或者where条件。
(3)对order by查询,有order by的查询不带limit语句。
1.3.3 Hive和Mysql有什么区别,大数据为什么不用mysql做存储和数据处理
- 查询语言:hive使用hql,mysql使用sql
- 数据存储位置:hive把数据存储在hdfs上,mysql存储在自己定义的系统中
- 数据格式:hive数据格式可以用户自定义,mysql有自己的系统定义格式
- 延迟性:hive延迟性高,mysql延迟性低
- 数据规模:hive数据存储量超级大,mysql只存储一些少量的业务数据
- 底层执行原理:hive底层是MR,mysql是executor执行器
1.3.4 hive如何调优 -----------hive调优hive调优hive调优
- 设置本地模式: 有时候Hive处理的数据量非常小,那么在这种情况下,为查询出发执行任务的时间消耗可能会比实际job的执行时间要长,对于大多数这种情况,hive可以通过本地模式在单节点上处理所有任务,对于小数据量任务可以大大的缩短时间
set hive.exec.mode.local.auto=true; --default false
set hive.exec.mode.local.auto.inputbytes.max=50000000
set hive.exec.mode.local.auto.input.files.max=5 --default 4
- 并行执行: 通过设置参数
hive.exec.parallel 值为 true
,就可以开启并发执行。不过,在共享集群中,需要注意下,如果 job 中并行阶段增多,那么集群利用率就会增加。
set hive.exec.parallel=true;//打开任务并行执行
set hive.exec.parallel.thread.number=16;//同一个 sql 允许最大并行度,默认为 8
- JVM重用: hadoop生态系统底层是java开发的,基于JVM运行,因此可以设置JVM重用来优化hadoop和hive。
hadoop默认配置是派生JVM来执行map和reduce任务的,这时JVM的启动可能会造成很大的开销,尤其是执行的job包含上千个task的时候,JVM重用可以使得JVM实例在同一个job中重新使用N次。这个参数可以在hadoop的安装目录下的mapred-site.xml文件中配置:
<property>
<name>mapred.job.reuse.jvm.num.tasks</name>
<value>10</value>
</property>
-
严格模式 :可以防止用户执行那些可能产生意想不到的不好的影响查询
-
合理设置map和reduce的数量
hive通过将查询划分为一个或者多个MR任务达到并行化的目的,每个任务都可能具有多个mapper和reducer任务,其中一些是可以并行执行的,确定最佳的mapper个数和reducer个数取决于多个变量,例如输入的数据量的大小以及对这些数据操作的类型等。
设置 太多 的mapper与reducer个数,就会导致启动阶段,调度与运行job的过程中产生过多的开销;
设置 太少 ,那么就可能没有充分利用好集群的并行性
hive会根据输入的数据量来分配reducer的个数,我们可以通过参数hive.exec.reducers.bytes.per.reducer
来设置每个reducer的数据量大小,默认是1G,将该值调大,可以减少reducer的数量,调小,可以增加 reducer的数量。
Reducer设置的原则:
每个Reduce处理的数据默认是256MB:hive.exec.reducers.bytes.per.reducer=256000000
每个任务最大的reduce数,默认为1009:hive.exec.reducers.max=1009
计算reduce数的公式:N=min(参数2,总输入数据量/参数1)
设置Reducer的数量:set mapreduce.job.reduces=n
-
Fetch抓取
-
explain执行计划: 通过查看执行计划来调整sql语句
-
防止小文件过多造成资源的多度占用以及影响查询效率
原因:小文件在HDFS中存储本身就会占用过多的内存空间,那么对于MR查询过程中过多的小文件又会造成启动过多的Mapper Task, 每个Mapper都是一个后台线程,会占用JVM的空间
比如:
在Hive中,动态分区会造成在插入数据过程中,生成过多零碎的小文件(请回忆昨天讲的动态分区的逻辑)
不合理的Reducer Task数量的设置也会造成小文件的生成,因为最终Reducer是将数据落地到HDFS中的
解决方案:
①在数据源头HDFS中控制小文件产生的个数,比如:采用Sequencefile作为表存储格式,不要用textfile,在一定程度上可以减少小文件(常见于在流计算的时候采用Sequencefile格式进行存储)
②减少reduce的数量(可以使用参数进行控制)
③慎重使用动态分区,最好在分区中指定分区字段的val值
④最好数据的校验工作,比如通过脚本方式检测hive表的文件数量,并进行文件合并
⑤合并多个文件数据到一个文件中,重新构建表
1.3.5 Hive中sort by、order by、distribute by、cluster by
- 使用order by会引发全局排序
- 使用distribute和sort进行分组排序
- 被distribute by设定的字段为KEY,数据会被HASH分发到不同的reducer机器上,然后sort by会对同一个reducer机器上的每组数据进行局部排序
- distribute+sort的结果是按组有序而全局无序的
1.3.6 生产环境为什么建议使用外部表
- 外部表不会加载数据到Hive,减少数据传输,数据还能共享
- Hive不会修改数据,所以无需担心数据的损坏
- 删除表时,不会删除数据,只删除表结构
1.4 hbase
1.4.1 为什么用hbase存储
- Hbase是一个高可靠、高性能、可伸缩、面向列的分布式数据库,非关系型数据库。
- Hadoop的hdfs提供了高可靠性的底层存储支持
- Hadoop的mr为hbase提供了高性能的计算能力
- Zk为hbase提供了稳定性
- Hive可与hbase结合使用
- Sqoop可以为hbase提供方便的RDBMS数据导入功能,是传统数据库向hbase中迁移变得容易
- Spark高性能的内存分布式计算引擎也可能帮助对hbase中的数据进行分析处理
1.4.2 rowkey 怎么设计
- 长度原则
最大长度64k, 实际应用中一般为10-100byte,以byte[]形式保存,一般设计定长。建议越短越好,不要超过16个字节
( 原因: ①数据的持久化文件HFile按照kv存储,如果rowkey过长,数据量大的时候,仅rowkey就会占很大内存,影响HFile的存储效率;
② Memstore会缓存部分数据到内存中,若rowkey过长,内存的有效利用率就会降低,就不能缓存更多的数据,从而降低检索效率
③目前操作系统都是64位,内存8字节对齐,控制在16字节,8字节的整数倍,利用了操作系统的最佳特性 ) - 唯一原则
保证Rowkey的唯一性。 - 排序原则
Hbase的rowkey是按照ASCII有序排序的,因此我们在设计rowkey的时候要充分利用这点 - 散列原则
设计的rowkey应均匀的分布在各个hbase节点上(避免热点问题)
1.4.3 rowkey 的热点问题
rowkey的设计原则是根据ASCII字典顺序进行全局排序的,如果rowkey的设计是一序列连续有序的数字,那么hbase region sever就会认为这些数据是在一块的,他就会把这一类型的有序数据全部都放到一个或少数几个节点上,造成少数region server的读/写请求过多、负载过大,而其他region server负载却很小,就造成了“热点”现象
1.4.4 rowkey 设计如何避免热点问题
- Reverse反转
更加的随机rowkey
缺点:牺牲了rowkey的有序性 - Salt 加盐
每一个rowkey前加前缀,随机字符,使得数据分布在不同的region上,打到region的负载均衡 - Hash散列或者Mod
用Hash散列来替代随机Salt前缀的好处是能让一个给定的行有相同的前缀,这在分散了Region负载的同时,使读操作也能够推
断。确定性Hash(比如md5后取前4位做前缀)能让客户端重建完整的RowKey,可以使用get操作直接get想要的行。
例如将上述的原始Rowkey经过hash处理,此处我们采用md5散列算法取前4位做前缀,结果如下:
9bf0-abc001 (abc001在md5后是9bf049097142c168c38a94c626eddf3d,取前4位是9bf0)
7006-abc002
95e6-abc003
若以前4个字符作为不同分区的起止,上面几个Rowkey数据会分布在3个region中。实际应用场景是当数据量越来越大的时候,
这种设计会使得分区之间更加均衡。
如果Rowkey是数字类型的,也可以考虑Mod方法。
1.4.5 hbase 的优化 ******************hbase优化hbase优化hbase优化
- 表设计
建表时就分区(预分区),rowkey设置定长(64字节),列簇 2到3个 - 写表
多Htable并发,提高吞吐量
Htable参数设置,手动flush, 降低IO
writeBuffer的大小
批量写,减少网络IO开销
多线程并发写,结合定时flush和写buffer,可以既保证在数据量小的时候,数据可以在较短时间内被flush(1秒内),同时又保证在数据量大的时候,写buffer一满就及时进行flush - 读表
多Htable并发,提高吞吐量
Htable参数设置
批量读
释放资源
缓存结果查询
1.4.6 为什么hbase读写比较快
-
读:
通过rowkey可以快速地位到在那个region上,位置信息保存在hbase的meta表里
读取速度快是因为它使用了LSM树型结构
HBase读取首先会在缓存(BlockCache)中查找,它采用了LRU(最近最少使用算法),如果缓存中没找到,会从内存中的MemStore中查找,只有这两个地方都找不到时,才会加载HFile中的内容
HBase会将数据保存到内存中,在内存中的数据是有序的,如果内存空间满了,会刷写到HFile中,而在HFile中保存的内容也是有序的
实时查询,可以认为是从内存中查询,一般响应时间在1秒内。 -
写:
HBase的机制是数据先写入到内存中,当数据量达到一定的量(如128M),再写入磁盘中, 在内存中,是不进行数据的更新或合并操作的,只增加数据,这使得用户的写操作只要进入内存中就可以立即返回,保证了HBase I/O的高性能。
二、Spark技术栈
2.1 scala
2.1.1 scala 偏函数
偏函数数学概念,它接受一个类型为A的参数,返回一个类型为B的结果
val pf:PartialFunction[Int,String] = {
case 1 => "one"
case 2 => "two"
case 3 => "three"
}
scala> pf(1)
res0:String = one
2.1.2 scala 柯理化
把一个接受多参数的函数转化成接受其中几个单一参数的函数
2.1.3 scala 的apply和unapply方法是什么作用
- apply ,在一个类的伴生对象中定义apply方法,在生成这个类的对象时,就省去了new关键字
- unapply,是apply的反向操作,接受一个对象,从中提取值
2.1.4 scala 定义元组后可变吗
不可变,可以包含不同的数据类型,最大长度22
2.1.5 java和scala的区别
scala源于java,最终被编译成.class文件运行在JVM虚拟机中,本质上还是java,可以互相调用API
- scala支持函数式编程,可以使用高阶函数
- scala没有static静态概念,使用单例对象object来实现
- scala中类自动带有getter / setter 方法
- scala中流程控制,没有break,可以导包break,用return
- scala 中 特质 trait 可以类比java中的接口,和java中的接口不同,scala中的特质可以包含带有方法体的方法
2.1.6 scala中的隐式函数 implicit
scala 在面对编译出现错误时,提供了一个由编译器自我修复的机制,编译器试图去寻找一个隐式的implicit 转换方法,转换出正确的类型,完成编译
2.1.7 trait(特质)和abstract class(抽象类)的区别?
- 一个类只能继承一个抽象类,但是可以通过with关键字继承多个特质
- 抽象类有带参数的构造函数,特质不行(如:
trait t(i: Int){}
,这种声明是错误的)
2.1.8 Scala 中 case class 与 class 的区别
class 类似Java中的class
case class 被称为样例类,是一种也输的类,常被用于模式匹配
具体区别:
- 初始化的时候可以不用new ,也可以加上,但是普通类必须加上new
- 默认实现了equals、hashCode方法
- 默认是可以序列化的,实现了Serializable
- case class 构造函数参数是public的,我们可以直接按访问
- case class默认情况下不能修改属性值
- case class 最重要的功能,支持模式匹配,这也是定义case class的重要原因
2.2 Spark Core
2.2.1 Spark与MapReduce Shuffle的异同
- MR:基于磁盘计算,如果数据量过大,磁盘io就会产生过大,性能会降低,计算速度变慢,并且MR的shuffle计算默认是需要进行分组排序的。每个数据都要分到相同的分区,还要排序,资源消耗太大
- Spark:基于内存,内存写满,写到磁盘,速度快,并且sparkshuffle可以不进行排序操作,利用hashshuffle和consolidation机制设置。Shuffle计算可以迭代,大大提高性能,缩短计算时间。
2.2.2 spark如何处理数据倾斜
什么是数据倾斜?
spark中数据倾是指在数据处理的时候,由于spark单个patition中的数据分布不均,导致大量的数据集中不到一台或某几台计算节点上,导致处理速度远低于平均计算速度,数据集中的几台机器很忙,而其他几台数据很少的机器很闲,这样影响整个集群计算性能。
数据倾斜的产生?
在spark中当一个RDD的数据需要被多个子RDD所使用的时候,我们需要进行shuffle将数据打散,把数据均匀的分配给子RDD进行并行计算,Shuffle过程中spark默认使用 HashPartitioner 对数据进行分区,在这个过程中可能由于我们的数据分布不均,我们在进行hash取摸的时候,并行度设置不足,导致多数据分配到一个task上,导致倾斜,或者就是相同key的数据hash取摸之后就是比较大,分配同一个task导致数据倾斜等,对于这行情况我们分以下场景进行解决
怎么处理spark中的数据倾斜?重点 --------数据倾斜数据倾斜数据倾斜
一般产生数据倾斜有两种表现形式:
- 大部分task,都执行的很快,剩下几个task,执行的特别特别慢,卡着半天都不动,等着可能要执行1个小时,2个小时才能执行完一个task,这时肯定就是发生数据倾斜了。
- 执行程序的时候,其他task都很快执行完了,但是有个task,突然间报错:
JVM Out Of Memory
,内存溢出了,task failed,反复执行总是会挂掉,这时就基本可以判断是由数据倾斜引起的,因为某一个分区大量数据堆积,task每处理一条数据,就要创建大量的对象,导致内存溢出了。
首先要定位数据倾斜
可以通过Spark日志服务器xxxxx:18088者yarn的UI进入到应用xxxx:8088,进入相应的Spark UI界面,查看stage,如果看到某一个stage执行的时间很长,就可以判定数据倾斜的这个stage
这时可以去log日志查看哪一行代码,导致了OOM异常;或者查看log中,执行到了第几个stage,从而判断是哪个suffle算子,一般常规的suffle算子如 reduceByKey、countByKey、groupByKey、join 等,根据代码逻辑判断是否会出现数据倾斜
解决办法?*********************************************************************
-
比较通用的方法,提高reduce并行度
比如说,rduceBykey中有一个shuffle read task的值默认为200,一次用两百个task来处理任务,那么我设置reduceBYkey(1000),这个时候task的数量就多了,然后分配到每个task中的key就少了,从而减少每个task处理的数据量,避免oom。
但是他有一点的局限性,map 端不断地写入数据,reduce task 不断地从指定位置读取数据,如果 task 很多,读取的速度就会增加,但是每个 key 对应的 reduce 处理的总量没变,所以它并没有从根本上解决数据倾斜的问题,只是尽量去减少 每个task中的key的数据量,适用于 较多 key 对应的数据量都很大的问题; -
对于 聚合类的shuffle 操作,可以使用随机 key 进行双重聚合
由于一个 key 对应的数据量太大,我先给这个 key 加个随机数,num_key,强行把一个 key 变成 多个 key,这样每个 key 的数据量减小,然后按 num_key 进行聚合,聚合之后,把 num_key 再转回 key,然后对 key 再次聚合;
这种方法适合于 reduceByKey、groupByKey 等算子,不适合 join 算子 -
对于join算子,将 reduce join 变成 map join
正常情况下,join 会产生 shuffle 过程,而且是 reduce join,即先将相同 key 对应的 value 汇聚到一个 reduce task 中,再进行 join
这种方法是有假定的前提的条件的,比如有两个rdd进行join操作,其中一个rdd的数据量不是很大,比如低于1个G的情况,就可以采用 广播小 RDD + map 大 RDD 实现 join 功能,
具体操作是就是选择两个rdd中那个比较数据量小的,然后我们把它拉到driver端,再然后通过广播变量的方式给他广播出去,这个时候再进行join 的话,因为数据都是在同一Executor中,此时再对另外一个map类算子执行map操作,连接两个rdd中key相同的值,这当中不会产生shuffle的过程,也就避免了数据倾斜,这时数据就可以按照需要的方式去处理
object MapJoinTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("WordCount")
val sc = new SparkContext(conf)
val lista=Array(
Tuple2("001","令狐冲"),
Tuple2("002","任盈盈")
)
//数据量小一点
val listb=Array(
Tuple2("001","一班"),
Tuple2("002","二班")
)
val listaRDD = sc.parallelize(lista)
val listbRDD = sc.parallelize(listb)
//val result: RDD[(String, (String, String))] = listaRDD.join(listbRDD)
//设置广播变量
val listbBoradcast = sc.broadcast(listbRDD.collect())
listaRDD.map( tuple =>{
val key = tuple._1
val name = tuple._2
val map = listbBoradcast.value.toMap
val className = map.get(key)
(key,(name,className))
}).foreach( tuple =>{
println("班级号"+tuple._1 + " 姓名:"+tuple._2._1 + " 班级名:"+tuple._2._2.get)
})
}
}
当然了,这种方法也是有缺陷的,比如两个rdd都非常大,比如超过了10个G,这个时候我们就不能用这种方法了,因为数据量太大了,广播变量还是需要太大的消耗,这时就不合适了。
对比 hive 中的数据倾斜 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
产生的原因?
- key值分布不均匀
- 业务数据本身的特性
- 建表时考虑不周
- 某些SQL语句本身就存在数据倾斜
解决办法?
- map端输入数据文件多,大小不均匀 -> 执行前合并小文件
- 大表join小表 -> 小表在左;小表加载到内存中
- join时关联的字段信息有null值 -> 子查询过滤掉空值;修复空的值,使用case when给空值随机分配key值
- 不同数据类型关联 -> 将数据类型统一
- 使用count(distinct) -> 使用sum、group by
2.2.3 spark rdd的宽窄依赖
宽依赖往往对应shuffle操作,需要在运行过程中将同一个父RDD的分区传入到不同的子RDD分区中,中间可能涉及多个节点之间的数据传输;
而窄依赖是指每个父RDD的分区只会传入到一个子RDD分区中,通常可以在一个节点内完成转换
当RDD分区丢失时,对于窄依赖来说,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重新计算与子RDD分区对应的父RDD分区就行。这个计算对数据的利用是100%的
当RDD分区丢失时,对于宽依赖来说,重算的父RDD分区只有一部分数据是对应丢失的子RDD分区的,另一部分就造成了多余的计算。宽依赖中的子RDD分区通常来自多个父RDD分区,极端情况下,所有父RDD都有可能重新计算。
窄依赖的函数有:map, filter, union, join(父RDD是hash-partitioned ), mapPartitions, mapValues
宽依赖的函数有:groupByKey, join(父RDD不是hash-partitioned ), partitionBy
2.2.4 groupByKey/reduceByKey/combineByKey
- reducebyKey会先在本地机器上进行局部聚合,然后在移动数据,进行全局聚合,shuffle之间进行聚合。在map端先进行聚合
- groupbyKey会先将数据进行移动,再做聚合
- combineByKey:比较底层的算子,reduceByKey底层就使用了combinebykey
2.2.5 spark 优化 重点 **************spark 优化spark 优化spark 优化
- 避免创建重复的RDD,对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据
- 尽可能复用同一个RDD
- 对多次使用的RDD进行持久化
cache() 和 persisit() 区别
如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可
- cache()方法 :是persist()的一种简化方式,底层调用 persisit(MEMORY_ONLY),使用非序列化的方式将RDD中的数据全部尝试持久化到内存中,此时再对rdd1执行两次算子操作时,只有在第一次执行map算子时,才会将这个rdd1从源头处计算一次,第二次执行reduce算子时,就会直接从内存中提取数据进行计算,不会重复计算一个rdd
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache()
rdd1.map(...)
rdd1.reduce(...)
- persist()方法:手动选择持久化级别,并使用指定的方式进行持久化。比如说,StorageLevel.MEMORY_AND_DISK_SER表示,内存充足时优先持久化到内存中,内存不充足时持久化到磁盘文件中。而且其中的_SER后缀表示,使用序列化的方式来保存RDD数据,此时RDD中的每个partition都会序列化成一个大的字节数组,然后再持久化到内存或磁盘中。序列化的方式可以减少持久化的数据对内存/磁盘的占用量,进而避免内存被持久化数据占用过多,从而发生频繁GC。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)
rdd1.map(...)
rdd1.reduce(...)
- 尽量避免使用shuffle类算子,比如Broadcast与map进行join代码示例
- 使用map-side预聚合的shuffle操作:map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。通常来说,在可能的情况下,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。
- 使用高性能的算子
使用reduceByKey/aggregateByKey替代groupByKey
使用mapPartitions替代普通map,mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些,容易导致OOM
使用foreachPartitions替代foreach - 广播大变量:有时在开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能
2.2.6 spark有几种部署模式
- 本地模式: 以多线程的方式运行在本地,一般是方便调试用
local: 只启动一个 executor
local[K] :启动k个executor
local[*]:启动和cpu数目相同的executor - Standalone模式: 分布式部署集群,自带完整的服务,资源管理和任务监控是spark自己监控
- Spark on yarn模式: 分布式部署在集群,资源和任务监控交给yarn管理,spark客户端直接连接yarn,不需要额外构建spark集群。有yarn-client和yarn-cluster两种模式:
yarn-client:适合调试,driver运行在客户端
yarn-cluster:适合生产,driver运行在集群子节点,具有容错功能
2.2.7 spark 架构 ------------------driver------executor---------stage------task--------------------
spark架构主要由以下组件构成:
- Application: 建立在 Spark上的用户程序,包括 Driver代码和运行在集群各节点 Executor中的代码
- Driver program: 驱动程序,作业的主进程,具有main函数,使用 Application中的main函数并创建 SparkContext,是程序的入口点
功能: 负责向集群申请资源,向master注册信息,负责了作业的调度,负责作业的解析,生成了Stage并调度Task到Executor上,包括DAGScheduler,TaskScheduler - Cluster Manager : 在集群(Standalone、Mesos、YARN)上获取资源的外部服务
- Worker Node: 集群中任何可以运行 Application代码的节点
- Executor : 某个 Application运行在 worker节点上的一个进程
- Job : Spark Action算子 触发生成,执行一个rdd的action的时候,会产生一个job,一个job又由多个 Task 组成并行计算,一个 Application中往往会产生多个 Job,
- Stage: 每个Job会被拆分成多组 Task,作为一个 TaskSet,其名称为 Stage
- Task: 被送到某个 Executor上的工作单元
rdd有多少个partition,就会有多少个task,因为一个task只处理一个partition上的数据
Master,Worker 是物理节点
Driver,Executor 是进程
2.2.8 spark 的提交流程 **********************************spark的提交流程
Standalone-Client 方式提交任务
- Client 模式下提交任务,在客户端启动Driver进程
- Driver会向Master申请启动Application启动的资源
- 资源申请成功,Driver端将Task发送到Worker端执行
- Worker将Task执行结果返回到Driver端
Standalone-Cluster 方式提交任务
- Standalone-Cluster模式提交App后,会向Master请求启动Driver
- Master接受请求后,随机在集群中一台节点启动Driver进程
- Driver启动后为当前的应用程序申请资源
- Driver端发送task到worker节点上执行
- worker将执行情况和执行结果返回给Driver端
yarn-cluster 方式提交任务
spark-submit --master yarn --deploy-mode cluster --class org.apache.spark.examples.SparkPi …/examples/jars/spark examples_2.11-2.3.1.jar 10000
执行流程
- 客户端提交Application应用程序,发送请求到RS(ResourceManager), 请求启动AM(ApplicationMaster)。
- RS收到请求后随机在一台NM(NodeManager)上启动AM(相当于Driver端)。
- AM启动,AM发送请求到RS,请求一批container用于启动Executor。
- RS返回一批NM节点给AM。
- AM连接到NM,发送请求到NM启动Executor。
- Executor反向注册到AM所在的节点的Driver。Driver发送task到Executor。
yarn-client 方式提交任务
spark-submit --master yarn --deploy-mode client --class org.apache.spark.examples.SparkPi …/examples/jars/spark examples_2.11-2.3.1.jar 10000
2.2.9 spark提交作业参数
Executor-cores ---- 默认1 建议2-5 使用3
Num-executors ---- 启动executors的数量,默认2
Executor-memory ---- executor内存大小 默认1G
Driver-cores ---- driver使用内核数, 默认1
Driver-memory ---driver内存大小,默认512M
2.2.10 spark和hadoop都是并行计算,有什么相同和区别
两者都是用mr模型来进行并行计算
- hadoop的一个作业称为job,job里面分为 map task 和 reduce task,每个task都是在自己的进程中运行的,当task结束时,进程也会结束
- spark用户提交的任务成为application,一个application对应一个sparkcontext,app中存在多个job,每触发一次action操作就会产生一个job
- hadoop的job只有map和reduce操作,表达能力比较欠缺而且在mr过程中会重复的读写hdfs,造成大量的io操作,多个job需要自己管理关系
- spark的迭代计算都是在内存中进行的,API中提供了大量的RDD操作如join,groupby等,而且通过DAG图可以实现良好的容错
2.2.11 spark中rdd概念和特征
RDD是 弹性分布式数据集,代表一个不可变、可分区、并行计算的元素集合
RDD五大特性:
- RDD是由多个分区构成的,每个Partition都有一个唯一索引编号
- 每个分区上都有compute函数,计算该分区中的数据
- RDD有依赖性,通常情况下一个 RDD是来源于另一个 RDD,RDD会记录下这些依赖,方便容错。也称 DAG
- 只有 Key-Value 类型的 RDD才有分区器 ,可以传递一个自定义的 Partitioner 进行重新分区,非 Key-Value类型的 RDD(PairRDD)分区器的值是 None
- 分区优先位置列表,对于一个 HDFS文件来说,这个列表保存了每个分区所在的数据块的位置。按照 “移动数据不如移动计算的” 的理念, Spark在进行任务调度的时候,会尽可能的将计算任务移动到所要处理的数据块的存储位置。
2.2.12 spark如何处理内存溢出?-----------------
- Driver端内存溢出
增大driver的内存参数: spark.driver.memory(default 1g)
spark程序中,sparkcontext和DAGScheduler都是运行在driver端的,对应rdd的stage切分也是在driver端运行的 - Map过程产生大量对象导致内存溢出
减少每个task的大小,即在map操作之前repartition,分区成更小的块传入map,不能使用rdd.coalesce,这个方法只能减少分区,不能增加分区,也不会有shuffle过程 - 数据不平衡导致内存溢出
调用repartition重新分区 - Shuffle后内存溢出
大部分的spark中shuffle,默认的partitioner都是hashpartitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制,此只对hashpartitioner有效,如果是别的partitioner或者是自己实现的partitioner就不能使用spark.default.parallelism。如果是别的partitioner导致的内存溢出,就需要从partitioner的代码增加partitions数量
2.2.13 Spark中的 checkpoint ---------------------checkpoint----checkpoint----checkpoint
场景:
checkpoint的意思就是建立检查点,类似于快照,比如说spark某次计算流程DAG特别长,服务器需要将整个DAG计算完成得出结果,但是在这当代中突然出现数据丢失了,spark又会根据RDD的依赖关系从头到尾计算一遍,这样就很耗时间。
这时可以通过中间的计算结果通过 cache 或者 persist 放到内存或者磁盘中,但是这样也不能保证数据完全不会丢失,存储的这个内存出问题了或者磁盘坏了,也会导致spark从头再根据RDD计算一遍,
这时更保险的措施就是,设置checkpoint,其中checkpoint的作用就是将DAG中比较重要的中间数据做一个检查点将结果存储到一个高可用的地方(通常这个地方就是HDFS里面)
原理:
- 当finalRDD执行action算子计算job任务的时候,spark会从finalRDD从后往前回溯查看哪些RDD使用了checkpoint算子
- 将使用了checkpoint的算子标记
- Spark会自动的启动一个job来重新计算标记了的RDD,并将计算的结果存入hdfs,然后切断RDD的依赖关系
2.2.14 spark的shuffle方式
Hashshuffle sortshuffle(默认)
- Hashshuffle特点:
数据不进行排序,速度快
直接写入缓冲区,缓冲区写满后溢写为文件
本shuffleMapStage的每一个task会生成与下一个并行度相同的文件数量
海量文件操作和临时文件的缓存,会占用内存导致内存溢出 - Sortshuffle特点:
会对数据进行排序
写入缓存之前,如果是reduceByKey之类的算子,则会先写入到一个map内存数据结构,如果是join之类的算子,则先写入到Array内存数据结构中。在每条数据写入前先判断是否达到一定阈值,到达则写入缓冲区
2.2.15 spark中广播变量的作用
使用广播变量,每个Executor的内存中,只留一份副本,不是对每个task都传输一次大变量,省去很多传输时间。
使用场景: 使用map join 代替reduce join 把小的数据集广播到各个节点上,节省昂贵的shuffle操作
Driver上有一张数据量小的表,其他节点上的task都需要lookup这张表,那么driver可以先把这张表copy到这些节点,这样task就可以在本地查表了
2.2.16 Spark Join 的优化经验
Spark作为分布式的计算框架,最为影响其执行效率的地方就是频繁的网络传输。所以一般的,在不存在数据倾斜的情况下,想要提高Spark job的执行效率,就尽量 减少job的shuffle过程 (减少job的stage),或者减小shuffle带来的影响。
- 尽量减少参与join的RDD的数量
- 尽量避免参与join的RDD都具有重复的key
- 尽量避免或者减少shuffle过程
- 条件允许的情况下,使用map-join 完成 join
2.3 spark sql
2.3.1 RDD/Dataset/DataFrame的区别
-
RDD不支持sparksql操作,DataFrame与Dataset均支持sparksql的操作
-
DataFrame关心的是数据的结构,Dataset关心的是数据的类型
-
DataFrame与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值
DataFrame与Dataset一般不和spark ml同时使用 -
DataFrame与Dataset支持一些特别方便的保存方式,如csv,可以带上表头
相互转换:
①:RDD -> Dataset
val ds = rdd.toDS()
②:RDD -> DataFrame
val df=rdd.toDF()
③:Dataset -> RDD
val rdd = ds.rdd
④: Dataset -> DataFrame
val df = ds.toDF()
⑤:DataFrame -> RDD
val rdd = df.rdd
⑥:DataFrame -> Dataset
val ds = df.as[T]
2.3.2 Spark SQL优化器 – Catalyst Optimizer
Catalyst 的目标是将逻辑计划转化为物理计划,执行过程可简单概述为:
SQL语句翻译成语法树 —> 生成逻辑计划 —> 优化逻辑计划 —> 在投影上检查过滤器 —> 检查过滤器是否可以下压 —>合并project —> 生成物理计划
Catalyst优化器常见的三种优化规则:谓词下推、常量累加、列剪枝
- 谓词下推:扫描数据量过滤
- 常量累加:减少常量操作
- 列剪枝:对列式数据库提高扫描效率,减少网络、内存数据量消耗
2.3.3 spark sql运行流程
- 对读入的sql语句进行解析 分析关键词,表达式,语句是否规范
- 将sql语句和数据库的数据字典进行绑定 列、表、视图
- 数据库选择最优的执行计划
- 执行计划
2.3.4 sparkSQL缓存方式有哪几种
- 通过sqlcontext实例,cacheTable(“表名”)缓存一张临时表
- 通过DataFrame实例,cache()缓存一张虚拟表
RegisterTempTable不是action算子,不发生缓存
2.3.5 sparksql中join操作与left join操作的区别
Join和sql中的inner join相似,返回结果是前面一个集合和后面一个集合中匹配成功的,过滤掉关联不上的
leftJoin类似于sql中的left outer join,返回结果是以第一个RDD为主,关联不上的记录为空
2.4 SparkStreaming
2.4.1 spark streaming 宕机怎么办
查看日志,先分析看看报什么错误
常见的报错有:心跳超时、KafkaUtil类在消费消息时发生OOM、netty请求无响应等等,节点被关闭的直接原因大都是RECEIVED SIGNAL TERM,这一系列的报错都指向了内存占用的问题。
- 首先,kafka报错的原因是KafkaConsumer在通过拉取消息后,在复制到jvm中生成对象时报OOM,直接原因,内存溢出;
- 其次,内存溢出时报的错误是:GC overhead limit exceeded,这个报错的原因是JVM频繁出现花费了98%的时间进行垃圾回收,而只得到2%可用的内存。executor和driver都有出现过。简单来说也就是jvm中老年代内存满了,且这些对象都是不可回收的,不管怎么full gc,jvm的内存总是不足;
- netty请求被拒绝的问题。这里面有的是driver和executor之间发送心跳消息超时,有的是executor和executor之间通过netty读取shuffle数据时的请求超时,根本原因应该也是被请求进程的频繁full gc导致无法及时响应请求;
- RECEIVED SIGNAL TERM问题。这个提示字面的意思是,进程收到了关闭信号,是CoarseGrainedExecutorBackend提示的。表示进程收到OS的term信号后关闭,可能的原因有人工调用yarn application -kill命令关闭任务、container因某些原因被NodeManager强制关闭等;
先分析原因,其次要想重启的话,可以用一些 azkaban,oozie这些任务调度工具,他可以实时的监控应用程序的状态、任务提交、应用程序实时重启等,也可以通过原始的通过脚本来监控应用的状态。
例如可以写一个脚本,让他每隔10分钟检查一次状态,如果程序不在运行,则重启
#!/bin/bash
# check spark app state
# if not running, restart spark app
#
# */10 * * * * /opt/spark/autorestart-sparkapp.sh 2>&1
#
basePath=$(pwd)
proxyUser="用户名,提交程序的应用名,免得把别人的程序kill掉了"
# 下面规定多几个匹配条件,就是匹配误杀的。
applicationType="SPARK"
appStatus="RUNNING"
# 日志临时目录
logDir=/tmp/aaaaaaaalog
initLockFile=${basePath}/autoInitSparkApp.lock
isNeedInit=false
nowDate=$(date "+%Y%m%d")
nowTime=$(date "+%H%M")
# wait app seconds
maxStartAppWait=600
if [ ! -e ${initLockFile} ]&&[[ ${nowTime} < "0200" ]]; then
isNeedInit=true
elif [ -e ${initLockFile} ];then
initDate=$(cat ${initLockFile})
if [ X${initDate} != X${nowDate} ]&&[[ ${nowTime} < "0200" ]]; then
isNeedInit=true
fi
fi
if [ ! -d "$logDir" ] ; then
mkdir $logDir
fi
# 用于临时存储spark 应用列表
jobListFile=/tmp/aaalog/jobList.txt
# aaaaaa之类代表应用名称,匹配误杀,多个判断条件安全一点
allAppNames=("aaaaaa" "bbbbbb" "cccccc")
yarn application -list 2>/dev/null|awk '{print $0;}'|grep -E "aaaaaa|bbbbbb|cccccc" > ${jobListFile}
declare isRunning=false
for idx in $(seq 0 ${#allAppNames[@]}) ;
do
appName=${allAppNames[$idx]}
isRunning=false;
jobId=""
if [ -z $appName ];then
continue;
fi
while read line
do
jobId=$(echo $line|awk '{print $1;}');
jobName=$(echo $line|awk '{print $2;}');
appType=$(echo $line|awk '{print $3;}');
user=$(echo $line|awk '{print $4;}');
queue=$(echo $line|awk '{print $5;}');
jobStatus=$(echo $line|awk '{print $6;}');
if [ ! -z $appName ]&&[ "$appName" == "$jobName" ]&&[ "$proxyUser" = "$user" ]&&[ "$appType" = "$applicationType" ]&&[ "$appStatus" = "$jobStatus" ];then
isRunning=true
break;
elif [ ! -z $appName ]&&[ "$appName" == "$jobName" ]&&[ "$proxyUser" = "$user" ]&&[ "$appType" = "$applicationType" ];then
isRunning=false
break;
else
jobId=""
jobName=""
jobStatus=""
isRunning=false
fi
done < $jobListFile
if [ $isRunning = true ];then
echo "Spark application "$appName" is running!"
if [ ${isNeedInit} = true ]&&[ ! -z $jobId ];then
yarn application -kill $jobId
fi
jobId=""
else
finishTime=0;
timeDiff=0;
if [ ! -z $jobId ];then
finishTime=`yarn application -status $jobId 2>/dev/null | grep "Finish-Time" | awk '{print $NF}'`
if [ "$finishTime" -eq 0 ]; then
timeDiffExpr=`date +%s`-`yarn application -status $jobId 2>/dev/null | grep "Start-Time" | awk '{print $NF}' | sed 's!$!/1000!'`
timeDiff=`echo $timeDiffExpr|bc`
# wait for $maxStartAppWait time maybe allays accept status
if [ "$timeDiff" -gt "$maxStartAppWait" ];then
yarn application -kill $jobId
sleep 15s
else
continue;
fi
fi
fi
if [ x"$appName" == x"${allAppNames[0]}" ];then
echo "Spark Submit $appName to Cluster!!"
# aaaaa 的应用提交脚本
nohup sh $basePath/run-aaaaaa-cluster.sh 2>&1 >/dev/null &
elif [ x"$appName" == x"${allAppNames[1]}" ];then
echo "Spark Submit $appName to Cluster!!"
# bbbbbb的应用提交脚本
nohup sh $basePath/run-bbbbbb-cluster.sh 2>&1 >/dev/null &
elif [ x"$appName" == x"${allAppNames[2]}" ];then
echo "Spark Submit $appName to Cluster!!"
# cccccc的应用提交脚本
nohup sh $basePath/run-cccccc-cluster.sh 2>&1 >/dev/null &
fi
sleep 30s
fi
done
if [ ${isNeedInit} = true ]; then
# delete checkpoint directory
hadoop fs -rm -r -f /tmp/aaaaaa/checkpoint
hadoop fs -rm -r -f /tmp/bbbbbb/checkpoint
echo ${nowDate} > ${initLockFile}
fi
程序挂掉了,防止数据丢失,就是可以用CheckPoint,重启后根据checkpoin进行恢复
2.4.2 spark streaming 有几种方式消费kafka中的数据?区别是什么?
-
Receiver的方式
Receiver方式为确保零数据丢失,必须在Spark Streaming中另外启用预写日志(Write Ahead Logs)。这将同步保存所有收到的Kafka数据到分布式文件系统(例如HDFS)上,以便在发生故障时可以恢复所有数据。 -
Direct的方式
Direct方式依靠checkpoint机制来保证。每次streaming 消费了kafka的数据后,将消费的kafka offsets更新到checkpoint。当你的程序挂掉或者升级的时候,就可以接着上次的读取,实现数据的零丢失。
- 对比
基于receiver的方式,是使用kafka的高阶API来在zk中保存消费过的offset。配合WAL机制可以保证数据零丢失的高可靠性。但无法保证数据被处理有且仅有一次,因为spark和zk之间可能是不同步的。
基于direct方式,使用kafka的简单api,spark streaming自己就负责追踪消费的offset,并保存在checkpoint中,spark自己一定是同步的,因此可以保证数据只消费一次。
2.4.3 spark streaming窗口函数的原理
窗口函数就是在原来定义的SparkStreaming计算批次大小的基础上再次进行封装,每次计算多个批次的数据,同时还需要传递一个滑动步长的参数,用来设置当次计算任务完成之后下一次从什么地方开始计算。
他有两个参数,窗口长度和滑动间隔。比如说我设置窗口长度为8s,滑动间隔为4s,这样就是表示每隔4s计算最近8s的数据。并且 窗口长度和滑动间隔这两个参数都必须是批处理间隔的整数倍。
2.4.4 sparkstreaming优化 -----------------------优化优化优化
- 优化运行时间
a. 增加并行度
b. 减少数据序列化,反序列化的负担
c. 设置合理的batch duration(批处理时间)
d. 减少因任务提交和分发所带来的负担 - 优化内存使用
a. 控制batch size(批处理间隔的数据量)
b. 及时清理不再使用的数据
c. 观察及适当调整GC策略
2.4.5 sparkstreaming容错原理 :checkpoint ----------------------------
Spark Streaming 中 checkpoint 两种类型的数据:
-
Metadata(元数据) checkpointing : 保存定义了 Streaming 计算逻辑至类似 HDFS 的支持容错的存储系统。用来恢复 driver,元数据包括:
配置 - 用于创建该 streaming application 的所有配置
DStream 操作 - DStream 一些列的操作
未完成的 batches - 那些提交了 job 但尚未执行或未完成的 batches -
Data checkpointing : 保存已生成的RDDs至可靠的存储,所有接收的数据通过receivers写入HDFS或者S3中checkpoint目录,这样当driver失败后,executor中数据丢失后,可以通过checkpoint恢复
激活 checkpoint ?
启用 checkpoint,需要设置一个支持容错 的、可靠的文件系统(比如 HDFS)目录来保存 checkpoint 数据。通过调用 streamingContext.checkpoint(checkpointDirectory) 来完成。另外,如果你想让你的 application 能从 driver 失败中恢复,你的 application 要满足:
- 若 application 为首次重启,将创建一个新的 StreamContext 实例
- 如果 application 是从失败中重启,将会从 checkpoint 目录导入 checkpoint 数据来重新创建 StreamingContext 实例
2.4.5 UpdateStateByKey算子操作
1.说明
SparkStreaming的一般是7天24小时不停息的运行,而在运行的时候,中间会有很多的状态,而有些状态我们需要一些操作,比如累计,更新或者其他的操作。那么如何将这些独立的状态联系起来就成了一种迫切的需求。
2.介绍
UpdateStateByKey的主要功能:
1、为Spark Streaming中每一个Key维护一份state状态,state类型可以是任意类型的, 可以是一个自定义的对象,那么更新函数也可以是自定义的。
2、通过更新函数对该key的状态不断更新,对于每个新的batch而言,Spark Streaming会在使用updateStateByKey的时候为已经存在的key进行state的状态更新。
注意
使用到updateStateByKey要开启checkpoint机制和功能。
多久会将内存中的数据写入到磁盘一份?
如果batchInterval设置的时间小于10秒,那么10秒写入磁盘一份。如果batchInterval设置的时间大于10秒,那么就会batchInterval时间间隔写入磁盘一份
3.代码说明
这里是以单词统计为例
import org.apache.hadoop.mapred.lib.HashPartitioner
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object Streaming_demo {
def main(args: Array[String]): Unit = {
// 创建conf
val conf = new SparkConf().setMaster("local[2]").setAppName("streaming_demo")
// 创建SparkStreamingContext
val ssc = new StreamingContext(conf, Seconds(3))
// 设置日志
ssc.sparkContext.setLogLevel("WARN")
// 设置检查点,checkpoint
ssc.checkpoint("/Users/ricky/Desktop/chekpoint")
// 使用Socket流产生数据
val lines = ssc.socketTextStream("localhost",9999)
// 切分数据
val words = lines.flatMap(_.split(" "))
// 遍历数据
val word = words.map(word=>{
(word,1)
});
// 使用UpdateStateByKey进行更新
val result = word.updateStateByKey((seq:Seq[Int],option:Option[Int])=>{
// 初始化一个变量
var value = 0;
// 该变量用于更新,加上上一个状态的值,这里隐含一个判断,如果有上一个状态就获取,如果没有就赋值为0
value += option.getOrElse(0)
// 遍历当前的序列,序列里面每一个元素都是当前批次的数据计算结果,累加上一次的计算结果
for(elem <- seq){
value +=elem
}
// 返回一个Option对象
Option(value)
})
// 累加,打印
val wordcount1 = result.reduceByKey(_ + _)
wordcount1.print()
// 启动SparkStreaming,设置阻塞
ssc.start()
ssc.awaitTermination()
}
}
2.4.6 UpdateStateByKey 与 mapWithState 的区别
- updateStateByKey 可以在指定的批次间隔内返回之前的全部历史数据,包括新增的,改变的和没有改变的。由于updateStateByKey在使用的时候一定要做checkpoint,当数据量过大的时候,checkpoint会占据庞大的数据量,会影响性能,效率不高。
- mapWithState 只返回变化后的key的值,这样做的好处是,我们可以只是关心那些已经发生的变化的key,对于没有数据输入,则不会返回那些没有变化的key的数据。这样的话,即使数据量很大,checkpoint也不会像updateStateByKey那样,占用太多的存储,效率比较高(再生产环境中建议使用这个)。
三、核心组件
3.1 flume
3.1.1flume组成: source channel sink
-
channel
File channel相对于memory channel,无数据丢失风险,存储容量大。
File channel 数据存储路径可以配置多磁盘文件路径,通过磁盘并行写入提高file channel性能。配置maxfilesize设置数据文件大小,当写入的文件大小达到上限,flume会重新创建新的文件存储写入event。配置dataDirs指向多个路径,每个路径对应不同的硬盘,增大Flume吞吐量。
Memory channel:读取速度快,存储量小,flume进程挂掉,服务器重启会导致数据丢失
Kafka channel :容量大,容错能力强,在日志收集层只配置Source组件和Kafka组件,不配置Sink组件,减少日志收集层启动的进程数,有效降低服务器内存、磁盘等资源的使用;
日志汇聚层,只配置kafka channel和sink,不需要配置Source -
Sink
Hdfs sink:长期存储大量数据,写入hdfs文件
Kafka sink:flume通过kafkasink 将event写入kafka主题,其他应用通过订阅主题消费数据。
3.1.2 Flume拦截器
Source将event写入channel之前可以使用拦截器对event进行各种形式的处理,source和channel之间可以有多个拦截器,不同拦截器使用不同规则处理event,包括时间、主机、UUID、正则表达式等多种形式的拦截器
3.1.3 Flume负载均衡
提高整个系统的容错能力和稳定性。设置sink组,同一个sink组有多个sink,不同sink之间可以配置成负载均衡或者故障转移
3.2 kafka
3.2.1 kafka和传统队列消息的区别
- kafka会将接收的消息分区,每个主题topic都有不同的分区,这样可以使数据存储不会受单一服务器的限制,另一方面也可以在多个服务器上并行。
- kafka可以实现高可用,每个分区都会有一定数量的副本replica,如果主机宕机了,副本所在的服务器就会解体上来,保证数据不会丢失。
- kafka可以保证分区内部消息有序行
- consumer group,可以保证每个分区只能被同一个group的一个consumer消费,但可以被多个group消费
- 结合zookeeper对kafka集群负载均衡管理
3.2.2 kafka的应用场景
数据传输量很大的情况下,用来接收数据做消峰处理,解耦生产者和消费者,缓存数据
3.2.3 kafka在高并发的情况下,如何避免数据丢失
① 消息丢失解决方案:
- 对kafka进行限速
- 启用重启机制,重试间隔时间设置长一点
- 设置
acks = all
,即需要相应的所有处于ISR的分区都确认收到该消息后,才算数据发送成功
② 消息重复解决方案
-
消息使用唯一id标识
-
生产者
ack = all
代表至少成功发送一次 -
消费者 offset手动提交,业务逻辑处理成功后,提交 offset
-
选择唯一主键存储到redis中,先查询是否存在,存在不处理;不存在,先插入redis中,在进行业务处理
3.2.4 kafka 到 spark streaming 怎么保证数据完整性,怎么保证不重复消费
保证数据完整性:
-
在Spark Streaming中 开启预写日志(Write Ahead Logs),同步保存所有收到的Kafka数据到分布式文件系统(例如HDFS)上,以便在发生故障时可以恢复所有数据。
-
Direct 方式
使用 checkpoint 机制,每次streaming 消费了kafka的数据后,将消费的kafka offsets更新到checkpoint。当你的程序挂掉或者升级的时候,就可以接着上次的读取,实现数据的零丢失。
出现重复重复消费的情况
两种情况可能出现重复消费:
-
当ack=-1时,如果在follower同步完成后,broker发送ack之前,leader发生故障,导致没有返回ack给Producer,由于失败重试机制,又会给新选举出来的leader发送数据,造成数据重复。
-
(手动管理offset时,先消费后提交offset)消费者消费后没有commit offset(程序崩溃/强行kill/消费耗时/自动提交偏移情况下unscrible)
出现漏消费的情况
-
(手动管理offset时,先提交offset后消费)先提交offset,后消费,有可能造成数据的重复
-
如果先提交offset,后消费,可能会出现数据漏消费问题。比如,要消费0,1,2,我先提交offset ,此时__consumer_offsets的值为4,但等我提交完offset之后,还没有消费之前,消费者挂掉了,这时等消费者重新活过来后,读取的__consumer_offsets值为4,就会从4开始消费,导致消息0,1,2出现漏消费问题。
-
当ack=0时,producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据;
-
当ack=1时,producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,而由于已经返回了ack,系统默认新选举的leader已经有了数据,从而不会进行失败重试,那么将会丢失数据
保证数据不重复消费:
- 幂等操作:重复执行不会产生问题
- 添加事务操作:针对每个partition的数据,产生一个uniqueId,只有这个partition的所有数据被完全消费,则算成功,否则算失效,要回滚。下次重复执行这个uniqueId时,如果已经被执行成功,则skip掉
3.2.5 kafka的消费者高阶和低阶API有什么区别
kafka提供了两套comsumer API: The high-level Consumer API 和 The Simple Consumer API
-
The high-level Consumer API : 提供了一个从kafka消费数据的高层抽象,有 consumer group 的语义,一个消息只能被group内的一个consumer所消费,且consumer消费消息时不关注 offset,最后一个offset由zookeeper保存
注: The high-level Consumer API 可以使 多线程 的应用,需要注意的是:
(1)如果消费线程大于patition数量,则有些线程接收不到消息
(2)如果patition数量大于线程数,则有些线程无法收到多个patition的消息 -
The Simple Consumer API : 需要开发人员更多的关注细节,如果想要对 partition 有更多的控制权,应该使用The Simple Consumer API ,它可以实现:
多次读取一个消息
只消费一个partition中的部分消息
使用事务来保证一个消息仅被消费一次
注: 使用 he Simple Consumer API 需要做一些额外工作:
(1)必须在应用程序中跟踪offset,从而确定下一条应该消费哪条消息
(2)应用程序需要通过程序获取每个partition的leader是谁
(3)需要处理leader的变更
总的来说,The Simple Consumer API 相较于 The high-level Consumer API 需要手动维护
3.2.6 kafka怎么保证数据消费一次且仅消费一次
- 幂等producer:保证发送单个分区的消息只会发送一次,不会出现重复消费
- 事务(transaction):保证原子性地写入到多个分区,即写入到多个分区的消息要么全部成功,要么全部回滚
3.2.7 kafka保证数据一致性和可靠性
①保证数据一致性:
设置 acks = all
对于leader新收到的msg,client不能立即消费,等待消息被所有 ISR 中的副本 replica 同步后,更新 HW-HighWaterMark,此时消息才能被client消费,这样就保证了如果leader fail,该消息仍然可以从新选举的leader中获取
②保证数据可靠性
当producer向leader发送数据时,可以通过 acks 参数设置数据可靠性的级别
acks = 0
:不论写入成功与否,server不需要给 producer 发送response,如果发生异常,server会终止连接,出发producer更新meta数据acks = 1
:leader写入成功后即发送response,此种情况下如果leader fail ,会丢失数据acks = -1
:等待所有 ISR 接收到消息再给 producer 发送 response ,这样安全性最强
3.2.8 spark实时作业宕掉,kafka指定的topic数据堆积怎么办?
应该措施:
spark.streaming.concurrentJobs=10
:提高Job并发数sparl.streaming.kafka.maxRatePerPartition = 20000
:设置每秒每个分区最大获取日志数,控制处理数据量,保证数据处理均匀spark.streaming.kafka.maxRetries = 50
:获取topic 分区leaders 及最新offsets时,调大重试次数- 设置尝试失败有效间隔时间
3.2.9 acks
kafka的acks参数有一个非常重要的作用。如果
- acks设置为0,表示Producer不会等待Broker的响应,Producer无法确定消息是否发送成功,可能会导致数据丢失,但acks值为0时,会得到最大的系统吞吐量。
- 如果acks设置为1,表示Producer会在leader Partition收到消息并得到Broker的一个确认,这样会有更好的可靠性。
- 如果设置为-1,Producer会在所有备份的Partition收到消息时得到Broker的确认,这个设置可以得到最高的可靠性保证。
3.2.10 kafka读写流程
写:
- 连接ZK集群,从ZK中拿到对应topic的partition信息和partition的Leader的相关信息
- 连接到对应Leader对应的broker
- 将消息发送到partition的Leader上
- 其他Follower从Leader上复制数据
读:
- 连接ZK集群,从ZK中拿到对应topic的partition信息和partition的Leader的相关信息
- 连接到对应Leader对应的broker
- consumer将自己保存的offset发送给Leader
- Leader根据offset等信息定位到segment(索引文件和日志文件)
- 根据索引文件中的内容,定位到日志文件中该偏移量对应的开始位置读取相应长度的数据并返回给consumer
3.2.11 kafka为什么只让leader进行读写
保证数据一致性,只与leader进行数据交互,其他follower从leader中备份数据
3.2.12 为了避免磁盘被沾满,kafka会周期性的删除旧消息,请问删除策略有哪些?控制粒度到什么程度?请具体描述一下?
kafka中有两种 保留策略 :
- 根据消息保留的时间,当消息在kafka中保存的时间超过了指定时间,就可以被删除
- 根据topic存储的数据大小,当topic所占的日志文件大小大于一个阈值,则可以开始删除最旧的消息
3.2.13 kafka 数据高可用的原理
- 数据存储格式,一个Topic可以分成多个partition。一个partition有多个segment组成,这样数据就可以在多台机器中分布式存储与备份,提高了数据的可靠性、安全性
- 副本复制与同步,ISR 列表中设置 acks = -1
3.2.14 kafka offset存放在哪儿?
Broker服务器上(0.9版本之后),默认将消费的offset迁入到kafka _consumer_offsets Topic中
3.2.15 如何保证kafka的消息有序
kafka只能保证一个partition中的消息被某个 consumer 消费时时顺序的,实时上,从topic的角度来说,当有多个partition时,消息仍然不是全局有序的,也就是说,要想做到全局有序,需要设置成只有一个partition
3.2.16 kafka的分区数
分区数并不是越多越好,一般分区数不要超过集群机器数量。分区数越多占用内存越大(ISR等),一个节点集中的分区也就越多,当它宕机的时候,对系统的影响也就越大
结合项目来说,一般数据量100G以内,10台机器,3台kafka节点,分区数3-10左右
副本数的设定,一般设置成2-3个,很多企业设置成2个
3.2.17 kafka消费者的分区分配策略
kafka内部存在两种默认的分区分配策略: Range 和 RoundRobin
- Rangle: topic里面的分区按照序号进行排序,消费者按照字母顺序进行排序,用partition分区的个数 / 消费者线程的总数来决定消费者要分几个去消费,如果除不尽,则前面的消费者将多消费一个分区
- RoundRobin: 将所有分区组成 TopicAndPartiton 列表,然后对TopicAndPartition 列表按照hashCode进行排序,最后按照轮询的方式发给每一个消费线程
3.2.18 kafka中数据量计算
假设每天总数量100G,每天产生1亿条日志,100000 万 / 24/60/60 = 1150条/每分钟
- 平均每秒钟:1150条
- 低估每秒钟:400条
- 高峰每秒钟: 1150条 * (2-20倍) = 2300条 - 23000条
- 每条日志大小:0.5k - 2k
- 每秒多少数据量: 2.3M - 20M
3.2.19 kafka 消息数据积压,kafka消费能力不足怎么处理?
如果是kafka消费能力不足,则可以考虑增加topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数
如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据处理时间 < 生产速度),处理的数据小于生产的数据,也会造成数据积压
3.2.20 kafka监控
开源的监控器有: KafkaManager、 KafkaMonitor
3.2.21 kafka高吞吐的实现
- 顺序读写:kafka的消息顺序读写,不断追加到文件中。因为顺序读写不需要硬盘磁头的寻道时间,只需要很少的扇区旋转时间,所以速度远快于随机读写
- 零拷贝:在IO的读写过程中,只需要将磁盘文件的数据复制到页面中缓存一次,然后将数据从页面缓存直接发送到网络中,减少了不必要的拷贝次数
- 分区:kafka队列中的topic可以多分区,每个分区又分为多个段的segment,每次文件操作都是对小文件的操作,非常轻便,也增加了并行处理能力
- 批量发送:先将消息缓存在内存中,然后一次请求批量发送缓存消息,大大减少服务端I/O的次数
- 数据压缩:Producer可以通过 GZIP 或者 Snappy 格式对消息集合进行压缩,减少网络传输数据量
- Consumer的负载均衡:当一个group中,有consumer 加入或者离开时,会触发 partitions均衡,均衡的最终目的是提升topic的并发消费能力
五、大数据算法题
5.1 海量数据处理
5.1.1 100亿数据进行排序
可以将这个大文件拆分,然后内部排序,快速排序,最后在归并
六、算法理论
6.1 分类
6.1.1 KNN邻近算法
计算每个训练样例到待分类样品的距离,取与样例距离最近的样品,则待分类的样例就属于这个样品。
算法步骤为:
计算未知实例到所有已知实例的距离;
选择参数 K;
根据多数表决( majority-voting )规则,将未知实例归类为样本中最多数的类别。
距离的衡量方法:欧氏距离,这种测量方式就是简单的平面几何中两点之间的直线距离。
6.2 聚类
将一组对象集合按他们的相似程度划分成多个类或者簇
- k-means算法:随机从数据集选取k个点,每个点初始的代表每个簇的聚类中心,然后计算剩余各样本到聚类中心的距离,根据距离的远近,将它赋给最近的簇,然后再重新计算每一簇的平均值。重复过程,如果相邻两次调整没有明显大得变化,那么说明数据聚类形成的簇已经收敛,达到聚类分层的效果。
6.3 回归
回归就是用两个变量来拟合一条最佳的直线,可以用一个属性来预测另外一个属性。
- 最小二乘法:通过最小化误差的平方和寻找数据的最佳函数匹配