大数据知识总结

hadoop

HDFS读数据流程

image-20230527200826866

1 首先是客户端向NameNode(是HDFS的话事人)发送下载文件请求。

2 NameNode收到请求之后,会做两个判断(1)判断该客户端是否有下载对应文件的权限(2)判断HDFS集群上是否有该文件。

3 判断通过后,NameNode向客户端相应元数据(存储块信息在哪)

4 客户端会创建一个读流(FSDataInputStream),去哪个节点读数据有两原则(1)节点最近(2)负载均衡,防止都读一个节点,结合以上两个原则选择节点读数据。

5 假设请求的文件被分为两个数据块,读请求是串行的,第一块读完之后,才读第二块进行追加。

HDFS写数据流程

image-20230527200905930

1 客户端向NameNode请求上传文件路径

2 NameNode收到请求之后,也会做两次判断(1)判断该客户是否有写权限,若没就直接拒绝(2)检查目标文件路径是否存在,若存在则写入失败。之后NN向客户端相应可以上传文件。

3 客户端向NN请求上传第一个Block(0-128M),请NN返回DataNode(NN要告诉客户端往哪个存)

4 NN向客户端返回DataNode信息会有如下原则(1)就近节点,优先本地(2)其他机架节点(3)其他机架另一个节点<优先级1->3>,NN向客户端返回dn1,dn2,dn3节点,表示采用这三个节点存储数据

5 客户端创建一个写流(FSDataOutputSteam),假若选择的dn1,会先在dn1,dn2,dn3之间建立通道,这样只需往dn1一个节点上写入即可,dn2和dn3的会从通道中取到同样的数据写入,增加了写入效率。

6 在通道中传输的最小单元叫packet(64k),在创建写流的时候,还会创建一个缓冲队列,该队列存储(chunk512byte+chunksum4byte<校验位>)当516byte攒够了64k会形成一个packet,放入缓冲队列中,然后从缓冲队列中在通道中传输。

7 当数据都写入完毕之后,各个节点会给上流节点应答成功。

8 写入流获得所有dn的ack之后,会告知NameNode我写完了,你更新一下元数据信息。

Yarn工作机制

image-20230527201006431

0 Mr程序提交到客户端所在的节点,通过job.waitForCompletion()来创建YarnRunner,本地模式下是localRunner。

1 yarnRunner向集群“老大”ResouceManager申请一个Application。

2 ResourceManager返回给yarnRunner所在节点一个”Application资源提交路径“(hdfs://…/)以及application_id

3 YarnRunner所在节点会放该hdfs路径上提交job所需的资源(1、job.split<控制开启多少个maptask> 2、Job.xml <配置文件>3、wc.jar<代码>)

4 资源提交完毕之后,该节点会向ResourceManager申请运行mrAppMaster(可以理解为是控制其他任务执行的老大)

5 ResourceManager将用户的请求初始化成一个Task,接着将该Task放入FIFO调度队列。

6 此时有一个比较空闲的NodeManager,它会主动领取ResourceManager调度队列中的任务。

7 领到任务之后,这个NodeManager会先创建容器(任何任务的执行都是在容器中执行的,容器中有对应的cpu、网络资源、磁盘io等资源)在容器中启动了一个MRAppmaster。

8 它会向集群资源的路径下载Job资源(job切片)。

9 拿到切片信息之后,会再次向ResourceManager申请,此时是申请运行MapTask容器,假若切片是两个,就会开启两个MapTask。

10 其他NodeManager领到任务之后,创建容器(这两个容器<cpu+ram+jar>有可能在一个NodeManager上也有可能在一个NodeManager上)

11 当以上两个容器拿到对应资源和jar之后,最初容器中的MRAppmaster向运行MapTask的容器发送MapTask程序启动脚本,此时每个MapTask对应的进程yarnchild,maptask任务的结果就是把job按照分区持久化到磁盘。

12 map阶段运行完之后,MRAppmaster会再次向ResourceManager申请两个容器,运行ReduceTask程序也是对应的yarnchild进程。

13 reduce向map获取相应的分区的数据。

14 reduce执行完毕之后,由MRAppmaster告知ResourceManager释放MapTask和ReduceTask以及MRAppmaster所占用的资源释放掉

MapReduce详细工作流程

![image-20231025184853398](https://img-blog.csdnimg.cn/img_convert/e13b4image-20231025184853398](https://img-blog.csdnimg.cn/img_convert/43fa29465967be6fc7b3b62f8b18a9db.png)

1

2

3

4 Mrappmaster最主要的是读取job切片信息,来确定开启几个MapTask任务(详细见yarn工作机制)。

5 TextInputReader默认是按行读取(RecorderReader),读取到MapTask中。

环形缓冲区(默认100M):一半存数据,一半存索引元数据。当环形缓冲区写到80%除,会进行反向写,留出的20%是为了让环形缓冲区的数据有足够的时间溢写,即将发生覆盖的时候会等待。

8 向环形缓冲区中写入数据的时候就会将数据标记分区,未来会分区1会进入reduce1,分区2会进入reduce2。到达80%进行溢写前对数据进行快速排序。由于数据是连续的,如果对数据进行排序的话移动元素太多,这时候只需要对修改索引kv起始位置。

9 产生了大量的溢写文件(每一个溢写文件都是环形缓冲区的80%的数据)。

10 将相同分区的溢写文件进行归并排序(保证每个分区内部是有序的),归并完之后会存储在磁盘上。

11 进行预聚合,将2个<a,1>传入reduce效率低于传入1个<a,2>。

image-20231025185009950

12 左下角Mrappmaster是整个程序的老大,当所有的MapTask任务完成之后,启动相应数量的ReduceTask,并告知ReduceTask处理数据范围(不同分区发送到不同的reduceTask)。

13 reduceTask主动从mapTask对应分区拉取数据,将拉取的数据再进行一次全局排序(也是归并排序),这样相同的key的数据就紧挨着,这样就可以把所有的<a,x>一次性全读到。

15 几乎不用

16 当reduce聚合完成的时候,由OutPutFormat核心组件的RecordWriter往外写。

Shuffle机制

Map 方法之后,Reduce 方法之前的数据处理过程称之为 Shuffle。

image-20231025211219644

1 从map方法出来之后,会先进入getPartition方法中,标记数据是哪个分区,然后进入到环形缓冲区(见MapReduce详细工作流程)。

2 溢写之前排序,排序方法:快排,排序对象:key的索引,按照什么排:字典。

3 第一次溢写combiner可选(减少传输数据量)

4 将溢写文件根据分区进行归并

5 还可以进行combiner,之后也可以进行压缩。

6 写到磁盘。

7 reduce主动拉取自己指定的分区,拉来的数据先尝试放到内存,内存不够溢写到磁盘。

8 将内存和磁盘的数据拿过来进行一次大归并。

9 按照相同的key分组,然后进入reduce方法,reduce处理的数据是 key(v1,v2,v3…)这时候就可以对集合里的v1v2v3做加减乘除了。

压缩

image-20231026101927820

**提示:**如果面试过程问起,我们一般回答压缩方式为 Snappy,特点速度快,缺点无法切分(可以回答在链式 MR 中,Reduce 端输出使用 bzip2 压缩,以便后续的 map 任务对数 据进行 split)

切片机制

1 简单地按照文件的内容长度进行切片
2 切片大小,默认等于 Block 大小
3 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
提示:切片大小公式:max(0,min(Long_max,blockSize))

NameNode工作机制

image-20231026082834895

NameNode

edit.001 日志文件,它以追加方式记录了每个编辑操作的详细信息,包括操作类型、文件路径、时间戳等。

fsimage 当NameNode启动时,它会读取fsimage文件,将其中的元数据信息加载到内存中,从而恢复文件系统的状态。

edits.001+fsimage(合在一起就是) 当NameNode启动时,它会读取fsimage文件,将其中的元数据信息加载到内存中,从而恢复文件系统的状态。随后,NameNode会读取编辑日志(Edit log),将其中的变更操作应用到内存中的元数据信息上,最终得到当前文件系统的状态。

1 第一次启动namenode会先进行格式化,创建 fsimage 和 edits 文件。如果不是第一次启动, 直接加载 编辑日志和镜像文件到内存。

2 客户端对元数据进行增删改的请求。

3 namenode先记录操作日志,更新滚动日志。(若先写内存,如果突然断电再次启动namenode时,不能恢复到断电前的一次操作)

4 然后再在内存中对数据进行增删改查。

Secondary NameNode

1 secondary namenode 询问 namenode 是否需要 checkpoint。直接带回 namenode 是否检查结果。

2 secondary namenode 请求执行 checkpoint。

3 namenode 滚动正在写的 edits 日志(理解为历史日志),这时候增删改请求都是写到edits_inprogress_002中。

4 将历史日志和镜像文件拷贝到 secondary namenode。

5 secondary namenode 加载历史日志和镜像文件到内存,并合并。

6 生成新的镜像文件 fsimage.chkpoint。

7 拷贝 fsimage.chkpoint 到 namenode。

8 namenode 将 fsimage.chkpoint 重新命名成 fsimage,覆盖原来的fsimage。

Hadoop优化

HDFS小文件影响

1 影响 NameNode 的寿命,因为文件元数据存储在 NameNode 的内存中。

2 影响计算引擎的任务数量,比如每个小的文件都会生成一个 Map 任务。

数据输入小文件处理

1 合并小文件:对小文件进行归档(Har)、自定义 Inputformat 将小文件存储成 SequenceFile 文件。

2 采用 ConbinFileInputFormat 来作为输入,解决输入端大量小文件场景。

3 对于大量小文件 Job,可以开启 JVM 重用。(每个小文件都需要启动一个独立的Java虚拟机(JVM),这会带来较大的开销。JVM重用是指在同一个作业中,多个任务(Task)共享同一个JVM实例。即使处理多个小文件,也只需要启动一个JVM,而不是为每个小文件都启动一个新的JVM。这样可以减少JVM的启动时间和资源开销,提高整体的作业执行效率。)

Map阶段

1 增大环形缓冲区大小。由 100m 扩大到 200m。

2 增大环形缓冲区溢写的比例。由 80%扩大到 90%:通过调整io.sort.mb及sort.spill.percent参数值,增大触发spill的内存上限,减少 spill(溢写)次数,从而减少磁盘 IO。。

3 减少对溢写文件的 merge 次数:通过调整io.sort.factor参数,增大merge的文件数目,减少merge的次数,从而缩短mr处理时间。。

4 不影响实际业务的前提下,采用 Combiner 提前合并,减少 I/O。

Reduce阶段优化

1 合理设置Map和Reduce数。都不能设置太少,也不能设置太多。太少,会导致Task等待,延长处理时间;太多,会导致 Map、Reduce任务间竞争资源,造成处理超时等错误。

2 设置Map、Reduce共存:调整slowstart.completedmaps参数,使Map运行到一定程度后,Reduce也开始运行,减少Reduce的等待时间。

3 规避使用Reduce,因为Reduce在用于连接数据集的时候将会产生大量的网络消耗。

4 增加每个Reduce去Map中拿数据的并行数

5 集群性能不错的前提下,增大Reduce端存储数据内存的大小。

IO传输优化

1 采用数据压缩的方式,减少网络IO的的时间。比如安装Snappy和LZOP压缩编码器。

2 使用SequenceFile二进制文件。

整体调优

1 MapTask默认内存大小为1G,可以增加MapTask内存大小为4-5G。

2 ReduceTask默认内存大小为1G,可以增加ReduceTask内存大小为4-5G。

3 可以增加MapTask的cpu核数,增加ReduceTask的CPU核数。

4 增加每个Container的CPU核数和内存大小。

5 调整每个Map Task和Reduce Task最大重试次数。

spark

hadoop 和 spark 区别

1 Hadoop 底层使用 MapReduce 计算架构,只有 map 和 reduce 两种操作,表达能力比较欠缺, 而且在 MR 过程中会重复的读写 hdfs,造成大量的磁盘 io 读写操作,所以适合高时延环境下批处理计算的应用。

2 Spark 是基于内存的分布式计算架构,提供更加丰富的数据集操作类型,主要分成转化操作和行动操作,包括 map、reduce、filter、flatmap、groupbykey、reducebykey、union 和 join 等,数据分析更加快速,所以适合低时延环境下计算的应用。

3 spark 与 hadoop 最大的区别在于迭代式计算模型。基于 mapreduce 框架的 Hadoop 主要分 为 map 和 reduce 两个阶段,两个阶段完了就结束了,所以在一个 job 里面能做的处理很有限;spark 计算模型是基于内存的迭代式计算模型,可以分为 n 个阶段,根据用户编写 的 RDD 算子和程序,在处理完一个阶段后可以继续往下处理很多个阶段,而不只是两个阶段。所以 spark 相较于 mapreduce,计算模型更加灵活,可以提供更强大的功能。但是 spark 也有劣势,由于 spark 基于内存进行计算,虽然开发容易,但是真正面对大数据的时候,在没有进行调优的情况下,可能会出现各种各样的问题,比如 OOM 内存溢出 等情况,导致 spark 程序可能无法运行起来,而 mapreduce 虽然运行缓慢,但是至少可以慢慢运行完。

spark解决了hadoop的哪些问题?

  • MR:抽象层次低,需要使用手工代码来完成程序编写,使用上难以上手;
  • Spark:Spark采用RDD计算模型,简单容易上手。
  • MR:只提供map和reduce两个操作,表达能力欠缺;
  • Spark:Spark采用更加丰富的算子模型,包括map、flatmap、groupbykey、reducebykey等;
  • MR:一个job只能包含map和reduce两个阶段,复杂的任务需要包含很多个job,这些job之间的管理以来需要开发者自己进行管理;
  • Spark:Spark中一个job可以包含多个转换操作,在调度时可以生成多个stage,而且如果多个map操作的分区不变,是可以放在同一个task里面去执行;
  • MR:中间结果存放在hdfs中;
  • Spark:Spark的中间结果一般存在内存中,只有当内存不够了,才会存入本地磁盘,而不是hdfs;
  • MR:只有等到所有的map task执行完毕后才能执行reduce task;
  • Spark:Spark中分区相同的转换构成流水线在一个task中执行,分区不同的需要进行shuffle操作,被划分成不同的stage需要等待前面的stage执行完才能执行。
  • MR:只适合batch批处理,时延高,对于交互式处理和实时处理支持不够;
  • Spark:Spark streaming可以将流拆成时间间隔的batch进行处理,实时计算。

hadoop 和 spark 的 shuffled 的区别

  • 从逻辑角度来讲,Shuffle 过程就是一个 GroupByKey 的过程,两者没有本质区别。只是 MapReduce 为了方便 GroupBy 存在于不同 partition 中的 key/value records,就提前对 key 进行排序。Spark 认为很多应用不需要对 key 排序,就默认没有在 GroupBy 的过程中对 key 排序。
  • 从数据流角度讲,两者有差别。MapReduce 只能从一个 Map Stage shuffle 数据,Spark 可以从多个 Map Stages shuffle 数据(这是 DAG 型数据流的优势,可以表达复杂的数据流操作。
  • Shuffle write/read 实现上有一些区别。以前对 shuffle write/read 的分类是 sort-based 和 hash-based。MapReduce 可以说是 sort-based,shuffle write 和 shuffle read 过程都是基于 key sorting 的 (buffering records + in-memory sort + on-disk external sorting)。早期的 Spark 是 hash-based,shuffle write 和 shuffle read 都使用 HashMap-like 的数据结构进行 aggregate (without key sorting)。但目前的 Spark 是两者的结合体,shuffle write 可以是 sort-based (only sort partition id, without key sorting),shuffle read 阶段可以是 hash-based。因此, 目前 sort-based 和 hash-based 已经“你中有我,我中有你”,界限已经不那么清晰。
  • 从实现角度来看,两者也有不少差别。 Hadoop MapReduce 将处理流程划分出明显的几个阶段:map(), spill, merge, shuffle, sort, reduce() 等。每个阶段各司其职,可以按照过程式的编程思想来逐一实现每个阶段的功能。在 Spark 中,没有这样功能明确的阶段, 只有不同的 stage 和一系列的transformation(),所以 spill, merge, aggregate 等操作需要蕴含在 transformation() 中。

Spark效率为什么高于mr

spark是借鉴了Mapreduce,并在其基础上发展起来的,继承了其分布式计算的优点并进行了改进,spark生态更为丰富,功能更为强大,性能更加适用范围广,mapreduce更简单,稳定性好。主要区别

(1)spark把运算的中间数据(shuffle阶段产生的数据)存放在内存,迭代计算效率更高,mapreduce的中间结果需要落盘

(2)Spark容错性高,它通过弹性分布式数据集RDD来实现高效容错,RDD是一组分布式的存储在节点内存中的只读性的数据集,这些集合是弹性的,某一部分丢失或者出错,可以通过整个数据集的计算流程的血缘关系来实现重建,mapreduce的容错只能重新计算

(3)Spark更通用,提供了transformation和action这两大类的算子,另外还有流式处理sparkstreaming模块、图计算等等,mapreduce只提供了map和reduce两种操作,流计算及其他的模块支持比较缺乏

(4)Spark框架和生态更为复杂,有RDD,血缘lineage、执行时的有向无环图DAG,stage划分等,很多时候spark作业都需要根据不同业务场景的需要进行调优以达到性能要求,mapreduce框架及其生态相对较为简单,对性能的要求也相对较弱,运行较为稳定,适合长期后台运行。

(5)Spark计算框架对内存的利用和运行的并行度比mapreduce高,Spark运行容器为executor,内部ThreadPool中线程运行一个Task,mapreduce在线程内部运行container,container容器分类为MapTask和ReduceTask.程序运行并行度高

(6)Spark对于executor的优化,在JVM虚拟机的基础上对内存弹性利用:storage memory与Execution memory的弹性扩容,使得内存利用效率更高

spark有几种部署方式

  1. Local:运行在一台机器上,通常是练手或者测试环境
  2. Standalone:构建一个基于 Master+Slaves 的资源调度集群,Spark 任务提交给 Master 运行。是 Spark 自身的一个调度系统
  3. Yarn: Spark 客户端直接连接 Yarn,不需要额外构建 Spark 集群。有 yarn-client 和 ya rn-cluster 两种模式,主要区别在于:Driver 程序的运行节点
  4. Mesos:国内大环境比较少用

spark提交任务参数

在提交任务时的几个重要参数

  • executor-cores —— 每个 executor 使用的内核数,默认为 1,官方建议 2-5 个
  • num-executors —— 启动 executors 的数量,默认为 2
  • executor-memory —— executor 内存大小,默认 1G
  • driver-cores —— driver 使用内核数,默认为 1
  • driver-memory —— driver 内存大小,默认 512M

提交任务的样式:

bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \      
--deploy-mode cluster \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10

简述 Spark 的架构与作业提交流程

==注:==结合yarn工作机制去理解。

Yarn Cluster 模式:

image-20231105142028437

Cluster 模式将用于监控和调度的 Driver 模块启动在 Yarn 集群资源中执行。一般应 用于实际生产环境。

  1. 在 YARN Cluster 模式下,任务提交后会和 ResourceManager 通讯申请启动 ApplicationMaster,
  2. 随后 ResourceManager 分配 container,在合适的 NodeManager 上启动 ApplicationMaster,此时的 ApplicationMaster 就是 Driver。
  3. Driver 启动后向 ResourceManager 申请 Executor 内存,ResourceManager 接到 ApplicationMaster 的资源申请后会分配 container,然后在合适的 NodeManager 上启动 Executor 进程
  4. Executor 进程启动后会向 Driver 反向注册,Executor 全部注册完成后 Driver 开始执行 main 函数,
  5. 然后在Executor进程中创建Executor计算对象
  6. 之后执行到 Action 算子时,触发一个 Job,并根据宽依赖开始划分 stage,每 个 stage 生成对应的 TaskSet,之后将 task 分发到各个 Executor 上执行。

Yarn Client 模式:

<img src=“https://img-blog.csdnimg.cn/img_convert/b803979f5fed38af14bdd569bd4b46ca](https://img-blog.csdpn"i.cn/mmae_conv-rt/c11134024423” style=“zoom:67%;” />

  1. Client 模式将用于监控和调度的 Driver 模块在客户端执行,而不是 Yarn 中,所以一般用于测试
  2. Driver 在任务提交的本地机器上运行
  3. Driver 启动后会和 ResourceManager 通讯申请启动 ApplicationMaster
  4. ResourceManager 分配 container,在合适的 NodeManager 上启动 ApplicationMaster(ExecutorLauncher),负责向 ResourceManager 申请 Executor 内存
  5. ResourceManager 接到 ApplicationMaster 的资源申请后会分配 container,然后 ApplicationMaster 在资源分配指定的 NodeManager 上启动 Executor 进程
  6. Executor 进程启动后会向 Driver 反向注册,Executor 全部注册完成后 Driver 开始执行 main 函数
  7. 然后在Executor进程中创建Executor计算对象
  8. 之后执行到 Action 算子时,触发一个 Job,并根据宽依赖开始划分 stage,每个 stage 生成对应的 TaskSet,之后将 task 分发到各个 Executor 上执行

Spark三大数据结构(RDD)

Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于
处理不同的应用场景。三大数据结构分别是:
➢ RDD : 弹性分布式数据集
➢ 累加器:分布式共享只写变量
➢ 广播变量:分布式共享只读变量

1、什么是 RDD

![image-20230528155913531.png" alt="image-202305215726daf5444687b2b1075yoom:0a7369.png)

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

➢ 弹性
⚫ 存储的弹性:内存与磁盘的自动切换;
⚫ 容错的弹性:数据丢失可以自动恢复;
⚫ 计算的弹性:计算出错重试机制;
⚫ 分片的弹性:可根据需要重新分片。
➢ 分布式:数据存储在大数据集群不同节点上
➢ 数据集:RDD 封装了计算逻辑,并不保存数据
➢ 数据抽象:RDD 是一个抽象类,需要子类具体实现
➢ 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的 RDD,在
新的 RDD 里面封装计算逻辑
➢ 可分区、并行计算

作用:

提供了一个抽象的数据模型,将具体的应用逻辑表达为一系列转换操作(函数)。另外不同RDD之间的转换操作之间还可以形成依赖关系,进而实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘IO和序列化开销,并且还提供了更多的API(map/reduec/filter/groupBy…),RDD在Lineage依赖方面分为两种Narrow Dependencies与Wide Dependencies,用来解决数据容错时的高效性以及划分任务时候起到重要作用。

2、如何创建RDD

  • 从集合(内存)中创建 RDD
    从集合中创建 RDD,Spark 主要提供了两个方法:parallelize 和 makeRDD,从底层代码实现来讲,makeRDD 方法其实就是 parallelize 方法

    val rdd1 = sparkContext.parallelize(
    	List(1,2,3,4)
    )
    val rdd2 = sparkContext.makeRDD(
    	List(1,2,3,4)
    )
    
  • 从外部存储(文件)创建 RDD
    由外部存储系统的数据集创建 RDD 包括:本地的文件系统,所有 Hadoop 支持的数据集,比如 HDFS、HBase 等。

    val fileRDE: RDD[String] = sparkContext.textFile("input")
    
  • 从其他 RDD 创建
    主要是通过一个 RDD 运算完后,再产生新的 RDD。

  • 直接创建 RDD(new)
    使用 new 的方式直接构造 RDD,一般由 Spark 框架自身使用。

(共享变量)累加器、广播变量

累加器:

累加器用来把 Executor 端变量信息聚合到 Driver 端。在 Driver 程序中定义的变量,在 Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后, 传回 Driver 端进行 merge。

广播变量:

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个 或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表, 广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark 会为每个任务 分别发送。

RDD机制

1 rdd分布式弹性数据集,简单的理解成一种数据结构,是spark框架上的通用货币。所有算子都是基于rdd来执行的,不同的场景会有不同的rdd实现类,但是都可以进行互相转换。rdd执行过程中会形成dag图,然后形成lineage保证容错性等。从物理的角度来看rdd存储的是block和node之间的映射。

2 RDD是spark提供的核心抽象,全称为弹性分布式数据集。

3 RDD在逻辑上是一个hdfs文件,在抽象上是一种元素集合,包含了数据。它是被分区的,分为多个分区,每个分区分布在集群中的不同结点上,从而让RDD中的数据可以被并行操作(分布式数据集)

4 比如有个RDD有90W数据,3个partition,则每个分区上有30W数据。RDD通常通过Hadoop上的文件,即HDFS或者HIVE表来创建,还可以通过应用程序中的集合来创建;RDD最重要的特性就是容错性,可以自动从节点失败中恢复过来。即如果某个结点上的RDD partition因为节点故障,导致数据丢失,那么RDD可以通过自己的数据来源重新计算该partition。这一切对使用者都是透明的。

5 RDD的数据默认存放在内存中,但是当内存资源不足时,spark会自动将RDD数据写入磁盘。比如某结点内存只能处理20W数据,那么这20W数据就会放入内存中计算,剩下10W放到磁盘中。RDD的弹性体现在于RDD上自动进行内存和磁盘之间权衡和切换的机制。

reduceBykey与groupByKey比较

reduceByKey:reduceByKey会在结果发送至reducer之前会对每个mapper在本地进行merge,有点类似于在MapReduce中的combiner。这样做的好处在于,在map端进行一次reduce之后,数据量会大幅度减小,从而减小传输,保证reduce端能够更快的进行结果计算。

groupByKey:groupByKey会对每一个RDD中的value值进行聚合形成一个序列(Iterator),此操作发生在reduce端,所以势必会将所有的数据通过网络进行传输,造成不必要的浪费。同时如果数据量十分大,可能还会造成OutOfMemoryError。

所以在进行大量数据的reduce操作时候建议使用reduceByKey。不仅可以提高速度,还可以防止使用groupByKey造成的内存溢出问题。

spark工作机制?

用户在client端提交作业后,会由Driver运行main方法并创建spark context上下文(负责作业的全局调度)。执行算子,形成dag图交给dag scheduler,按照依赖关系划分stage输入task scheduler(负责分发任务)。WorkerNode上的Executor会主动向Task Scheduler申请任务,Task Scheduler根据申请情况将分发给Worker Node,让Executor(驻留在Worker Node中)派一个线程去执行。(到底分发给谁,是有一个基本原则:计算向数据靠拢,发现数据在机器A上,就把计算程序扔给机器A)任务在Executor运行得到结果再反馈给Task Scheduler再由它传递给DAG Scheduler然后交由SparkContext做最后处理,如返回给用户、写入HDFS等。

**SparkContext:**向资源管理器(yarn等)申请资源,并把作业分解成不同阶段,把每个阶段的任务调度到不同节点上去执行,会向executor分配资源如cpu内存,分配好资源之后启动Executor进程,

spark有哪些组件?

  • master:管理集群和节点,不参与计算。
  • worker:计算节点,进程本身不参与计算,和master汇报。
  • Driver:运行程序的main方法,创建spark context对象。
  • spark context:控制整个application的生命周期,包括dagsheduler和task scheduler等组件。
  • client:用户提交程序的入口。

持久化

231312


RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

mapRDD.cache()或者mapRDD.persist(),cache其实就是调用的无参persist默认StorageLevel.MEMORY_ONLY是持久化到内存(JVM的堆内存中),想持久化到磁盘就mapRDD.persist(StorageLevel.DISK_ONLY)

缓存有可能丢失,存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制(cache 操作会增加血缘关系,不改变原有的血缘关系)保证了即使缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部 Partition。

Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如:reduceByKey)。这样做的目的是为了当一个节点 Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用 persist 或 cache。

image-20230528143650701

注意:持久化操作是在行动算子执行时完成的。

checkpoint

所谓的检查点其实就是通过将 RDD 中间结果写入磁盘,当spark应用程序特别复杂,从初始的RDD开始到最后整个应用程序完成有很多的步骤,而且整个应用运行时间特别长,这种情况下就比较适合使用checkpoint功能。如果检查点之后有节点出现问题,可以从检查点开始重做血缘(等同于改变数据源),减少了开销。对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。

持久化与CheckPoint 比较

1)Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点切断血缘依赖。
2)Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint 的数据通常存储在 HDFS 等容错、高可用的文件系统,可靠性高。
3)建议对 checkpoint()的 RDD 使用 Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存中读取数据即可,否则需要再从头计算一次 RDD。因为checkpoint是单独计算的,会再执行一遍作为checkpoint的检查点结果。(之后在RDD所处的job运行结束之后,会启动一个单独的job,来将checkpoint过的RDD数据写入之前设置的文件系统,进行高可用、容错的类持久化操作。)

持久化和CheckPoint的应用场景

cache: 对于会被重复使用,但是数据量不是太大的 RDD,可以将其 cache()到内存当中。 cache 是每计算出一个要 cache 的 partition 就直接将其 cache 到内存中。缓存完之后,可以 在任务监控界面 storage 里面看到缓存的数据。

**checkpoint:**对于 computing chain 计算链过长或依赖其他 RDD 很多的 RDD,就需要进行 checkpoint,将其放入到 HDFS 或者本地文件夹当中。需要注意的是,checkpoint 需要等到 job 完成了,再启动专门的 job 去完成 checkpoint 操作,因此 RDD 是被计算了两次的。一般使用的时候配合 rdd.cache(),这样第二次就不用重新计算 RDD 了,直接读取 cache 写磁盘。

Spark Shuffle

图示

…原理

依赖

image-20230528135833925

Task和分区的关系。

1班(分区1)的1、2进入new不需要等2班(分区2),故没有阶段这一概念。

image-20230528140811163阶段1每个分区都是完成状态才可以进入下一个阶段。

image-20230528141017126

OneToOne(窄依赖)

image-20230527203221994

shuffle依赖(宽依赖)

其实是没有宽依赖的,不是窄就是宽。

image-20230528135854538

如何划分state

遇到一个宽依赖就划分一个state

spark排除故障

1、故障一:控制 reduce 端缓冲大小以避免 OOM

在Shuffle过程,reduce端task并不是等到map端task将其数据全部写入磁盘后再去拉取,而 是map端写一点数据,reduce端task就会拉取一小部分数据,然后立即进行后面的聚合、算子 函数的使用等操作reduce端task能够拉取多少数据,由reduce拉取数据的缓冲区buffer来决定, 因为拉取过来的数据都是先放在buffer中,然后再进行后续的处理,buffer的默认大小为 48MB

reduce端task会一边拉取一边计算,不一定每次都会拉满48MB的数据,可能大多数时候拉取一部分数据就处理掉了

虽然说增大reduce端缓冲区大小可以减少拉取次数,提升Shuffle性能,但是有时map端的数据量非常大,写出的速度非常快,此时reduce端的所有task在拉取的时候,有可能全部达到自己缓冲的最大极限值,即48MB,此时,再加上reduce端执行的聚合函数的代码,可能会创建大量的对象,这可难会导致内存溢出,即OOM 如果一旦出现reduce端内存溢出的问题,我们可以考虑减小reduce端拉取数据缓冲区的大小, 例如减少为12MB

在实际生产环境中是出现过这种问题的,这是典型的以性能换执行的原理。reduce端拉取数据 的缓冲区减小,不容易导致OOM,但是相应的,reudce端的拉取次数增加,造成更多的网络 传输开销,造成性能的下降

注意,要保证任务能够运行,再考虑性能的优化

2、故障二:JVM GC 导致的 shuffle 文件拉取失败

在 Spark 作业中,有时会出现 shuffle file not found 的错误,这是非常常见的一个报错,有 时出现这种错误以后,选择重新执行一遍,就不再报出这种错误

出现上述问题可能的原因是 Shuffle 操作中,后面 stage 的 task 想要去上一个 stage 的 task 所在的 Executor 拉取数据,结果对方正在执行 GC,执行 GC 会导致 Executor 内所有的工作 现场全部停止,比如 BlockManager、基于 netty 的网络通信等,这就会导致后面的 task 拉取 数据拉取了半天都没有拉取到,就会报出 shuffle file not found 的错误,而第二次再次执行 就不会再出现这种错误

可以通过调整 reduce 端拉取数据重试次数和 reduce 端拉取数据时间间隔这两个参数来对 Sh uffle 性能进行调整,增大参数值,使得 reduce 端拉取数据的重试次数增加,并且每次失败后 等待的时间间隔加长

JVM GC 导致的 shuffle 文件拉取失败,设置如下参数:

val conf = new SparkConf()
    .set("spark.shuffle.io.maxRetries","60")
    .set("spark.shuffle.io.retryWait","60")

3、故障三:解决各种序列化导致的报错

当 Spark 作业在运行过程中报错,而且报错信息中含有 Serializable 等类似词汇,那么可能是 序列化问题导致的报错。

序列化问题要注意以下三点:

  1. 作为 RDD 的元素类型的自定义类,必须是可以序列化的;
  2. 算子函数里可以使用的外部的自定义变量,必须是可以序列化的;
  3. 不可以在 RDD 的元素类型、算子函数里使用第三方的不支持序列化的类型,例如 Connection。

4 、故障四:解决算子函数返回 NULL 导致的问题

在有些算子函数里面是需要我们有一个返回值的。但是,有时候我们可能对某些值,就是不想有什么返回值。我们如果直接返回NULL的话,那么可以不幸的告诉大家,是不行的,会报错的。但是如果碰到你的确是对于某些值,不想要有返回值的话,有一个解决的办法:

  1. 在返回的时候,返回一些特殊的值,不要返回null,比如“-999” 。
  2. 在通过算子获取到了一个RDD之后,可以对这个RDD执行filter操作,进行数据过滤。filter内可以对数据进行判定,如果是-999,那么就返回false,给过滤掉就可以了。
  3. 在filter之后,可以使用coalesce算子压缩一下RDD的partition的数量,让各个partition的数据比较紧凑一些,也能提升一些性能。

transformation 算子

  • map(func)映射
  • mapPartitions(func):类似于 map,但独立地在 RDD 的每一个分片上运行假设有 N个元素,有 M 个分区,那么map 的函数的将被调用 N 次,而 mapPartitions 被调用M 次
  • mapPartitionsWithIndex()带分区号次处理所有分区。
  • flatMap ()压平
  • glom()分区转换数组
  • groupBy()分组
  • filter()过滤
  • sample()采样
  • distinct()去重
  • coalesce()重新分区 Coalesce 算子包括:配置执行 Shuffle 和配置不执行 Shuffle 两
    种方式。
  • repartition()重新分区(执行 Shuffle)
  • sortBy()排序
  • union()并集
  • subtract ()差集
  • zip()拉链
  • partitionBy()按照 K 重新分区
  • reduceByKey()按照 K 聚合 V
  • groupByKey()按照 K 重新分组
  • sortByKey()按照 K 进行排序
  • aggregateByKey()按照 K 处理分区内和分区间逻辑
  • mapValues()只对 V 进行操作
  • join()连接 将相同 key 对应的多个 value 关联在一起
  • cogroup() 类似全连接,但是在同一个 RDD 中对 key 聚合 操作两个 RDD 中的 KV元素,每个 RDD 中相同 key 中的元素分别聚合成一个集合。

coalesce 和 repartition 区别:

  1. coalesce 重新分区,可以选择是否进行 shuffle 过程。由参数 shuffle: Boolean = false/true 决定。
  2. repartition 实际上是调用的 coalesce,进行 shuffle。

coalesce 一般为缩减分区,如果扩大分区,不使用 shuffle 是没有意义的,repartition 扩大分区执行 shuffle。

reduceByKey 和 groupByKey 区别:

  1. reduceByKey:按照 key 进行聚合,在 shuffle 之前有 combine(预聚合)操作,返 回结果是 RDD[k,v]。
  2. groupByKey:按照 key 进行分组,直接进行 shuffle。

action算子

  • reduce()聚合函数 聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据。

  • collect()以数组的形式返回数据集 以数组 Array 的形式返回数据集的所有元素。

  • count()返回 RDD 中元素个数

  • first()返回 RDD 中的第一个元素

  • take()返回由 RDD 前 n 个元素组成的数组

  • takeOrdered()返回该 RDD 排序后前 n 个元素组成的数组

  • aggregate() 先分区内聚合,然后分区间

  • fold() 折叠操作 是 aggregate()简化版

  • countByKey()统计每种 key 的个数

  • foreach(f)遍历 RDD 中每一个元素

  • save 相关算子 saveAsTextFile(path)保存成 Text 文件 saveAsSequenceFile保存成 Sequencefile 文件saveAsObjectFile(path) 序列化成对象保存到文件

引起 Shuffle 过程的 Spark 算子

reduceBykey:

groupByKey:

…ByKey:

reduceByKey 与 groupByKey 的区别(重点)

reduceByKey:按照 key 进行聚合,在 shuffle 之前有 combine(预聚合)操作,返回结 果是 RDD[k,v]。

groupByKey:按照 key 进行分组,直接进行 shuffle。 开发指导:reduceByKey 比 groupByKey,建议使用。但是需要注意是否会影响业务逻辑。

Repartition 和 Coalesce

关系:

两者都是用来改变 RDD 的 partition 数量的,repartition 底层调用的就是 coalesce 方法,coalesce(numPartitions, shuffle = true)

区别:

repartition 一定会发生 shuffle,coalesce 根据传入的参数来判断是否发生 shuffle


图一


图二

==注:==coalesce 默认是不会shuffle的,例如3个分区减少到2个分区,会把其中的两个分区合并到一个分区去(如图一),如果shuffle = true,那么3个分区的数据就会打乱,重新组成新的分区(如图二)。

hive

Hive SQL 的编译过程

SQL 转化为 MapReduce 的过程 整个编译过程分为六个阶段:

  1. Antlr 定义 SQL 的语法规则,完成 SQL 词法,语法解析,将 SQL 转化为抽象语法树 AST Tree
  2. 遍历 AST Tree,抽象出查询的基本组成单元 QueryBlock(查询块)
  3. 遍历 QueryBlock,翻译为执行操作树 OperatorTree(操作树)
  4. 逻辑层优化器进行 OperatorTree(操作树)变换,合并不必要的 ReduceSinkOperator (创建将发送到 Reducer 端的对),减少 shuffle 数据量
  5. 遍历 OperatorTree(操作树),翻译为 MapReduce 任务 6. 物理层优化器进行 MapReduce 任务的变换,生成最终的执行计划

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree), 是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每 个节点都表示源代码中的一种结构。

==物理执行计划中:==可以是mapreduce任务,也可以是spark任务,取决你所配置的引擎。

Hive 和数据库比较

Hive 和数据库除了拥有类似的查询语言,再无类似之处。
1、数据存储位置
Hive 存储在 HDFS 。数据库将数据保存在块设备或者本地文件系统中。
2、数据更新
Hive 中不建议对数据的改写。而数据库中的数据通常是需要经常进行修改的,
3、执行延迟
Hive 执行延迟较高。数据库的执行延迟较低。当然,这个是有条件的,即数据规模较
小,当数据规模大到超过数据库的处理能力的时候,Hive 的并行计算显然能体现出优势。
4、数据规模
Hive 支持很大规模的数据计算;数据库可以支持的数据规模较小。

内部表和外部表

1)内部表:当我们删除一个内部表时,Hive 也会删除这个表中数据。内部表不适合 和其他工具共享数据。

2)外部表:删除该表并不会删除掉原始数据,删除的是表的元数据

  • 未被 external 修饰的是内部表(managed table),被 external 修饰的为外部表(external table);
  • 内部表数据由 Hive 自身管理,外部表数据由 HDFS 管理;
  • 内部表数据存储的位置是 hive.metastore.warehouse.dir(默认:/user/hive/warehouse), 外部表数据的存储位置由自己制定;
  • 删除内部表会直接删除元数据(metadata)及存储数据;删除外部表仅仅会删除元数 据,HDFS 上的文件并不会被删除;
  • 对内部表的修改会将修改直接同步给元数据,而对外部表的表结构和分区进行修改, 则需要修复(MSCK REPAIR TABLE table_name;)

应用场景:

  • 因为hive内部表在删除表是同时删除表数据与元数据,而外部表删除的时候,仅仅会删除元数据,HDFS上的文件并不会被删除,所以外部表相对来说更加安全些,数据组织也更加灵活,方便共享源数据。
  • 如果所有的数据都由hive处理,则创建内部表;如果数据的处理由hive和其他工具一起处理,则创建外部表。
  • 我们在管理表不方便和其他工作共享数据。可以创建一个外部表指向这份数据,而并不需要对其具有所有权。

join优化

1、Common Join

Common Join是Hive中最稳定的join算法,其通过一个MapReduce Job完成一个join操作。Map端负责读取join操作所需表的数据,并按照关联字段进行分区,通过Shuffle,将其发送到Reduce端,相同key的数据在Reduce端完成最终的Join操作。

image-20231027190133395

需要注意的是,sql语句中的join操作和执行计划中的Common Join任务并非一对一的关系,一个sql语句中的相邻的且关联字段相同的多个join操作可以合并为一个Common Join任务。

2、Map Join

Map Join算法可以通过两个只有map阶段的Job完成一个join操作。其适用场景为大表join小表。若某join操作满足要求,则第一个Job会读取小表数据,将其制作为hash table,并上传至Hadoop分布式缓存(本质上是上传至HDFS)。第二个Job会先从分布式缓存中读取小表数据(意思是从HDFS拉取数据到nodeManager本地节点),并缓存在Map Task的内存中,然后扫描大表数据,大表切片,每一个map负责处理一个切片,这样在map端即可完成关联操作。

注:不管是map还是reduce都是运行在container里,而container又都在nodeManager,这样所有的Task节点都可以读取到hash table了。

image-20231027192838241

注:common join是两个maptask,而map join是一个maptask,所以每个mapper都加载hash table数据,然后就可以在maptask里完成join。

3、Bucket Map Join

Bucket Map Join是对Map Join算法的改进,其打破了Map Join只适用于大表join小表的限制,可用于大表join大表的场景。

Bucket Map Join的核心思想是:若能保证参与join的表均为分桶表,且关联字段为分桶字段,且其中一张表的分桶数量是另外一张表分桶数量的整数倍,就能保证参与join的两张表的分桶之间具有明确的关联关系,所以就可以在两表的分桶间进行Map Join操作了。这样一来,第一个job将小表的两个桶分别制作一个hash table上传到HDFS上(所谓的分布式缓存),第二个Job根据大表有几个桶就会有几个Mapper(这里是读取使用的是BucketInputFormat,专门根据桶去切片),每个Mapper只需要缓存一个小表bucket,然后每一个Mapper再去扫描对应的大表的bucket,这样就可以在map阶段完成join操作,最后再合并四个mapper即使join结果。

image-20231028153026785

4、Sort Merge Bucket Map Join

Sort Merge Bucket Map Join(简称SMB Map Join)基于Bucket Map Join。SMB Map Join要求,参与join的表均为分桶表,且需保证分桶内的数据是有序的,且分桶字段、排序字段和关联字段为相同字段,且其中一张表的分桶数量是另外一张表分桶数量的整数倍。(比Bucket Map join 多了一个排序要求)

SMB Map Join同Bucket Join一样,同样是利用两表各分桶之间的关联关系,在分桶之间进行join操作,不同的是,分桶之间的join操作的实现原理Bucket Map Join,两个分桶之间的join实现原理为Hash Join算法;而SMB Map Join,两个分桶之间的join实现原理为Sort Merge Join算法。

Hash Join和Sort Merge Join均为关系型数据库中常见的Join实现算法。Hash Join的原理相对简单,就是对参与join的一张表构建hash table,然后扫描另外一张表,然后进行逐行匹配。Sort Merge Join需要在两张按照关联字段排好序的表中进行,其原理如图所示:

image-20231028162125685

Hive中的SMB Map Join就是对两个分桶的数据按照上述思路进行Join操作。可以看出,SMB Map Join与Bucket Map Join相比,在进行Join操作时,Map端是无需对整个Bucket构建hash table,也无需在Map端缓存整个Bucket数据的,每个Mapper只需按顺序逐个key读取两个分桶的数据进行join即可。由于不需要缓存小表数据,故对内存要求低,当然也不用做hash操作。

Tips:由于已排序,所以不用全表扫描,每次只读取两个表的一部分数据进行关联,若关联不上又该怎么处理?这里只做了解,后续我要是被面试官问到img,我再补充。

Hive 优化

1、MapJoin

如果不指定 MapJoin 或者不符合 MapJoin 的条件,那么 Hive 解析器会将 Join 操作转换 成 Common Join,即:在 Reduce 阶段完成 join。容易发生数据倾斜。可以用 MapJoin 把小表全部加载到内存在 map 端进行 join,避免 reducer 处理。

2、行列过滤

  • 列处理:在 SELECT 中,只拿需要的列,如果有,尽量使用分区过滤,少用 SELECT *。
  • 行处理:在分区剪裁中,当使用外关联时,如果将副表的过滤条件写在 Where 后面, 那么就会先全表关联,之后再过滤。

3、列式存储

4、采用分区技术

5、合理设置 Map 数

(1)通常情况下,作业会通过 input 的目录产生一个或者多个 map 任务。

主要的决定因素有:input 的文件总个数,input 的文件大小,集群设置的文件块大小。

(2)是不是 map 数越多越好?

答案是否定的。如果一个任务有很多小文件(远远小于块大小 128m),则每个小文件也会被当做一个块,用一个 map 任务来完成,而一个 map 任务启动和初始化的时间远远大于逻辑处理的时间,就会造成很大的资源浪费。而且,同时可执行的 map 数是受限的。

(3)是不是保证每个 map 处理接近 128m 的文件块,就高枕无忧了?

答案也是不一定。比如有一个 127m 的文件,正常会用一个 map 去完成,但这个文件 只有一个或者两个小字段,却有几千万的记录,如果 map 处理的逻辑比较复杂,用一个 map 任务去做,肯定也比较耗时。

针对上面的问题 2 和 3,我们需要采取两种方式来解决:即减少 map 数和增加 map 数;

6、小文件进行合并

在 Map 执行前合并小文件,减少 Map 数:CombineHiveInputFormat 具有对小文件进行 合并的功能(系统默认的格式)。HiveInputFormat 没有对小文件合并功能。

7、合理设置 Reduce 数

Reduce 个数并不是越多越好

(1)过多的启动和初始化 Reduce 也会消耗时间和资源;

(2)另外,有多少个 Reduce,就会有多少个输出文件,如果生成了很多个小文件, 那么如果这些小文件作为下一个任务的输入,则也会出现小文件过多的问题;

在设置 Reduce 个数的时候也需要考虑这两个原则:处理大数据量利用合适的 Reduce 数;使单个 Reduce 任务处理数据量大小要合适;

8、常用参数

SET hive.merge.mapfiles = true; – 默认 true,在 map-only 任务结束时合并小文件

SET hive.merge.mapredfiles = true; – 默认 false,在 map-reduce 任务结束时合并小文件

SET hive.merge.size.per.task = 268435456; – 默认 256M

SET hive.merge.smallfiles.avgsize = 16777216; – 当输出文件的平均大小小于 16m 该值时,启动一个独立的 map-reduce 任务进行 文件 merge

9、开启 map 端 combiner(不影响最终业务逻辑)

set hive.map.aggr=true;

10、压缩(选择快的)

设置 map 端输出、中间结果压缩。(不完全是解决数据倾斜的问题,但是减少了 IO 读 写和网络传输,能提高很多效率)

11、开启JVM重用

数据倾斜

数据倾斜问题,通常是指参与计算的数据分布不均,即某个key或者某些key的数据量远超其他key,导致在shuffle阶段,大量相同key的数据被发往同一个Reduce,进而导致该Reduce所需的时间远超其他Reduce,成为整个任务的瓶颈。

非join导致的数据倾斜

1、Map-Side(combiner)

开启Map-Side聚合后,数据会现在Map端完成部分聚合工作。这样一来即便原始数据是倾斜的,经过Map端的初步聚合后,发往Reduce的数据也就不再倾斜了。最佳状态下,Map-端聚合能完全屏蔽数据倾斜问题。

相关参数:

--启用map-side聚合
set hive.map.aggr=true;

--用于检测源表数据是否适合进行map-side聚合。检测的方法是:先对若干条数据进行map-side聚合,若聚合后的条数和聚合前的条数比值小于该值,则认为该表适合进行map-side聚合;否则,认为该表数据不适合进行map-side聚合,后续数据便不再进行map-side聚合。
set hive.map.aggr.hash.min.reduction=0.5;

--用于检测源表是否适合map-side聚合的条数。
set hive.groupby.mapaggr.checkinterval=100000;

--map-side聚合所用的hash table,占用map task堆内存的最大比例,若超出该值,则会对hash table进行一次flush。
set hive.map.aggr.hash.force.flush.memory.threshold=0.9;
2、Skew-GroupBy(随机打散)

map-side维护一个hash table,当数据大的时候会flush到磁盘中,io操作会影响性能

Skew-GroupBy的原理是启动两个MR任务,第一个MR按照随机数分区,将数据分散发送到Reduce,完成部分聚合,第二个MR按照分组字段分区,完成最终聚合。

相关参数:

--启用分组聚合数据倾斜优化
set hive.groupby.skewindata=true;

join导致的数据倾斜

1、map-join

默认是使用common join算法,也就是通过一个MapReduce Job完成计算。Map端负责读取join操作所需表的数据,并按照关联字段进行分区,通过Shuffle,将其发送到Reduce端,相同key的数据在Reduce端完成最终的Join操作。

如果关联字段的值分布不均,就可能导致大量相同的key进入同一Reduce,从而导致数据倾斜问题。

使用map join算法,join操作仅在map端就能完成,没有shuffle操作,没有reduce阶段,自然不会产生reduce端的数据倾斜。该方案适用于大表join小表时发生数据倾斜的场景。

2、skew join

skew join是用来解决大表和大表进行关联发生倾斜的问题,那这里为什么不用Bucket Map join呢,他不是专门用来处理大表和大表的join吗?因为,Bucket Map join是将一个大表根据关联字段进行分桶,分桶其实就是一个mr任务,关联字段就是倾斜字段,那这个mr任务不又数据倾斜了吗?

skew join的原理是,为倾斜的大key单独启动一个map join任务进行计算,其余key进行正常的common join。原理图如下:

image-20231101144707811

在倾斜的Reducer1中,skew join会自动检测是否有倾斜的key,来自A表的K 1和来自B表的K 2一般来说也是一大(a-K 1)一小(b-K 2)的情况,然后走map join操作。

相关参数:

--启用skew join优化
set hive.optimize.skewjoin=true;
--触发skew join的阈值,若某个key的行数超过该参数值,则触发
set hive.skewjoin.key=100000;
4、调整SQL语句

若参与join的两表均为大表,其中一张表的数据是倾斜的,此时也可通过以下方式对SQL语句进行相应的调整。

假设原始SQL语句如下:A,B两表均为大表,且其中一张表的数据是倾斜的。

hive (default)>
select
    *
from A
join B
on A.id=B.id;

其join过程如下:

image-20231106184323464

图中1001为倾斜的大key,可以看到,其被发往了同一个Reduce进行处理。

调整SQL语句如下:

hive (default)>
select
    *
from(
    select --打散操作
        concat(id,'_',cast(rand()*2 as int)) id,
        value
    from A
)ta
join(
    select --扩容操作
        concat(id,'_',0) id,
        value
    from B
    union all  --小表变成原来的两倍数据量,不然关联不上呀
    			--数据量大不怕,给资源就是喽,怕的是倾斜
    select
        concat(id,'_',1) id,
        value
    from B
)tb
on ta.id=tb.id;

调整之后的SQL语句执行计划如下图所示:

image-20231106184204370

3、过滤null

4、倾斜的key单独算

复杂数据类型

1、基本数据类型
Hive说明定义
tinyint1byte有符号整数
smallint2byte有符号整数
int4byte有符号整数
bigint8byte有符号整数
boolean布尔类型,true或者false
float单精度浮点数
double双精度浮点数
decimal十进制精准数字类型decimal(16,2)表示为:16=小数位+整数位。2=小数位
varchar字符序列,需指定最大长度,最大长度的范围是[1,65535]varchar(32)
string字符串,无需指定最大长度
timestamp时间类型
binary二进制数据
2、复杂数据类型
类型说明定义取值
array数组是一组相同类型的值的集合arrayarr[0]
mapmap是一组相同类型的键-值对集合map<string, int>map[‘key’]
struct结构体由多个属性组成,每个属性都有自己的属性名和数据类型structid:int,name:stringstruct.id
3、类型转换

Hive的基本数据类型可以做类型转换,转换的方式包括隐式转换以及显示转换。

隐式转换:

a. 任何整数类型都可以隐式地转换为一个范围更广的类型,如tinyint可以转换成int,int可以转换成bigint。

b. 所有整数类型、float和string类型都可以隐式地转换成double。

c. tinyint、smallint、int都可以转换为float。

d. boolean类型不可以转换为任何其它的类型。

显示转换:

可以借助cast函数完成显示的类型转换。

cast('1' as int)

udf udaf udtf区别

UDF操作作用于单个数据行,并且产生一个数据行作为输出。大多数函数都属于这一类(比如数学函数和字符串函数)。
UDAF 接受多个输入数据行,并产生一个输出数据行。像COUNT和MAX这样的函数就是聚集函数。
UDTF 操作作用于单个数据行,并且产生多个数据行-------一个表作为输出。lateral view explore()
简单来说:
UDF:返回对应值,一对一
UDAF:返回聚类值,多对一
UDTF:返回拆分值,一对多

自定义过udf吗?

用过,讲广告数仓解析ip,ua那块,先搁这里。教学代码看idea的 udf_test 项目

自定义IP解析函数

我们采用的免费IP地址库为ip2region v2.0。

自定义一个名为parse_ip(“hdfs://hadoop102:8020//xx//xx//ip2region.xdb”,ip)的函数,第一个参数是存放在hdfs上的ip解析库路径。第二个参数ip地址,返回的是结构体。

将代码打包上传到hdfs上,然后在hive中创建函数:

image-20231103213339937

代码实现:

package com.atguigu.ad.hive.udf;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF;
import org.apache.hadoop.hive.serde2.objectinspector.ConstantObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.io.IOUtils;
import org.lionsoul.ip2region.xdb.Searcher;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;

public class ParseIP extends GenericUDF {

    //定义IP定位查找器
    private Searcher searcher;

    @Override
    public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException {

        //校验参数个数
        if (arguments.length != 2) {
            throw new UDFArgumentException("parse_ip函数需要接受两个参数");
        }

        //检查第1个参数是否是STRING类型
        ObjectInspector hdfsPathOI = arguments[0];
        if (hdfsPathOI.getCategory() != ObjectInspector.Category.PRIMITIVE) {
            throw new UDFArgumentException("parse_ip函数的第1个参数应为基本数据类型");
        }
        PrimitiveObjectInspector primitiveHttpURLOI = (PrimitiveObjectInspector) hdfsPathOI;
        if (PrimitiveObjectInspector.PrimitiveCategory.STRING != primitiveHttpURLOI.getPrimitiveCategory()) {
            throw new UDFArgumentException("parse_ip函数的第1个参数应为STRING类型");
        }

        //构造Searcher对象
        if (hdfsPathOI instanceof ConstantObjectInspector) {

            //获取第一个参数(HDFS路径)的值(常量)
            String filePath = ((ConstantObjectInspector) hdfsPathOI).getWritableConstantValue().toString();
            //创建Path对象
            Path path = new Path(filePath);
            Configuration conf = new Configuration();
            try {
                //获取HDFS文件系统客户端
                FileSystem fs = FileSystem.get(conf);
                //打开文件获取输入流
                FSDataInputStream inputStream = fs.open(path);
                //创建ByteArrayOutputStream(可将内容写入字节数组)
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                //使用Hadoop提供的IOUtils将输入流的全部内容拷贝到输出流
                IOUtils.copyBytes(inputStream, outputStream, 1024);
                //有输出流得到字节数组
                byte[] buffer = outputStream.toByteArray();
                //构造基于内存的Searcher
                searcher = Searcher.newWithBuffer(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        //检查第2个参数是否是STRING类型
        ObjectInspector ipOI = arguments[1];
        if (ipOI.getCategory() != ObjectInspector.Category.PRIMITIVE) {
            throw new UDFArgumentException("parse_ip函数的第2个参数应为基本数据类型");
        }
        PrimitiveObjectInspector primitiveIPOI = (PrimitiveObjectInspector) ipOI;
        if (PrimitiveObjectInspector.PrimitiveCategory.STRING != primitiveIPOI.getPrimitiveCategory()) {
            throw new UDFArgumentException("parse_ip函数的第2个参数应为STRING类型");
        }

        //构造函数返回值的对象检查器
        ArrayList<String> structFieldNames = new ArrayList<>();
        ArrayList<ObjectInspector> structFieldObjectInspectors = new ArrayList<>();

        structFieldNames.add("country");
        structFieldNames.add("area");
        structFieldNames.add("province");
        structFieldNames.add("city");
        structFieldNames.add("isp");

        structFieldObjectInspectors.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);
        structFieldObjectInspectors.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);
        structFieldObjectInspectors.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);
        structFieldObjectInspectors.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);
        structFieldObjectInspectors.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        return ObjectInspectorFactory.getStandardStructObjectInspector(structFieldNames, structFieldObjectInspectors);
    }

    @Override
    public Object evaluate(DeferredObject[] arguments) throws HiveException {

        //获取IP地址
        String ipAddress = arguments[1].get().toString();

        //构造返回值对象,Struct的列名已经在initialize方法中声明,故此处只需返回值即可
        ArrayList<Object> result = new ArrayList<>();
        try {
            //查找ip对应的region
            String region = searcher.search(ipAddress);
            //切分region
            String[] split = region.split("\\|");
            //构造放回值
            result.add(split[0]);
            result.add(split[1]);
            result.add(split[2]);
            result.add(split[3]);
            result.add(split[4]);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result;
    }

    @Override
    public String getDisplayString(String[] children) {
        //用于在Explain执行计划中展示函数调用信息
        return getStandardDisplayString("parse_ip", children);
    }
}

窗口函数

1

202012291848205063

Serde

四个by

1、order by

hive sql执行过程:

image-20231028164233753

注意:**在hive中慎用!**如果是做全局排序的话,所有的数据会进入到一个reduce。

如果非要用 一般搭配limit,map端会做一些优化,例如每个Map有100w数据,加上limit100之后,每个map只会返回前100条数据,这样reduce只用从200中选取前100即可,而不是200w中选取前100。

...
order by sql
limit 100

那如果就需要全局有序,那没招,只能给reduce加资源img

2、sort by

Sort by为每个reduce产生一个排序文件。每个Reduce内部进行排序,因此,如果用 sort by进行排序并且设置 mapped. reduce. tasks > 1,则 sort by只会保证每个 reducer的输出有序,并不保证全局有序。(全排序实现:先用 sortby保证每个 reducer输出有序,然后在进行 order by归并下前面所有的 reducer输出进行单个 reducer排序,实现全局有序。)

3、distribute by

distribute by是控制在map端如何拆分数据给reduce端的。hive会根据 distribute by后面列,对应 reduce的个数进行分发,默认是采用hash算法。sort by为每个 reduce产生一个排序文件。在有些情况下,你需要控制某个特定行应该到哪个 reducer,这通常是为了进行后续的聚集操作。distribute by刚好可以做这件事。因此, distribute by经常和 sort by配合使用。

4、cluster by

如果Distribute by和Sort By的字段相同,则等价于cluster by
解释:先对数据进行分区,再对分区中的数据进行排序
但是排序只能是升序排序,不能指定排序规则为ASC或者DESC

分区(重点)

Hive中的分区就是把一张大表的数据按照业务需要分散的存储到多个目录,每个目录就称为该表的一个分区。在查询时通过where子句中的表达式选择查询所需要的分区,这样的查询效率会提高很多。

一级分区

1、建表语句
hive (default)> 
create table dept_partition
(
    deptno int,    --部门编号
    dname  string, --部门名称
    loc    string  --部门位置
)
    partitioned by (day string) --分区字段
    row format delimited fields terminated by '\t';

注:day可以当做一个普通的字段来使用

2、分区表读写数据

(1)load

  • 数据准备:

在/opt/module/hive/datas/路径上创建文件dept_20220401.log,并输入如下内容。

[atguigu@hadoop102 datas]$ vim dept_20220401.log

10 行政部 1700

20 财务部 1800

  • 装载语句
hive (default)> 
load data local inpath '/opt/module/hive/datas/dept_20220401.log' 
into table dept_partition 
partition(day='20220401');
  • 查询结果

image-20231028093204393

(2)insert

  • 插入语句
hive (default)> 
insert overwrite table dept_partition partition (day = '20220402')
select deptno, dname, loc
from dept_partition
where day = '2022401';
  • 查询结果

image-20231028093942213

==注意:查询的时候会多一个day字段,查询分区表数据时,可以将分区字段看作表的伪列,可像使用其他字段一样使用分区字段,==hdfs是没有这个字段的,所以查询出来的day字段来自路劲分区信息。

注意指定 partition (day = ‘20220402’)

二级分区

思考:如果一天内的日志数据量也很大,如何再将数据拆分?答案是二级分区表,例如可以在按天分区的基础上,再对每天的数据按小时进行分区。

1、建表语句
hive (default)>
create table dept_partition2(
    deptno int,    -- 部门编号
    dname string, -- 部门名称
    loc string     -- 部门位置
)
partitioned by (day string, hour string)
row format delimited fields terminated by '\t';
2、二级分区表读写数据

(1)数据装载

hive (default)> 
load data local inpath '/opt/module/hive/datas/dept_20220401.log' 
into table dept_partition2 
partition(day='20220401', hour='12');

(2)查询分区数据

hive (default)> 
select 
    * 
from dept_partition2 
where day='20220401' and hour='12';

动态分区

动态分区是指向分区表insert数据时,==被写往的分区不由用户指定,而是由每行数据的最后一个字段的值来动态的决定。==使用动态分区,可只用一个insert语句将数据写入多个分区。

1、动态分区相关参数

(1)动态分区功能总开关(默认true,开启)

set hive.exec.dynamic.partition=true

(2)严格模式和非严格模式

动态分区的模式,默认strict(严格模式),要求必须指定至少一个分区为静态分区,nonstrict(非严格模式)允许所有的分区字段都使用动态分区。

set hive.exec.dynamic.partition.mode=nonstrict

(3)一条insert语句可同时创建的最大的分区个数,默认为1000。

set hive.exec.max.dynamic.partitions=1000

(4)单个Mapper或者Reducer可同时创建的最大的分区个数,默认为100。

set hive.exec.max.dynamic.partitions.pernode=100

(5)一条insert语句可以创建的最大的文件个数,默认100000。

hive.exec.max.created.files=100000

(6)当查询结果为空时且进行动态分区时,是否抛出异常,默认false。

hive.error.on.empty.partition=false
2、建表语句
hive (default)> 
create table dept_partition_dynamic(
    id int, 
    name string
) 
partitioned by (loc int) 
row format delimited fields terminated by '\t';
3、设置动态分区
set hive.exec.dynamic.partition.mode = nonstrict;
hive (default)> 
insert into table dept_partition_dynamic 
partition(loc) 
select 
    deptno, 
    dname, 
    loc 
from dept;

:在满足dept_partition_dynamic两个字段的基础上,需要多选一个字段作为动态分区字段。

分区表基本操作(修复分区)

(1)查询表所有分区信息

hive> show partitions dept_partition;

(2)增加分区

  • 增加单个分区

    hive (default)> 
    alter table dept_partition 
    add partition(day='20220403');
    
  • 增加多个分区(分区之间不能有逗号)

    hive (default)> 
    alter table dept_partition 
    add partition(day='20220404') partition(day='20220405');
    

(3)删除分区

  • 删除单个分区

    hive (default)> 
    alter table dept_partition 
    drop partition (day='20220403');
    
  • 删除多个分区(分区之间必须有逗号)

    hive (default)> 
    alter table dept_partition 
    drop partition (day='20220404'), partition(day='20220405');
    

(4)修复分区==(以HDFS路径为准,去修改元数据)==

Hive将分区表的所有分区信息都保存在了元数据中,只有元数据与HDFS上的分区路径一致时,分区表才能正常读写数据。若用户手动创建/删除分区路径,Hive都是感知不到的,这样就会导致Hive的元数据和HDFS的分区路径不一致。再比如,若分区表为外部表,用户执行drop partition命令后,分区元数据会被删除,而HDFS的分区路径不会被删除,同样会导致Hive的元数据和HDFS的分区路径不一致。

若出现元数据和HDFS路径不一致的情况,可通过如下几种手段进行修复。

  • add partition

    若手动创建HDFS的分区路径,Hive无法识别,可通过add partition命令增加分区元数据信息,从而使元数据和分区路径保持一致。

  • drop partition

    若手动删除HDFS的分区路径,Hive无法识别,可通过drop partition命令删除分区元数据信息,从而使元数据和分区路径保持一致。

  • **msck(metastore-check)**重要常用!!

    不需要像add partition需要指定某个分区,可以自动识别增加或删除分区。

    若分区元数据和HDFS的分区路径不一致,还可使用msck命令进行修复,以下是该命令的用法说明。

    hive (default)> 
    msck repair table table_name [add/drop/sync partitions];
    

    说明:

    msck repair table table_name add partitions:该命令会增加HDFS路径存在但元数据缺失的分区信息。

    msck repair table table_name drop partitions:该命令会删除HDFS路径已经删除但元数据仍然存在的分区信息。

    msck repair table table_name sync partitions:该命令会同步HDFS路径和元数据分区信息,相当于同时执行上述的两个命令(以HDFS有无为基准)。

    msck repair table table_name:等价于msck repair table table_name add partitions命令。

分桶(重点)

分区提供一个隔离数据和优化查询的便利方式。不过,并非所有的数据集都可形成合理的分区。对于一张表或者分区,Hive 可以进一步组织成桶,也就是更为细粒度的数据范围划分,分区针对的是数据的存储路径分桶针对的是数据文件

分桶表的基本原理是:首先为每行数据计算一个指定字段的数据的hash值,然后模以一个指定的分桶数,最后将取模运算结果相同的行,写入同一个文件中,这个文件就称为一个分桶(bucket)。

1、建表语句

hive (default)> 
create table stu_buck_sort(
    id int, 
    name string
)
clustered by(id) sorted by(id) --必须指定按照哪个字段分桶(hash)
into 4 buckets   --分区到几个文件里(一个桶代表一个文件)
row format delimited fields terminated by '\t';

Tip:分桶字段是表中已存在字段,而分区表的分区字段不能是表中已存在字段。

如果加上 sorted by(id) 说明是一个分桶排序表,两个字段可以不一样,分桶排序表桶文件的数据会按照指定字段排序。

2、数据装载

(1)数据准备

在/opt/module/hive/datas/路径上创建student.txt文件,并输入如下内容。

1001	student1
1002	student2
1003	student3
1004	student4
1005	student5
1006	student6
1007	student7
1008	student8
1009	student9
1010	student10
1011	student11
1012	student12
1013	student13
1014	student14
1015	student15
1016	student16

(2)导入数据到分桶表中

说明:Hive新版本load数据可以直接跑MapReduce,老版的Hive需要将数据传到一张表里,再通过查询的方式导入到分桶表里面。

hive (default)> 
load data local inpath '/opt/module/hive/datas/student.txt' 
into table stu_buck;

(3)查看创建的分桶表中是否分成4个桶

image-20231028150532597

(4)观察每个分桶中的数据

有了分区为什么还要分桶

1、获得更高的查询处理效率。桶为表加上了额外的结构,Hive在处理有些查询时能利用这个结构,如Bucket Map Join

2、使取样( sampling)更高效。在处理大规模数据集时,在开发和修改查询的阶段,如果能在数据集的一小部分数据上试运行查询,会带来很多方便。

总结:对于一张表或者分区,Hive 可以进一步组织成桶,也就是更为细粒度的数据范围划分,分区针对的是数据的存储路径分桶针对的是数据文件

不同:与分区不同的是,分区依据的不是真实数据表文件中的列,而是我们指定的伪列,但是分桶是依据数据表中真实的列而不是伪列。

简述delete,drop,truncate的区别

delete 删除数据
drop 删除表
truncate table 表名 清空表中所有的数据

union all和union,full join的区别

union 去重 union all 不去重
union(all)是行拼接,full join是列拼接

kafka

好相关概念

massage: kafka 中最基本的传递对象,有固定格式。

topic: 一类消息,如 page view,click 行为等。

producer: 产生信息的主体,可以是 服务器 日志信息等。

consumer: 消费 producer 产生话题消息的主体。

broker: 消息处理结点,多个 broker 组成 kafka 集群。

partition: topic 的物理分组,每个 partition 都是一个有序队列。

segment: 多个大小相等的段组成了一个 partition。

offset: 一个连续的用于定位被追加到分区的每一个消息的序列号,最大值为 64 位的 long 大小,19 位数字字符长度。

kafka架构

image-20231105202506326

在某个topic里的数据,例如log拆分成多个partition,这体现了分治的思想,每个patition在最终存储的时候又会存多个副本(replication),不同的副本会存在不同节点,这样的话任意的一个节点挂掉,数据不会丢失,但其实这是存在风险的,因为kafka利用了缓存,数据在真正落盘之前都要在Block case里缓存,增加磁盘的读写性能,缓存满了或者失效了,缓存里的数据才会往磁盘进行溢写。这种情况下就一定会带来风险,一旦集群断电了,缓存里的数据还没来得及往磁盘里溢写,这个时候数据就丢失了。为保证性能的同时又尽量保证数据不丢失,kafka是如何做到的?跳至 《Kafka 数据怎么保障不丢失?》

kafka为什么快?

  • 顺序读写:Kafka 的消息是不断追加到文件中的,这个特性使 Kafka 可以充分利用磁盘的顺序读写性能,顺序读写不需要硬盘磁头的寻道时间,只需很少的扇区旋转时间,所以速度远快于随机读写
  • 零拷贝:跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区”。系统上下文切换减少为 2 次,可以提升一倍的性能
  • 文件分段:Kafka 的队列 topic 被分为了多个区 partition,每个 partition 又分为多个段 segment,每次文件操作都是对一个小文件的操作,非常轻便,同时也增加了并行处理能力
  • 批量发送:Kafka 允许进行批量发送消息,先将消息缓存在内存中,然后一次请求批量发送出去

细说kafka零拷贝

零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。

聊聊传统IO流程

比如:读取文件,再用socket发送出去
传统方式实现:
先读取、再发送,实际经过1~4四次copy。

buffer = File.read 
Socket.send(buffer)

1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。
image-20231027153111954

注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从内核缓冲区传输到socket buffer也就是第5步。

kafka文件传输调用的是java NIO里面的transferTo()方法,实际上java的transferTo()最终调用的是linux的sendfile()方法。

Kafka 数据怎么保障不丢失?

分三个点说,一个是生产者端,一个消费者端,一个 broker 端。

1、生产者数据的不丢失

1、producer端

  • 如果是同步模式: ack 设置为 0,风险很大,一般不建议设置为 0。即使设置为 1,也会随着 leader 宕机丢失数据。所以如果要严格保证生产端数据不丢失,可设置为-1。
  • ==如果是异步模式:==也会考虑 ack 的状态,除此之外,异步模式下的有个 buffer,通过 buffer 来进行控 制数据的发送,有两个值来进行控制,时间阈值与消息的数量阈值,如果 buffer 满 了数据还没有发送出去,有个选项是配置是否立即清空 buffer。可以设置为-1,永久阻塞,也就数据不再生产。异步模式下,即使设置为-1。也可能因为程序员的不 科学操作,操作数据丢失,比如 kill -9,但这是特别的例外情况。
  • producer本身提供一个重试参数叫retries,如果因为网络问题或者broker故障发送失败,那么producer会自动重试。

2、broker端

ack机制+broker的副本机制

每个 broker 中的 partition 我们一般都会设置有 replication(副本)的个数,生产 者写入的时候首先根据分发策略(有 partition 按 partition,有 key 按 key,都没有 轮询)写入到 leader 中,follower(副本)再跟 leader 同步数据,这样有了备份, 也可以保证消息数据的不丢失。在结合《ack机制》。

3、consumer端

通过 offset commit 来保证数据的不丢失,kafka 自己记录了每次消费的 offset 数值, 下次继续消费的时候,会接着上次的 offset 进行消费。只要producer和broker数据没丢失,consumer端几乎不会丢失,即便出现没有消费完数据就提交更新了offset,我们也可以通过重新调整offset的值来实现重新消费。

ACK机制

image-20231105203549459

注:

ack=1 中等,如果在 follower 同步成功之前 leader 故障,那么将会丢失数据

ack=0 最低

ack=-1 最高

当ack=-1时,当producer把数据发送到leader以后,在ISR列表里面,也就是候选人列表里边follower会立即从leader这一块进行数据同步,都完成数据同步以后,kafka才会向生产者返回ack,生产者再继续发送其他的消息,虽然说性能有所下降,但数据可靠性提高了。因为返回ack的时候数据已经在多个节点里了,任意一个节点挂掉对系统没影响。这里存在一个问题,当ISR中的follower长时间与leader数据不同步,超过一定时间后就会被移除ISR,这时候ISR只剩一个从副本了,这时候可靠性就会降级。还有一种情况就是两个follower都被移出ISR了,ISR现在为空,这时候ack=-1这种级别也被称为all级别,降级成了ack=1这个级别,这个时候可以通过如下参数去限制:

  • 调整ISR最小副本数,配合ACK=all(-1)使用,follower不足就会报错NotEnoughRepicasException
min.insync.replicas
  • 生产者设置重试次数(处理RetriableException),也可以通过Callback处理其他异常.不能说一报错上边的错,程序就终止了,设置重试次数。
retries
  • 设置幂等性保证单分区数据不重复,多分区幂等性需要采用事务性Producer
enable.idempotenc=true

即便如此还是存在丢失的情况,如果整个集群宕机,那么缓存里的数据肯定就没有了,它本身是用来抗压的,需要高性能,可靠性自然达不到金融级别的数据安全。这时候就得以来producer,在生产消息的时候也往mysql里写一份,顺手再往kafka推。

kafka两种存储方式

Kafka的持久化机制是通过将消息写入磁盘来实现数据的持久性和可靠性。当消息被写入Kafka时,它们首先会被缓存到内存中以实现高吞吐量和低延迟的传递。然后,Kafka会定期将缓存中的消息批量写入磁盘,以确保数据的持久化。在持久化完成之前,如果发生了重启或故障,尚未写入磁盘的数据会丢失。

Kafka提供了两种存储方式:缓存日志

  1. 缓存存储:Kafka使用内存来缓存最近的消息。当一个消息被写入Kafka后,它首先被写入内存中的缓存。然后,Kafka使用一定的时间间隔将缓存中的消息批量写入磁盘。缓存中的消息可以快速地被读取和获取,以提供低延迟的读写性能。缓存的大小可以通过Kafka配置中的log.flush.interval.messageslog.flush.interval.ms参数来配置。

  2. 日志存储:Kafka使用日志文件来持久化消息。当缓存中的消息被写入磁盘时,它们被追加到一个或多个日志文件中。每个分区都有一个对应的日志文件,每个日志文件都有一个递增的偏移量来唯一标识消息。这种日志存储方式保证了消息的持久性和顺序性,并且可以在需要时进行高效地读取和检索。

重复消费

**1、重复消费:**已经消费了数据,但是offset没提交。

image-20231106151457859

以下两种情况造成重复消费:

==重复消费情况一:==kafka是通过offset这个标记来维护当前已经消费了的数据,消费者每消费一批数据,kafka broker就会更新offset的值(当前值+1)避免重复消费,默认情况下,消息消费完之后会自动提交offset值,kafka消费端自动提交offset逻辑里有一个默认5秒的一个间隔,所以例如在消费数据提交offset的后五秒之内,consumer挂了,这时候就会导致offset没有提交,从而会产生重复消费的问题。

==重复消费情况二:==kafka中有一个partition balance的机制就是把多个patition均衡的分配给多个消费者,consumer端会从分配的partition里面去消费消息,如果consumer在默认的5分钟以内没办法处理完这一批消息,就会触发kafka的Rebalance机制。从而导致offset自动提交失败,在Balance之后consumer端还是从没有提交offset的位置开始去消费,从而导致重复消费的一个问题。

解决:

1、提高消费端的处理性能避免触发Balance:

  • 可以使用异步or多线程的方式去处理消息,缩短单个消息消费的时长
  • 调整消息处理的超时时间(例如默认5分钟调整至10分钟)
  • 减少一次性从Broker上拉取数据的条数

2、可以针对消息生产md5然后保存到mysql或者redis里面,在处理消息之前先去mysql或者redis里面判断是否已经消费过。这其实是利用幂等性的方式来实现。(但对于海量数据这不是一个可行的方案

漏消费的情况

image-20231106170555142

==漏消费情况:==设置ofset为**手动**提交,当offset被提交时,数据还在内存中未落盘,此时刚好消费者线程被kill掉,那么ofset已经提交,但是数据未处理,导致这部分内存中的数据丢失。

kafka如何保证消息消费的顺序

只能保证partition内消息有序,不能保证partition间的有序。如果真需要partition间有序,根据业务需要把数据有序的打到一个partition中。(自定义路由规则)

什么是ISR,为什么要引入ISR

ISR:(In-Sync Replicas)与 leader 保持同步的 follower 集合 (所有与 leader 副本保持一定程 度同步的副本(包括 Leader)组成 ISR)

==前置原因:==发送到Broker的消息,最终是以partition的一个物理形态存储在磁盘上,而kafka为了保证kafka的可靠性,提供了Partition的副本机制,在副本集合中存在一个Leader partition和多个follower partition。生产者发送消息都时候,会先把消息存在Leader Partition里面,然后再把消息复制到follower partition中。这样一旦Leader节点挂了,可以从剩下的partition里选取一个leader,然后消费者可以继续从新的leader partition中去获取数据。

**为什么引入:**在partiton的多副本设计方案里,有两个需求:1、副本数据的同步。2、新Leader的选举。而这两个需求都会进行网络通信,kafka为了避免网络延迟带来的性能问题,以及尽可能的去保证新选取的leader里面的数据是最新的。所以设计了一个ISR这样的方案。

如果某个follower里的数据落后leader太多,就会被踢出ISR(当追上leader的时候,再加入ISR),即ISR列表里的节点数据一定是最新的,所以后续的leader选举只需要从ISR列表中筛选即可。

所以:

  1. 尽可能的保证数据同步的效率,因为同步效率不高的节点都会被踢出ISR列表
  2. 避免数据丢失,因为ISR里面的节点数据是和Leader副本最接近的

补充可以看**《ACK机制》**

Kafka 消息数据积压,消费能力不足怎么处理?

  1. 如果是 Kafka 消费能力不足,则可以考虑增加 Topic 的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。(两者缺一不可)
  2. 如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过 少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会 造成数据积压。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值