01-Hadoop面试题
壹—HDFS
HDFS的组成
1)NameNode :就是Master,它是一个主管、管理者。
-
管理HDFS的名称空间
-
配置副本策略
-
管理数据块映射信息
-
处理客户端的读写请求
2)DataNode
-
存储实际的数据块
-
执行数据块的读写操作
3)Client
-
文件切分。文件上传HDFS的时候,Client将文件切分成一个一个的Block,然后进行上传;
-
与NameNode交互,获取文件位置信息;
-
与DataNode交互,读取或者写入数据;
-
Client提供一些命令来管理HDFS,比如NameNode格式化;
-
Client可以通过一些命令来访问HDFS,比如对HDFS增删改查操作;
4)Secondary NameNode:并非NameNode的热备。当NameNode挂掉的时候,它并不能马上替换NameNode并提供服务。
- 辅助NameNode,分担其工作量,比如定期合并FsImage和Edits,并推送给NameNode;
- 在紧急情况下,可辅助回复NameNode。
HDFS文件块的问题
块大小问题
HDFS中的文件是分块存储,块的大小可以通过配置参数(dfs.blocksize)来规定,默认大小在Hadoop中是128M,1.x版本中是64M。寻址时间为传输时间的1%时,则为最佳状态。因此传输时间10ms/0.01=1S。目前磁盘普遍传输效率100M/S。1S*100M/S=100MB,而在计算机中,一般容量都是取2的n次方,所以我们取128M为块大小。寻址时间、传输时间的平衡
为啥不能太小或者太大?
太小:会增加寻址时间,程序一直在找块的开始位置;
太大:会增加传输时间,程序在处理数据的时候会非常慢;
块大小的设置主要取决于磁盘传输速率,当磁盘传输达到200M/S或者300M/S的时候,可以适当的提高块的大小。
HDFS的写数据流程
1)客户端通过DistributedFileSystem模块向NameNode请求上传文件,NameNode检查目标文件是否存在,父目录是否存在;
2)NameNode返回是否可以上传;
3)客户端请求第一个Block上传到哪几个DataNode服务器上;
4)NameNode返回3个DataNode节点,分别为dn1、dn2、dn3;
5)客户端通过FSDataOutputStream模块请求dn1上的数据,dn1收到请求后会继续调用dn2,然后dn2调用dn3,将这个通信管道建立完成。
6)dn1,dn2,dn3逐级应答客户端。
7)客户端开始往dn1上传第一个Block,以Packet为单位,dn1收到一个Packet就会传到dn2,dn2传给dn3;dn1每传一个packet会放入一个应答队列等待应答。
8)当一个Block传输完成之后,客户端再次请求NameNode上传第二个Block的服务器。(重复执行3-7)
节点距离计算
两个节点到达最近的共同祖先的距离总和。
机架感知
//BlockPlacementPolicyDefault
protected Node chooseTargetInOrder(int numOfReplicas, Node writer, Set<Node> excludedNodes, long blocksize, int maxNodesPerRack, List<DatanodeStorageInfo> results, boolean avoidStaleNodes, boolean newBlock, EnumMap<StorageType, Integer> storageTypes) throws NotEnoughReplicasException {
int numOfResults = results.size();
if (numOfResults == 0) {//第一个副本
DatanodeStorageInfo storageInfo = this.chooseLocalStorage((Node)writer, excludedNodes, blocksize, maxNodesPerRack, results, avoidStaleNodes, storageTypes, true);//选择本地
writer = storageInfo != null ? storageInfo.getDatanodeDescriptor() : null;
--numOfReplicas;
if (numOfReplicas == 0) {
return (Node)writer;
}
}
DatanodeDescriptor dn0 = ((DatanodeStorageInfo)results.get(0)).getDatanodeDescriptor();
if (numOfResults <= 1) {
this.chooseRemoteRack(1, dn0, excludedNodes, blocksize, maxNodesPerRack, results, avoidStaleNodes, storageTypes);//选择远程机架
--numOfReplicas;
if (numOfReplicas == 0) {
return (Node)writer;
}
}
if (numOfResults <= 2) {
DatanodeDescriptor dn1 = ((DatanodeStorageInfo)results.get(1)).getDatanodeDescriptor();
if (this.clusterMap.isOnSameRack(dn0, dn1)) {
this.chooseRemoteRack(1, dn0, excludedNodes, blocksize, maxNodesPerRack, results, avoidStaleNodes, storageTypes);//选择远程机架
} else if (newBlock) {
this.chooseLocalRack(dn1, excludedNodes, blocksize, maxNodesPerRack, results, avoidStaleNodes, storageTypes);
} else {
this.chooseLocalRack((Node)writer, excludedNodes, blocksize, maxNodesPerRack, results, avoidStaleNodes, storageTypes);
}
--numOfReplicas;
if (numOfReplicas == 0) {
return (Node)writer;
}
}
this.chooseRandom(numOfReplicas, "", excludedNodes, blocksize, maxNodesPerRack, results, avoidStaleNodes, storageTypes);
return (Node)writer;
}
副本节点选择?
第一个副本在Client所处的节点上,如果客户端在集群外,随机选择一个。
第二个副本在另一个机架的随机一个节点。
第三个副本在第二个副本所在机架的随机节点。
HDFS读数据流程
1)客户端通过DistributedFileSystem向NameNode请求下载文件,NameNode通过查询元数据,找到文件块所在的DataNode地址。
2)挑选一台DataNode(就近原则,然后随机)服务器,请求读取数据。
3)DataNode开始传输给客户端(从磁盘里面读取数据输入流,以Packet为单位来做校验)。
4)客户端以Packet为单位接收,先在本地缓存,然后写入目标文件。
串行的读
NameNode和Secondary NameNode工作机制
第一阶段:NameNode启动
1)第一次启动NameNode格式化后,创建Fsimage和Edits文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。
2)客户端对元数据进行增删改的请求。
3)NameNode记录操作日志,更新滚动日志。
4)NameNode在内存中对元数据进行增删改。
第二阶段:SecondaryNameNode
1)Secondary NameNode询问NameNode是否需要CheckPoint。直接带回NameNode是否检查结果。
2)Secondary NameNode请求执行Checkpoint。
3)NameNode滚动正在写的Edits日志。
4)将滚动前的编辑日志和镜像文件拷贝到SecondaryNameNode。
5)Secondary NameNode加载编辑日志和镜像文件到内存,并合并。
6)生成的镜像文件fsimage。chkpoint。
7)拷贝fsimage.chkpoint到NameNode。
8)NameNode将fsImage.chkpoint重新命名成fsimage。
DataNode工作机制
1)一个数据块在DataNode上以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是元数据块的长度,块数据的校验和,以及时间戳。
2)DataNode启动后向NameNode注册,通过后,周期性(6小时)的向NameNode上报所有块信息。
3)心跳是每3秒一次,心跳返回结果带有NameNode给该DataNode的命令如复制块数据到另一台机器,或删除某个数据块。如果超过十分钟没有收到某个DataNode的心跳,则认为该节点不可用。
4)集群中可以安全加入和退出一些机器。
贰—MapReduce
一、概述
MapReduce核心思想
1)分布式的运算程序往往分成2个阶段。
2)第一个阶段的MapTask并发实例,完全并行运行,互不相干。
3)第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
4)MapReduce编程模型只能包含一个Map阶段和Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
MapReduce的提交流程
二、序列化机制
三、MapReduce框架原理
Setup map/reduce clearup
1.数据输入
切片与MapTask并行度决定机制
数据块:Block是HDFS物理上把数据分成一块一块。数据块是HDFS存储数据单位。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask。
1)一个Job的Map阶段并行度由客户端在提交Job时的切片数决定
2)每一个Split切片分配一个MapTask并行实例处理
3)默认情况下,切片大小=BlockSize
4)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
Job提交流程
https://www.bilibili.com/video/BV1Qp4y1n7EN?p=88&spm_id_from=pageDriver
waitForCompletion()
submit();
// 1 建立连接
connect();
// 1)创建提交Job 的代理
new Cluster(getConfiguration());
// (1)判断是本地运行环境还是yarn 集群运行环境
initialize(jobTrackAddr, conf);
// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的Stag 路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取jobid ,并创建Job 路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝jar 包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向Stag 路径写XML 配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(),job.getCredentials());
FileInputFormat 切片源码解析(input.getSplits(job))
3个要点
1)按照每个文件单独切片2)computteSplitSize blockSize minSize maxSize3)1.1倍
1)先找到数据存储的目录
2)开始遍历处理目录下的每一个文件
3)遍历每一个文件:
a)获取文件大小;
b)计算切片大小computeSplitSize(Math.max(minSize,Math.min(maxSize,blockSize)))=blockSize=128M;
c)默认情况下,切片大小=blockSize;
d)开始切,形成第1个切片:ss.txt——0:128M,第2个切片ss.txt——128:256M第3个切片ss.txt——256:300M(每次切片是,都要判断切完剩下的部分是否大于块的1.1倍,不大于就划分一块切片)
e)整个切片信息写到一个切片规划文件中
f)整个切片的核心过程都在getSplit()方法中完成
g)InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。
4)提交切片规划文件到YARN上,YARN上的MrApplicationMaster就可以根据切片规划文件计算开启MapTask个数。
FileInputFormat切片机制
1)切片机制
1.简单地按照文件的内容长度进行切片
2.切片大小,默认等于Block大小
3.切片时不考虑数据集整体,而是逐个针对每一个文件单独切分
2)参数设置
1.切片大小计算公式Math.max(minSize,Math.min(maxSize,blockSize))——默认情况下切片大小=blockSize
mapreduce.input.fileinputformat.split.minsize=1默认为1
mapreduce.input.fileinputformat.split.maxsize=Long.MAXValue
3)切片大小设置
maxsize切片最大值:参数如果调的比blocksize小,则会让切片表小,而且就等于配置的这个参数的值。
minsize切片最小值:参数调的比blockSize大,则可以让切片变得比blockSize还大
CombineTextInputFormat切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
1)虚拟存储过程:
将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize 值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2 倍,此时将文件均分成2 个虚拟存储块(防止出现太小切片)。
例如setMaxInputSplitSize 值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M 逻辑划分,就会出现0.02M 的小的虚拟存储文件,所以将剩余的4.02M 文件切分成(2.01M 和2.01M)两个文件。
(2)切片过程:
(a)判断虚拟存储的文件大小是否大于setMaxInputSplitSize 值,大于等于则单独形成一个切片。
(b)如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
(c)测试举例:有4 个小文件大小分别为1.7M、5.1M、3.4M 以及6.8M 这四个小文件,则虚拟存储之后形成6 个文件块,大小分别为:
1.7M,(2.55M、2.55M),3.4M 以及(3.4M、3.4M)
最终会形成3 个切片,大小分别为:
(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
2.MapReduce详细工作流程
1.找到待处理的文件
2.客户端submit前,先做切片
3.提交信息:切片信息,jar包,job.xml
4.计算出MapTask数量
5.默认TextInputFormat,RecoderReader、InputFormat
6.读完之后输入到Mapper
7.输出到outputCollector,环形缓冲区——索引+数据,默认100M,写到80%反向写找到index和data的中间部分,然后开始反向写
8.对于分区内部的数据进行排序,快排,对索引进行排序
9.一些到磁盘,还是写到同一个文件
10.merge归并排序,对已经有序的多个数据进行排序——磁盘上
11.有combiner的话可以进行一个预聚合
12.所有的MapTask完成后,启动响应数量的reduceTask,并告知ReduceTask处理返范围
13.下载到ReduceTask本地磁盘,合并文件,归并排序
14.一次读取一组
15.分组
16.默认TextOutputFormat
3.Shuffle机制
shuffle机制
map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
1)MapTask手机map方法输出的kv对,放到内存缓冲区中
2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
3)多个溢写文件会被合并成大的溢出文件
4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
5)ReduceTask根据自己的分区号,去各个MapTask机器上取响应的结果分区数据
6)ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
7)合并成大文件后,shuffle过程也就结束了,后面进入ReduceTask的逻辑运算过程
注:
1)shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
2)缓冲区的大小可以通过参数调整, mapreduce.task.io.sort.mb默认100M。
分区
默认分区:
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
(1)如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
(2)如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件part-r-00000;
排序
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
排序分类
(1)部分排序MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
(2)全排序最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
(3)辅助排序:(GroupingComparator分组) 在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
(4)二次排序在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
Combiner
(1)Combiner是MR程序中Mapper和Reducer之外的一种组件。
(2)Combiner组件的父类就是Reducer。
(3)Combiner和Reducer的区别在于运行的位置Combiner是在每一个MapTask所在的节点运行;
(4)Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
(5)Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv 应该跟Reducer的输入kv类型要对应起来。
4.数据输出
自定义Outputformat
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
LogRecordWriter lrw = new LogRecordWriter(job);
return lrw;
}
}
实现RecordWriter类
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream atguiguOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
// 创建两条流
try {
FileSystem fs = FileSystem.get(job.getConfiguration());
atguiguOut = fs.create(new Path("D:\\hadoop\\atguigu.log"));
otherOut = fs.create(new Path("D:\\hadoop\\other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
String log = key.toString();
// 具体写
if (log.contains("atguigu")){
atguiguOut.writeBytes(log+"\n");
}else {
otherOut.writeBytes(log+"\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
// 关流
IOUtils.closeStream(atguiguOut);
IOUtils.closeStream(otherOut);
}
}
在driver中设置自定义的outputformat
//设置自定义的outputformat
job.setOutputFormatClass(LogOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path("D:\\input\\inputoutputformat"));
//虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
//而fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("D:\\hadoop\\output1111"));
5.MapReduce内核源码解析
MapTask工作机制
(1)Read 阶段:MapTask 通过InputFormat 获得的RecordReader,从输入InputSplit 中解析出一个个key/value。
(2)Map 阶段:该节点主要是将解析出的key/value 交给用户编写map()函数处理,并产生一系列新的key/value。
(3)Collect 收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value 分区(调用Partitioner),并写入一个环形内存缓冲区中。
(4)Spill 阶段:即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
溢写阶段详情:
步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
(5)Merge阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并mapreduce.task.io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
ReduceTask工作机制
(1)Copy 阶段:ReduceTask 从各个MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Sort 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照MapReduce 语义,用户编写reduce()函数输入数据是按key 进行聚集的一组数据。为了将key 相同的数据聚在一起,Hadoop 采用了基于排序的策略。由于各个MapTask 已经实现对自己的处理结果进行了局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
(3)Reduce 阶段:reduce()函数将计算结果写到HDFS 上。
ReduceTask并行度决定机制
(1)ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。
(2)ReduceTask默认值就是1,所以输出文件个数为一个。
(3)如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜
(4)ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask。
(5)具体多少个ReduceTask,需要根据集群性能而定。
(6)如果分区数不是1,但是ReduceTask为1,是否执行分区过程。答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1 肯定不执行。
MapTask & ReduceTask源码解析
6.压缩
叁—Yarn
基础架构
工作机制
提交流程
调度器与调度算法
FIFO
Capacity Scheduler——雅虎
Fair Scheduler——非死不可
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决