Hadoop的简介
Apache Hadoop是一款支持数据密集型分布式应用程序并以Apache 2.0许可协议发布的开源软件框架,有助于使用许多电脑组成的网络来解决数据、计算密集型的问题。基于MapReduce计算模型,它为大数据的分布式存储与处理提供了一个软件框架。所有的Hadoop模块都有一个基本假设,即硬件故障是常见情况,应该由框架自动处理。
Apache Hadoop的核心模块分为存储和计算模块,前者被称为Hadoop分布式文件系统(HDFS),后者即MapReduce计算模型。Hadoop框架先将文件分成数据块并分布式地存储在集群的计算节点中,接着将负责计算任务的代码传送给各节点,让其能够并行地处理数据。这种方法有效利用了数据局部性,令各节点分别处理其能够访问的数据。与传统的超级电脑架构相比,这使得数据集的处理速度更快、效率更高。
Hadoop组件
HDFS
HDFS分布式存储框架,适合海量数据的存储
HDFS的原理:是一种允许文件通过网络在多台主机上分享的文件系统,可让多机器上的多用户分享文件和存储空间
通透性;让实际上是通过网络来访问文件的动作,由程序与用户看来,就像是访问本地的磁盘一般。
容错;即使系统中有某些节点宕机,整体来说系统仍然可以持续运作而不会有数据损失【通过副本机制实现】。
分布式文件管理系统很多,hdfs只是其中一种,不合适小文件。
HDFS的节点
一、管理节点NameNode
NameNode是整个文件系统的管理节点,它维护着整个文件系统的目录树,文件目录的元信息和每个文件对应的数据块列表,接收用户的操作请求。
①维护每个DataNode的心跳,用于查看数据节点是否存活
②用于管理HDFS中所有的文件信息,包括文件的路径、文件的block块信息、文件的大小及其权限
③响应客户端的请求,根据上传或下载返回请求的结果
二、数据节点DataNode
DataNode节点提供真实文件数据的存储服务
①用于存储客户端上传的具体文件,文件上传过程会进行数据的切分,根据block块大小会将文件切分成多块,并且block块会存储在多个DataNode中
②响应客户端的上传请求和下载请求
③检测和管理存储在本节点上的block块信息
Block块为HDFS中基本的存储单位,但并不是最小的存储单位
一个文件的长度大小是size,那么从文件的偏移开始,按照固定的大小,顺序对文件进行划分并编号,划分好的每一个块称为一个block。
BLock块大小:
1.在Hadoop1.X中大小默认为64M
2.在Hadoop2.X中大小默认为128M
为什么BLock块大小为128M?
问题:
1. 如果一个文件不进行做拆分,对应一个DataNode中存储一个完整的文件,对导致DataNode存取压力过大,由于计算本地化,所以计算压力也会很大,由此需要拆分
2. 如果拆分过大? 会导致1问题,同时下载速度也会很慢
3. 如果拆分过小? 对应一个拆分过的文件会特别多,那么对应的元数据信息也会很多,会导致NameNode压力过大,检索数据信息时间会很长
基于以上问题如何设计?
规定:一个数据的检索时间最长不超过10ms,规定检索时间为BLock数据下载时间的1%为1秒
数据下载时间由网络及磁盘决定,由于磁盘和网络传输时间在100M/s,基于这个理论值得到 block块大小是在128M
设置BLock块大小
hdfs-site.xml中dfs.blocksize属性
副本数
Replication。多副本。默认是三个
设置hdfs-site.xml的dfs.replication属性
副本机制与机架?
如果当前集群设置副本数为3 那么对应存放位置为在同一个机架中会存放两个份,其他机架中保存一份,可以保证数据的安全。
为什么block块不每个机架存放一份?
由于后续做数据计算时,会将计算结果进行汇总,如果数据过于分散,那么后续网络传输压力会很大。
三、Secondary NameNode
Secondary NameNode是NameNode的备份节点
①由于NameNode需要保存元数据信息,并且元数据信息是放在内存中的
②数据存放在内存中会有安全性问题,需要持久化
③由于NameNode对外请求压力大,并不适合做持久化工作,所以交由Secondary NameNode
HDFS的读写
一、HDFS的读流程
1.首先调用FileSystem对象的open方法,其实是一个DistributedFileSystem的实例
2.DistributedFileSystem通过rpc获得文件的第一个block的locations,同一block按照副本数会返回多个locations,这些locations按照hadoop拓扑结构排序,距离客户端近的排在前面.
3.前两步会返回一个FSDataInputStream对象,该对象会被封装成DFSInputStream对象,DFSInputStream可以方便的管理datanode和namenode数据流。客户端调用read方法,DFSInputStream最会找出离客户端最近的datanode并连接。
4.数据从datanode源源不断的流向客户端。
5.如果第一块的数据读完了,就会关闭指向第一块的datanode连接,接着读取下一块。这些操作对客户端来说是透明的,客户端的角度看来只是读一个持续不断的流。
6.如果第一批block都读完了,DFSInputStream就会去namenode拿下一批blocks的location,然后继续读,如果所有的块都读完,这时就会关闭掉所有的流
如果在读数据的时候,DFSInputStream和datanode的通讯发生异常,就会尝试正在读的block的排第二近的datanode,并且会记录哪个datanode发生错误,剩余的blocks读的时候就会直接跳过该datanode。DFSInputStream也会检查block数据校验和,如果发现一个坏的block,就会先报告到namenode节点,然后DFSInputStream在其他的datanode上读该block的镜像
该设计的方向就是客户端直接连接datanode来检索数据并且namenode来负责为每一个block提供最优的datanode,namenode仅仅处理block location的请求,这些信息都加载在namenode的内存中,hdfs通过datanode集群可以承受大量客户端的并发访问。
二、HDFS的写流程
1.客户端通过调用DistributedFileSystem的create方法创建新文件,并向NameNode发送请求,namenode接收到请求后会做各种校验,比如文件是否存在,客户端有无权限去创建等。如果校验通过,namenode就会记录下新文件,否则就会抛出IO异常
2.NameNode在客户端中创建DFSOutputStream对象,DFSOutputStream可以协调namenode和datanode并会把数据切成一个个小packet,每个Packet大小为64K,packet会由多个chunks组成,每个chunk大小为512字节,并伴随一个4字节的校验码,然后将packet存放至dataQuene中
3.当dataQuene中存在数据时,会向NameNode请求上传第一个BLock块
4.NameNode接收到请求后,返回BLock块存储位置列表
5.客户端接收到列表后,根据机架感知(拓扑结构),以及负载均衡,寻找最优的一个DataNode节点进行连接
6.客户端会与最近的DataNode创建通道(pipeline),之后该DataNode会与其他的DataNode创建通道
7.开始上传第一个packet数据,dataQuene会将packet数据发送到本地的ackQueue
8.本地的ackQueue会将packet上传至DataNode的ackQueue
9.当第一个DataNode接收到packet,会将packet从ackQueue中分发至其他DataNode的ackQueue
10.当pipeline中的所有datanode都表示已经收到的时候,这时本地akc queue才会把对应的packet包移除掉。
11.当第一个BLock块中所有的packet数据上传完成,会重新向NameNode申请上传下一个BLock块所在的Locations位置,按之前的顺序再次提交,直到所有的BLock上传完成后通知namenode把文件标示为已完成
问题:如果上传过程中出现问题?
如果在写的过程中某个datanode发生错误,会采取以下几步:
1) pipeline被关闭掉;
2)为了防止丢包ack queue里的packet会同步到data queue里;
3)把产生错误的datanode上当前在写但未完成的block删掉;
4)block剩下的部分被写到剩下的两个正常的datanode中;
5)namenode找到另外的datanode去创建这个块的复制
MapReduce
MapReduce作用
将用户的业务逻辑和Hadoop中的组件结合起来形成一个分布式程序
JAVA类型与Hadoop类型对应
Java中的数据类型通过序列化以后,会产生大量的描述数据信息,在网络传输过程中,会占用大量资源,所以Hadoop实现了自己的一套数据结构,并且能够和Java中的数据类型进行对应
Hadoop text 对应 JAVA中的 String
Java类型 | Writable |
---|---|
字符串(String) | Text |
布尔型(boolean) | BooleanWritable |
字节型(byte) | ByteWritable |
整型(int) | IntWritable |
VIntWritable | IntWritable |
浮点型(float) | FloatWritable |
长整型(long) | LongWritable |
VLongWritable | LongWritable |
双精度浮点型(double) | DoubleWritable |
空值NULL | NullWritable |
MapReduce代码解析
1.Map端
① 输入数据为KV数据对,其中K为其数据读取的偏移量,Value为当前输入源的一行数据
②输出为KV数据对
③所有的业务逻辑都是在Map方法中实现的
④执行过程中每一行数据都会调用一次map方法
⑤一个切片数产生一个MapTask任务
2.Reduce端
①输入数据为Map端输出的数据
②所有的业务逻辑都在Reduce方法中实现
③如果ReduceTask数量为n个,那么对应输出文件也为n个
④Reduce中会将Map端输出的相同的Key放置一起,对应其Value是一个Iterable迭代器
3.Driver端
Driver的作用是将当前的程序打包提交给yarn集群(如果是在集群中运行),并通过Yarn中的ResourceManager创建当前程序的ApplicationMaster
具体代码步骤:
① 创建job,并设置Job中的配置,包括类,当前Job名称
②通过job设置Mapper和 Reducer(Reducer并不是必须的)
③设置Mapper输出的KV类以及最终输出的KV类
④设置输入输出的数据路径
⑤提交job
切片
切片源码
1、
public List<InputSplit> getSplits(JobContext job) throws IOException {
// 设置一个时间管理器,用于统计切片所花费的时间
StopWatch sw = new StopWatch().start();
// getFormatMinSplitSize=1L getMinSplitSize =1L => minSize = 1
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
// maxSize=9223372036854775807L
long maxSize = getMaxSplitSize(job);
// generate splits
// 创建List用来存储split
List<InputSplit> splits = new ArrayList<InputSplit>();
// 通过listStatus获取输入路径下所有的文件
List<FileStatus> files = listStatus(job);
for (FileStatus file: files) {
// 获取文件所在路径
Path path = file.getPath();
// 获取文件长度
long length = file.getLen();
if (length != 0) {
BlockLocation[] blkLocations;
// 判断file对象类型
if (file instanceof LocatedFileStatus) {
// 本地文件系统
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
// HDFS上的文件系统
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
// 判断文件是否可以切分,有的文件会是压缩格式
if (isSplitable(job, path)) {
// blockSize值为当前文件大小,如果当文件大小超过128M时,大小为128M
long blockSize = file.getBlockSize();
// 经过对比后 blockSize=splitSize
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
// 当前文件长度赋予 bytesRemaining表示剩余长度
long bytesRemaining = length;
// SPLIT_SLOP=1.1
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
// 当前文件大小超过切片大小的1.1倍,开始切分数据
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
// 创建切片 length-bytesRemaining表示开始位置 splitSize表示切片长度
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
// 将剩余长度减去所切去的切片长度
bytesRemaining -= splitSize;
}
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
// 如果剩余字节数小于 128M * 1.1 之后作为一个切片保存
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
} else {
// not splitable
// 如果不能切片,那么会创建一个切片用于处理数据
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else {
// 如果文件为空,那么也会创建一个切片
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// Save the number of input files for metrics/loadgen
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
return splits;
}
总结:
① 切片大小并不总是为128M
②一个文件小于128M也会产生一个切片
③ 一个切片对应一个MapTask任务
2、
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
for (FileStatus file: files) {
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
if (isSplitable(job, path)) {
long blockSize = file.getBlockSize(