1、Hadoop组成结构
1.1、MapReduce架构:分布式计算框架
-
Map阶段:各个任务并行处理数据
-
Reduce阶段:对map结果进行汇总
1.2、Yarn架构:资源调度框架
-
NodeManager(NM):单个节点的leader,管理YARN集群中的每个节点
-
管理单个节点的资源;
-
处理来自ResourceManager的命令;
-
处理来自ApplicationMaster的命令;
-
-
ApplicationMaster(AM): 管理在YARN内运行的每个应用程序实例
-
负责数据的切分,任务的监控与容错;
-
为应用程序申请资源分配给内部任务;
-
-
ResourceManager(RM):全局资源调度器,负责整个系统资源的管理和分配。
-
处理客户端的请求,进行资源的分配与调度;
-
监控NodeManager;
-
启动或者监控ApplicationMaster;
-
-
Container:任务资源管理器
-
任务运行环境的资源管理;任务运行时,Yarn分配给AM的资源(内存、CPU、磁盘、网络等)时,RM为AM返回的资源便是用Container表示的,YARN会为每个任务分配一个Container,且该任务只能使用该Container中描述的资源。
-
1.3、HDFS架构:分布式存储框架
-
NameNode(NN):master,管理资源文件元数据
-
处理客户端的读写请求;
-
管理HDFS的命名空间;
-
配置副本策略;
-
管理数据块(Block)的映射信息;
-
-
DataNode (DN): slave,数据具体存储
-
存储实际的数据块;
-
执行数据块的读写操作;
-
-
Secondary NameNode(SNN):分担NN的压力
-
辅助NameNode ,分担工作量,定期合并Fsimage 和 Edits,合并成新的Fsimage,推送给NameNode;
-
辅助NameNode恢复;
-
2、HDFS读写流程:
2.1、数据写入:
-
client向nameNode 请求文件上传,nameNode检查目标文件是否存在,父目录是否存在;
-
nameNode返回是否可以上传;
-
client对文件切分,请求第一个block传输到哪些DataNode服务器上;
-
NameNode 返回3个DataNode 服务器DataNode1,DataNode2,DataNode3;
-
client请求3台中的一台DataNode1(网络拓扑最近的一台)上传数据(RPC,建立pipeline),DataNode1 收到请求继续调用DataNode2 ,然后DataNode2 调用DataNode3 ,将整个pipeline建立完成,然后逐级返回客户端;
-
dn1、dn2、dn3逐级应答客户端。
-
Client开始往DataNode 1上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位。写入的时候DataNode会进行数据校验。DataNode 1收到一个packet就会传给DataNode 2,DataNode 2传给DataNode 3,DataNode 1每传一个packet会放入一个应答队列等待应答;
-
当一个Block传输完成之后,客户端再次请求NameNode上传第二个Block到服务器。(重复执行3-7步);
2.2、数据读取:
-
客户端通过Distributed FileSystem向NameNode请求下载文件,NameNode通过查询元数据,找到文件块所在的DataNode地址;
-
挑选一台DataNode(网络拓扑就近原则,然后随机)服务器,请求建立socket流读取数据;
-
DataNode开始传输数据给客户端(从磁盘里面读取数据输入流,以Packet为单位来做校验);
-
客户端以Packet为单位接收,先在本地缓存,然后写入目标文件。
3、NN和2NN工作机制
Fsimage:NameNode内存中元数据序列化后形成的文件,在 /data/tmp/dfs/name/current目录下生成
fsimage_0000000000000000
fsimage_0000000000000000.md5
seen_txid
VERSION
-
Fsimage:HDFS文件系统元数据的一个永久性的检查点,其中包含HDFS文件系统所有的目录和文件inode的序列化信息。
-
Edits:存放HDFS文件系统的所有更新操作的路径,文件系统客户端执行的所有写操作,会被优先记录到Edits文件中。
-
seen_txid:文件保存的是一个数字,就是最后一个edits_的数字;
-
每次NameNode启动的时候会将Fsimage文件读入内存,加载Edits里的更新操作,保证内存的元数据信息是最新的、同步的,可以看成NameNode启动的时候,将Fsimage和Edits文件进行了合并。
工作机制:
-
Edits:记录客户端更新元数据信息的每一步操作(可通过Edits运算出元数据)。
-
NameNode启动时,先滚动Edits并生成一个空的edits.inprogress,然后加载Edits和Fsimage到内存中,此时NameNode内存就持有最新的元数据信息。Client开始对NameNode发送元数据的增删改的请求,这些请求的操作首先会被记录到edits.inprogress中(查询元数据的操作不会被记录在Edits中,因为查询操作不会更改元数据信息),如果此时NameNode挂掉,重启后会从Edits中读取元数据的信息。然后,NameNode会在内存中执行元数据的增删改的操作。
-
由于Edits中记录的操作会越来越多,Edits文件会越来越大,导致NameNode在启动加载Edits时会很慢,所以需要对Edits和Fsimage进行合并(所谓合并,就是将Edits和Fsimage加载到内存中,照着Edits中的操作一步步执行,最终形成新的Fsimage)。SecondaryNameNode的作用就是帮助NameNode进行Edits和Fsimage的合并工作。
-
SecondaryNameNode首先会询问NameNode是否需要CheckPoint(触发CheckPoint需要满足两个条件中的任意一个,定时时间到和Edits中数据写满了)。直接带回NameNode是否检查结果。SecondaryNameNode执行CheckPoint操作,首先会让NameNode滚动Edits并生成一个空的edits.inprogress,滚动Edits的目的是给Edits打个标记,以后所有新的操作都写入edits.inprogress,其他未合并的Edits和Fsimage会拷贝到SecondaryNameNode的本地,然后将拷贝的Edits和Fsimage加载到内存中进行合并,生成fsimage.chkpoint,然后将fsimage.chkpoint拷贝给NameNode,重命名为Fsimage后替换掉原来的Fsimage。NameNode在启动时就只需要加载之前未合并的Edits和Fsimage即可,因为合并过的Edits中的元数据信息已经被记录在Fsimage中。
4、mapReduce工作流程
(1)、客户端执行submit()方法之前,会先获取待读取文件的信息;
(2)、将文件切片信息,jar包,job.xml 提交到yarn;
(3)、yarn根据job.xml ,启动切片数量相应的MapTask;
(4)、MapTask 调用inputFormat()方法读取HDFS文件,InputFormat()方法调用RecordRead()方法,默认TextInputFormat将数据以行首字母的偏移量为key,一行数据为value,传到map()方法;
(5)、map()方法做一些业务处理之后,将数据传输到分区方法中,将数据进行分区标注后,发送到环形缓冲区中。
(6)、环形缓冲区默认大小为100MB,达到80%后进行溢写;
(7)、溢写之前排序,按照key的字典序(快排);
(8)、溢写会产生大量溢写文件,会调用merge()方法,并用归并排序,默认10个溢写文件合并成一个大文件。
(9)、在不影响最终结果的前提下,可以先做一次combiner的操作;
(10)、等待所有的mapTask结束之后,会启动一定数量的reducerTask(根据分区数);
(11)、reduceTask会拉去map端数据到内存,内存不够时,写磁盘,待全部数据拉取完毕之后,会进行一次归并排序;
(12)、对并排序后的文件会在进行一次分组操作,将数据以组为单位发送到reduce()方法。
(13)、reduce()方法之后,做一些逻辑判断,调用outputFormat()方法,outputFormat调用RecordWirte()将数据以kv形式写到HDFS上。
5、Job提交流程:
5.1、Job提交源码:
Driver:
log.info("-----------job提交-------");
boolean b = job.waitForCompletion(true);
job.submit();
//1.建立连接
this.connect();
//(1)、创建job代理
new Cluster(Job.this.getConfiguration());
//(1.1)根据conf判断是本地还是yarn
initialize(jobTrackAddr, conf);
//2.提交job
submitter.submitJobInternal(Job.this, Job.this.cluster);
//(1)、创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
//(2)、获取jobid
JobID jobId = this.submitClient.getNewJobID();
//(3)、拷贝jar包到集群
this.copyAndConfigureFiles(job, submitJobDir);
//(3.1)、文件上传
rUploader.uploadFiles(job, jobSubmitDir);
//(4)、计算切片,生产切片文件
int maps = this.writeSplits(job, submitJobDir);
//(4.1)、获取切片数据
List<InputSplit> splits = input.getSplits(job);
//(4.1.1)、生成切片大小
long splitSize = this.computeSplitSize(blockSize, minSize, maxSize);
//(4.1.1.1)
Math.max(minSize, Math.min(maxSize, blockSize));
//(4.1.2)、切片个数判断
(double)bytesRemaining / (double)splitSize > 1.1D;
//(5)、向Stag路径写xml配置文件
writeConf(conf, submitJobFile);
//(5.1)、写xml
conf.writeXml(out);
//(6)、提交Job,返回提交状态
submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
5.2、FileInputFormat切片源码解析:
(1)、程序先找到源文件目录;
(2)、遍历该目录下的所有文件;
(3)、遍历文件:
a、获取文件大小 fs.sizeOf(xx.txt)
b、计算切片大小 computeSplitSize( Math.max(minSize, Math.min(maxSize, blockSize)))=blockSize = 128M; 默认情况下,切片大小 = blockSize ,2.x版本liunx =128M, windows = 32M;
c、文件开始切片,形成第一个切片:(double)bytesRemaining / (double)splitSize > 1.1D;
每次切片时,都需要判断切片完成剩下的部分是否大于块的1.1倍,不大于1.1倍就划分为1个切片。
d、将切片信息写到一个切片job.split中;
f、整个切片的核心过程是getSplits()方法;
g、InputSplit记录了切片的元数据信息,比如起始位置、长度以及所在的结点列表。
(4)、提交切片到Yarn上,Yarn上的MrAppMaster就可以根据切片规划文件计算开启MapTask数。
5.3、FileInputFormat参数
(1)、源码中计算切片公式
class FileInputFormat
Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.maxsize = 1L;
mapreduce.input.fileinputformat.split.maxsize = 9223372036854775807L; LongMaxValue
因此,默认情况下,切片大小=blockSize
(2)、切片大小设置
通过公式: Math.max(minSize, Math.min(maxSize, blockSize)); 可以推出:调整 maxSize,使其比blockSize小,会让切片变小,切片大小 = maxSize 调整 minSize,使其比blockSize大,会让切片变大,切片大小 = minSize
6、DataNode工作机制
-
一个数据块在DataNode上以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是元数据包括数据块的长度,块数据的校验和,以及时间戳。
-
DataNode启动后向NameNode注册,通过后,周期性(1小时)的向NameNode上报所有的块信息。
-
心跳是每3秒一次,心跳返回结果带有NameNode给该DataNode的命令如复制块数据到另一台机器,或删除某个数据块。如果超过10分钟没有收到某个DataNode的心跳,则认为该节点不可用。
-
集群运行中可以安全加入和退出一些机器。
7、HDFS高可用
-
Zookeeper为基础的集群上,NameNode 部署在2个节点上;两个NameNode 在ZK中谁先注册,谁就是Active,剩余的就是Standly状态;而同一时间只有一个NameNode 对外提供服务 ->Active NameNode。
-
Zookeeper中有两个FailoverController,一个负责ANN的状态,一个监控SNN的状态,FailoverController通过心跳负责将监控信息保存在znode中。
-
Standly NN负责同步Active NN中的元数据信息,也接收Activie NN的块报告,所有的DN上的Block 块的信息会同时发送给active NN 和 Standly NN,这样他们的信息能够同步。(JN进程进行主从复制)
-
Edits日志只有Active状态的NameNode 节点可以进行写操作,但是两个NameNode 都可以读取Edits;
-
当Active NN节点发生故障,zookeeper 通过FailoverController 感知到,ZK会自动销毁Active znode(排它锁)。Standly NN的监控器检测到Active NN的znode被移除,通知NameNode 节点开始工作,并发送 kill -9 nameNode 进程号,杀掉进程,Standly NN切换到Active状态
8、HDFS 动态扩容、缩容
扩容:
-
新机器上修改hosts配置,设置免密登录(ssh-copy-id)
-
安装环境(jdk,hadoop)
-
在NameNode机器的hdfs-site.xml配置文件中增加dfs.hosts属性
-
新节点启动DataNode(hadoop-daemon.sh start datanode)
-
配置负载均衡;(sbin/start-balancer.sh -threshold 5)【各个节点与集群总的存储使用率相差不超过5%,默认10%】,等待集群自均衡完成即可。
-
新的机器上单独启动NodeManager(yarn-daemon.sh start nodemanager)
-
yarn node -list查看集群情况
缩容:
-
退役节点服务器配置目录(etc/hadoop)下创建dfs.hosts.exclude文件,添加需要退役的主机名称(ip或者主机名),在hdfs-site.xml配置文件中添加dfs.hosts.exclude属性,值为新增文件的全路径。如下:
<property> <name>dfs.hosts.exclude</name> <value>/etc/hadoop/dfs.hosts.exclude</value> </property>
-
刷新集群,在NameNode机器上执行命令(hdfs dfsadmin -refreshNodes),刷新NameNode,刷新ResourceManager。
hdfs dfsadmin -refreshNodes 刷新NameNode yarn rmadmin -refreshNodes 刷新ResourceManager
-
等待退役节点状态为decommissioned(所有的块已经复制完成),停止该节点以及资源管理器。注:如果副本数为3,退役节点小于3,是不能退役成功的,需要修改副本数后才能退役。
sbin/hadoop-daemon.sh stop datanode sbin/yarn-daemon.sh stop nodemanager sbin/start-balancer.sh 重新负载
9、用mapreduce怎么处理数据倾斜问题?
数据倾斜:map /reduce程序执行时,reduce节点大部分执行完毕,但是有一个或者几个reduce节点运行很慢,导致整个程序的处理时间很长,这是因为某一个key的条数比其他key多很多(有时是百倍或者千倍之多),这条key所在的reduce节点所处理的数据量比其他节点就大很多,从而导致某几个节点迟迟运行不完,此称之为数据倾斜。
(1)局部聚合加全局聚合。
第一次在 map 阶段对那些导致了数据倾斜的 key 加上 1 到 n 的随机前缀,这样本来相
同的 key 也会被分到多个 Reducer 中进行局部聚合,数量就会大大降低。
第二次 mapreduce,去掉 key 的随机前缀,进行全局聚合。
思想:二次 mr,第一次将 key 随机散列到不同 reducer 进行处理达到负载均衡目的。第
二次再根据去掉 key 的随机前缀,按原 key 进行 reduce 处理。
这个方法进行两次 mapreduce,性能稍差。
(2)增加 Reducer,提升并行度
JobConf.setNumReduceTasks(int)
(3)实现自定义分区
根据数据分布情况,自定义散列函数,将 key 均匀分配到不同 Reducer
10、Shuffle过程详解
具体来说:就是将maptask输出的处理结果数据,分发给reducetask,并在分发的过程中,对数据按key进行了分区和排序;
1)Map 方法之后 Reduce 方法之前这段处理过程叫 Shuffle
2)Map 方法之后,数据首先进入到分区方法,把数据标记好分区,然后把数据发送到 环形缓冲区;环形缓冲区默认大小 100m,环形缓冲区达到 80%时,进行溢写;溢写前对数据进行排序,排序按照对 key 的索引进行字典顺序排序,排序的手段快排;溢写产生大量溢写文件(在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序),需要对溢写文件进行归并排序;对溢写的文件也可以进行 Combiner 操作,前提是汇总操作,求平均值不行。最后将文件按照分区存储到磁盘,等待 Reduce 端拉取。
3)每个 Reduce 拉取 Map 端对应分区的数据。拉取数据后先存储到内存中,内存不够 了,再存储到磁盘。拉取完所有数据后,采用归并排序将内存和磁盘中的数据都进行排序。
在进入 Reduce 方法前,可以对数据进行分组操作,合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)
注意:
Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M。
11、排序
11.1、快排:
private static int[] qsort(int arr[] ,int start, int end){
int tem = arr[start]; //定义分界值
int i = start;
int j = end ;
while (i < j){ //直到 i = j时,排序完成
while ((i <j) && (arr[j] > tem)){ //将大于或等于分界值的数据集中到数组右边
j--;
}
while ((i < j) && (arr[i] < tem)){ //小于分界值的数据集中到数组的左边
i++ ;
}
if((arr[i] == arr[j]) && (i <j)){ //相同的数,但是位置不对
i++;
}else { //交换顺序
// int temp = arr[i];
// arr[i] = arr[j];
// arr[j] = temp;
swap(arr,i,j);
}
}
if (i - 1 >start) arr = qsort(arr,start,i-1);
if (j + 1 <end ) arr = qsort(arr,j+1,end);
return arr;
}
private static void swap(int[] x, int a, int b) {
int t = x[a];
x[a] = x[b];
x[b] = t;
}
11.2、归并排序:
/**
* 归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用分治思想
* 分段有序(2分法),再把排好序的合并
* @param src 需要排序的数组
* @param low 开始位置
* @param high 结束位置
*/
private static int[] mergeSort(int[] src, int low, int high) {
if (low == high)
return new int[] { src[low] };
int mid = low + (high - low) / 2;
int[] leftArr = mergeSort(src, low, mid); //左有序数组
int[] rightArr = mergeSort(src, mid + 1, high); //右有序数组
int[] newNum = new int[leftArr.length + rightArr.length]; //新有序数组
int m = 0, i = 0, j = 0;
while (i < leftArr.length && j < rightArr.length) {
newNum[m++] = leftArr[i] < rightArr[j] ? leftArr[i++] : rightArr[j++];
}
while (i < leftArr.length)
newNum[m++] = leftArr[i++];
while (j < rightArr.length)
newNum[m++] = rightArr[j++];
return newNum;
}