文章目录

  • HDFS读流程
  • 读流程的概述
  • 读流程大体步骤
  • 对逻辑图的解释
  • 读流程代码
  • 剖析OPEN方法
  • 客户端类关系图
  • NameNode数据存储逻辑图
  • **服务端方法关系图**
  • **对Block列表进行排序**
  • 剖析READ方法
  • 数据传输格式
  • 客户端方法关系图
  • 服务端方法关系图
  • 数据的检验
  • 对checkSum的检验
  • 对packet的检验
  • 读异常与恢复
  • Q&A


HDFS读流程

读流程的概述

hdfs数据分析 简述hdfs数据读流程_hdfs数据分析

读流程大体步骤

读流程就是client向HDFS读取数据的过程,分成三个部分,其大体步骤:

客户端通过调用FileSystem对象的open()方法来打开hdfs上的文件,这个方法在底层会调用ClientProtocol.open()方法,该方法会返回一个HdfsDataInputStream对象用于读取数据块。HdfsDataInputStream是一个DFSInputStream的装饰类,真正进行数据块读取操作的是DFSInputStream对象。
DistributedFileSystem通过调用RPC接口ClientProtocol.getBlockLocations()方法向名字节点NameNode获取该hdfs文件起始块的位置,同一Block按照重复数会返回多个位置,这些位置按照Hadoop集群拓扑结构排序,距离客户端近的排在前面;所以DFSInputStream会选择一个最优的DataNode节点,然后建立与这个节点的数据连接并读取数据块。
客户端通过DFSInputStream.read()方法从最优的DataNode节点上读取数据块,数据块会以数据包(packet)为单位从数据节点通过流式接口传递到客户端,当一个数据块读取完毕时,其会再次调用ClientProtocol.getBlockLocations()获取文件的下一个数据块位置信息,并建立和这个新的数据块的最优DataNode之间的连接,然后hdfs客户端就会继续读取该数据块了。
一旦客户端完成读取,就对HdfsDataInputStream调用close()方法关闭文件读取的输入流

对逻辑图的解释

DistributedFIleSystem:DistributedFIleSystem是FileSystem的子类,是HDFS的客户端API。

FSDataInputStream:是open操作,从NameNode所得到的输入流,里面包含文件所属的blcok信息与DataNode信息,用来访问DataNode读取数据。

读流程代码

FileSystem fs = FileSystem.get(new Configuration());
//通过HDFS的配置信息,Client连接NameNode,获取其文件系统
FSDataInputStream in = fs.open(path);
//path是你所读取文件的路径
in.read(byte[]);
//将读取的数据存放到byte数组中
in.close();

两个方法open和read。代表了客户端与NameNode和DataNode的交互

剖析OPEN方法

客户端的作用在于 向NameNode发送请求,获取文件所对应的block和DataNode。

客户端类关系图

hdfs数据分析 简述hdfs数据读流程_数据块_02

DistributedFilesystem是Filesystem的子类,DFSClient是DistributedFilesystem的子类,HdfsDataInputStream是DFSInputStream的装饰类。与服务器端的交互由HDFS的客户端(DFSClient)去完成。这是因为DFSClient全面负责和NameNode的通信。

在实际的DistributedFileSystem.open()过程中,其内部是委托给DFSClient类的实际对象dfs.open();其主要作用在于打开文件,并构造获取该文件对应的输入流DFSInputStream。在DFSInputStream的构造方法内部会初始化DFSInputStream的基本属性:包括 dfsClient类的引用,~~verifyChecksum读取数据时是否进行校验(这个主要适用于零拷贝),~~buffersize读取数据时缓冲区大小(4KB),src读取文件地址;
调用openInfo()方法:从NameNode处获取文件对应的数据块的位置信息,并将返回的数据块位置信息保存DFSInputStream.locatedBlocks字段中。

HdfsDataInputStream与DFSInputStream的关系

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

DistributedFileSystem.open:方法里重写了FileSystemLinkResolver的doCall和next方法,doCall方法在于调用DFSClient进行进行下一步的操作,

next方法在于当获取失败后重新执行FS.open方法,重新获取文件信息。

DFSClient.open:open里执行new DFSInputStream(this, src, verifyChecksum),生成DFSInputStream。

new DFSInputStream:open里执行new DFSInputStream(this, src, verifyChecksum),生成DFSInputStream。

DFSInputStream.openInfo:调用fetchLocatedBlocksAndGetLastBlockLength()方法获取block块的信息,得到信息后,如果最后一个block不完整,会wait,再次获取(安全模式)

DFSInputStream.fetchLocatedBlocksAndGetLastBlockLength():先调用dfsClient.getLocatedBlocks()方法通过rpc接口ClientProtocol.getBlockLocations()从NameNode获取文件对应的所有数据块的位置信息;

然后将新获取的数据块位置信息与locatedBlocks保存的位置信息进行对比,更新最新的locatedBlocks字段;

最后会调用readBlockLength()方法通过rpc接口ClientDatanodeProtocol去获取文件最后一个数据块的大小,然后更新locatedBlocks记录的最后一个数据块的长度;

二三步一般不执行

DFSClient.getBlockLocations:对文件的起始值和长度赋值,长度默认是10个block大小,可以通过DFSClient.prefetchSize,进行设置会调用NameNode的getBlockLocations来获取文件的block信息

当集群重启的时候(如果允许安全模式下读文件),或者当一个文件正在创建的时候,Datanode向Namenode进行Block Report,这个过程中可能Namenode还没有完全重建好Block到Datanode的映射关系信息,所以即使在这种情况下,仍然会返回对应的正在创建的Block所在的Datanode列表信息,可以从前面getBlockLocationsInternal方法中看到,INode的对应UnderConstruction状态为true。这时,一个Block对应的所有副本中的某些可能还在创建过程中。
上面方法中,调用updateBlockInfo来更新文件的Block元数据列表信息,对于文件的某些Block可能没有创建完成,所以Namenode所保存的关于文件的Block的的元数据信息可能没有及时更新(Datanode可能还没有完成Block的报告)。

安全模式:安全模式是HDFS所处的一种特殊状态,在这种状态下,文件系统只接受读数据请求,而不接受删除、修改等变更请求。在NameNode主节点启动时,HDFS首先进入安全模式,DataNode在启动的时候会向namenode汇报可用的block等状态,当整个系统达到安全标准时,HDFS自动离开安全模式。如果HDFS出于安全模式下,则文件block不能进行任何的副本复制操作,因此达到最小的副本数量要求是基于datanode启动时的状态来判定的,启动时不会再做任何复制(从而达到最小副本数量要求)

我们看一下,在updateBlockInfo方法中,返回false的情况:Client向Namenode发起的RPC请求,已经获取到了组成该文件的数据块的元数据信息列表,但是,文件的最后一个数据块的存储位置信息无法获取到,说明Datanode还没有及时通过block report将数据块的存储位置信息报告给Namenode。通过在openInfo()方法中可以看到,获取文件的block列表信息有3次重试机会,也就是调用updateBlockInfo方法返回false,可以有12秒的时间,等待Datanode向Namenode汇报文件的最后一个块的位置信息,以及Namenode更新内存中保存的文件对应的数据块列表元数据信息。
我们再看一下,在updateBlockInfo方法中,返回true的情况:

  • 文件已经创建完成,文件对应的block列表元数据信息可用
  • 文件正在创建中,但是当前能够读取到的已经完成的最后一个块(非组成文件的最后一个block)的元数据信息可用
  • 文件正在创建中,文件的最后一个block的元数据部分可读:从Namenode无法获取到该block对应的位置信息,这时Client会与Datanode直接进行RPC通信,获取到该文件最后一个block的位置信息

上面Client会与Datanode直接进行RPC通信,获取文件最后一个block的元数据,这时可能由于网络问题等等,无法得到文件最后一个block的元数据,所以也会返回true,也就是说,Client仍然可以读取该文件,只是无法读取到最后一个block的数据。
这样,在Client从Namenode/Datanode获取到的文件的Block列表元数据已经是可用的信息,可以根据这些信息读取到各个Block的物理数据块内容了,准确地说,应该是文件处于打开状态了,已经准备好后续进行的读操作了。

NameNode数据存储逻辑图

hdfs数据分析 简述hdfs数据读流程_数据_03

在HDFS中,文件也是以树形结构进行存储的,rootDir是根目录。INode模仿Linux文件系统中的索引节点inode,INode为一个抽象类,INode中保存了文件名,文件所有者,文件的访问权限,文件父目录(INodeDirectory引用,无论是INode的INodeFile子类还是INodeDirectory子类,都拥有父目录),修改时间,访问时间等。INodeDirectory和INodeFile是INode的子类,INodeDirectory中保存了Inode的列表,保存了目录下的每个子文件的INode。而INodeDirectory本身并不包含Block。INodeFile为INode的子类,表示一个具体文件。INodeFile中保存的blocks表示构成文件的一系列数据块,其中BlockInfo类继承与Block类,其中不仅包含了该block对应的INode,而且包含了该块所处的DataNode(DataNodeDescriptor)。

进行open方法的意义,在于获取文件相应的Block块和DataNode 信息。

讲一下NameNode的内存存储?

NameNode管理着整个HDFS文件系统的元数据。从架构设计上看,元数据大致分成两个层次:Namespace管理层,负责管理文件系统中的树状目录结构以及文件与数据块的映射关系;块管理层,负责管理文件系统中文件的物理块与实际存储位置的映射关系BlocksMap,如图1所示[1]。Namespace管理的元数据除内存常驻外,也会周期Flush到持久化设备上FsImage文件;BlocksMap元数据只在内存中存在;当NameNode发生重启,首先从持久化设备中读取FsImage构建Namespace,之后根据DataNode的汇报信息重新构造BlocksMap。这两部分数据结构是占据了NameNode大部分JVM Heap空间。

服务端方法关系图

根据文件名,找到相应的Block和DataNode

hdfs数据分析 简述hdfs数据读流程_big data_04

服务端首先根据文件名,利用广搜,在树形目录中找到自己对应的文件INodeFile,然后获取INodeFile下的block和dataNode,将block所属的datanode进行排序,然后封装Block和dataNode的信息,返回给Client。

Block 是以List的形式进行保存的。

NameNodeRpcServer.getBlockLocations:接收Client的请求

FSNamesystem.getBlockLocationsInt:根据你的文件目录,利用INode,INodeDirectory,INodeFile类,通过广搜的方法来获取其在NameNode中的目录,然后根据所获取的INodeFile,来获取相应的block信息

BlockManager.createLocatedBlocks:将LocatedBlockLis封装成Blocks

BlockManager.createLocatedBlockList:将LocatedBlock封装成list

BlockManager.createLocatedBlock:将blockId,所属的DataNode列表,字节初始位置,是否可用,封装成LocatedBlock

DataNodeManager.sortLocatedBlock:根据距离,heartbeat标志位对DataNode进行排序

对Block列表进行排序

我们再回到FSNamesystem类,调用getBlockLocationsInternal方法的getBlockLocations方法中,在返回文件block列表LocatedBlocks之后,会对每一个Block所在的Datanode进行的一个排序,排序的基本规则有如下2点:

  • Client到Block所在的Datanode的距离最近,这个是通过网络拓扑关系来进行计算,例如Client的网络路径为/dc1/r1/c1,那么路径为/dc1/r1/dn1的Datanode就比路径为/dc1/r2/dn2的距离小,/dc1/r1/dn1对应的Block就会排在前面
  • 从上面一点可以推出,如果Client就是某个Datanode,恰好某个Block的Datanode列表中包括该Datanode,则该Datanode对应的Block排在前面
  • Block所在的Datanode列表中,如果其中某个Datanode在指定的时间内没有向Namenode发送heartbeat(默认由常量DFSConfigKeys.DFS_NAMENODE_STALE_DATANODE_INTERVAL_DEFAULT定义,默认值为30s),则该Datanode的状态即为STALE,具有该状态的Datanode对应的Block排在后面

基于上述规则排序后,Block列表返回到Client。

剖析READ方法

read方法在于根据我们得到文件的Block

数据传输格式

hdfs数据分析 简述hdfs数据读流程_数据块_05

PacketLength大小为: 4 + CHECKSUMS(校验数据的大小)+ DATA(真实数据的大小)

BlockSender发送数据的格式包括两个部分: 校验信息头(ChecksumHeader) 和数据包序列(packets) 。

|校验信息头(ChecksumHeader) |数据包序列(packets) |

4.1.校验信息头(ChecksumHeader)
ChecksumHeader是一个校验信息头, 用于描述当前Datanode使用的校验方式等信息。

| 1 byte校验类型(CHECKSUM—TYPE) | 4 byte校验块大小(BYTES_PER_CHECKSUM)|

■ 数据校验类型: 数据校验类型定义在org.apache.hadoop.util.DataChecksum中, 目前包括三种方式——空校验(不进行校验) 、 CRC32以及CRC32C。 这里使用1byte描述数据校验类型, 空校验、 CRC32、 CRC32C、分别对应于值0、 1、 2、3、4。

另外两种类型 : CHECKSUM_DEFAULT、CHECKSUM_MIXED 对应于值3、4 不能用户创建 DataChecksum .

■ 校验块大小: 校验信息头中的第二个部分是校验块的大小, 也就是多少字节的数据产生一个校验值。 这里以CRC32为例, 一般情况下是512字节的数据产生一个4字节的校验和, 我们把这512字节的数据称为一个校验块(chunk) 。 这个校验块的概念非常重要, 它是HDFS中读取和写入数据块操作的最小单元.

4.2.数据包序列
BlockSender会将数据块切分成若干数据包(packet) 对外发送, 当数据发送完成后,会以一个空的数据包作为结束。

每个数据包都包括一个变长的包头、 校验数据以及若干字节的实际数据。

|变长的数据包头(packetHeader) | |校验数据 | |实际数据…… |

■ 数据包头——数据包头用于描述当前数据包的信息, 是通过ProtoBuf序列化的,包括4字节的全包长度, 以及2字节的包头长度, 之后紧跟如下数据包信息。

当前数据包在整个数据块中的位置。
数据包在管道中的序列号。
当前数据包是不是数据块中的最后一个数据包。
当前数据包中数据部分的长度。
是否需要DN同步。
■ 校验数据——校验数据是对实际数据做校验操作产生的, 它将实际数据以校验块为单位, 每个校验块产生一个检验和, 校验数据中包含了所有校验块的校验和。校验数据的大小为: (实际数据长度+校验块大小 - 1) / 校验块大小×校验和长度。
■ 实际数据——数据包中的实际数据就是数据块文件中保存的数据, 实际数据的传输是以校验块为单位的, 一个校验块对应产生一个校验和的实际数据。 在数据包中会将校验块与校验数据分开发送, 首先将所有校验块的校验数据发送出去, 然后再发送所有的校验块。

客户端方法关系图

hdfs数据分析 简述hdfs数据读流程_hdfs数据分析_06

currentNode = blockSeekTo(targetPos);其会获取保存当前所要读取block的最佳DataNode位置信息;blockSeekTo()方法首先会调用getBlockAt()方法,根据自己的偏移量,去获取当前游标所在的数据块信息block,然后调用chooseDataNode()方法获取一个最佳的DataNode节点,因为Data Node是已经排好序存放的,我们只需要按顺序取出能用的就可以了;之后便会构造读取该block数据块的blockReader对象用于数据流的读取;

deadNodes列表存放着不能访问的Data Node

在这个方法中,根据情况调用短路读(新旧版本),本地读,网络读,

向DataNode发送请求,包含输出流、OP读标志、请求信息,接收DataNode传来的ChecksumHeader,生成相应的BlockReader

read:在read中执行完readPacket方法后,使用 curDataSlice.get(buf, off, nRead);讲packet中数据传给buf数组,完成读操作

readNextPacket:接收DataNode传来的packet,获取packet的数据头,data和检验码,进行检验数据,如果数据数据全部读取完成,然后向DataNode发送状态码。

服务端方法关系图

hdfs数据分析 简述hdfs数据读流程_hadoop_07

DataXceiverServer是数据节点DataNode上一个用于接收数据读写请求的后台工作线程,为每个数据读写请求创建一个单独的线程去处理。

通过run()方法我们得知,当datanode正常运转的时候,DataXceiverServer线程主要负责在一个while循环中利用TcpPeerServer(也就是ServerSocket)的accept()方法阻塞,直到接收到客户端或者其他DataNode的连接请求,然后:

1、获得peer,即Socket的封装;

2、判断当前DataNode上DataXceiver线程数量是否超过阈值,如果超过的话,直接抛出IOException,利用IOUtils的cleanup()方法关闭peer后继续循环,否则继续3;

3、创建一个后台线程DataXceiver,并将其加入到datanode的线程组threadGroup中,并启动该线程,响应数据读写请求;(守护线程)

DataXceiver主要有peer,DataNode,DataXceiver.

peer封装了Client的请求信息

构建输入输出流,通过readOp()读取操作符,通过processOp()方法根据操作符op调用相应的方法处理操作符op。

81代表了READ_BLOCK的byte值。

readBlock()方法大体处理流程如下:

1、将请求中的客户端名称clientName赋值给previousOpClientName;

2、获取输出流baseStream,即socketOut;

3、将输出流baseStream依次包装成BufferedOutputStream、DataOutputStream,其缓冲区大小取参数io.file.buffer.size的一半,参数未配置的话默认为512,且最大也不能超过512;

4、调用checkAccess()方法进行访问权限检查;

5、发送数据块:

5.1、 获取数据节点注册信息DatanodeRegistration;

5.2、更新当前线程名称:Sending block…;

5.3、构造数据块发送器BlockSender对象blockSender,构造时,需要对应数据块block、数据在块中的起始位置blockOffset、读取数据的长度length等信息;

5.4、调用writeSuccessWithChecksumInfo()方法发送操作状态和Checksum;

5.5、调用数据块发送器blockSender的sendBlock()方法,发送数据块;

5.6、数据节点datanode记录相关系统性能指标的增长,这里是读取的字节数、读取的块数;

5.7、关闭数据块发送器。

**sendBlock()**整个发送的流程可以分为如下几步:

■ 在刚开始读取文件时, 触发一次预读取, 预读取部分数据到操作系统的缓冲区中。

■ 构造pktBuf缓冲区, 也就是能容纳一个数据包的缓冲区。 这里首先要确定的就是pktBuf缓冲区的大小, 最好是一个数据包的大小。

对于两种不同的发送数据包的模式transferTo和ioStream, 缓冲区的大小是不同的。 在transfertTo模式中 , 数据块文件是通过零拷贝方式直接传输给客户端的,并不需要将数据块文件写入缓冲区中, 所以pktBuf缓冲区只需要缓冲校验数据即可; 而ioStream模式则需要将实际数据以及校验数据都缓冲下来, 所以pktBuf大小是完全不同的。

■ 接下来就是循环调用sendPacket()方法发送数据包序列, 直到offset>=endOffset,也就是整个数据块都发送完成了。 这里首先调用manageOsCache()进行预读取,然后循环调用sendPacket()依次将所有数据包发送到客户端。 最后更新offset——也就是数据游标, 更新seqno——记录已经发送了几个数据包。

■ 发送一个空的数据包用以标识数据块的结束。

■ 完成数据块发送操作之后, 调用close()方法关闭数据块以及校验文件, 并从操作系统的缓存中删除已读取的数据。

一次发送的数据最少是一个packet大小,一次发送一个block

sendPacket

■ 首先计算数据包头域在pkt缓存中的位置headerOff, 再计算checksum在pkt中的位置checksumOff, 以及实际数据在pkt中的位置dataOff。 然后将数据包头域、 校验数据以及实际数据写入pkt缓存中。 如果verifyChecksum属性被设置为true, 则调用verifyChecksum()方法确认校验和数据正确。

■ 接下来就是发送数据块了, 将pkt缓存中的数据写入IO流中。 这里要注意, 如果是transferT()方式, pkt中只有数据包头域以及校验数据, 实际数据则直接通过transferTo方式从文件通道(FileChannel) 直接写入IO流中。

■ 使用节流器控制写入的速度

数据的检验

hdfs数据分析 简述hdfs数据读流程_数据块_08

DataXceiver.readBlock()首先向客户端回复一个BlockOpResponseProto响应, 指明请求已经成功接收, 并通过BlockOpResponseProto响应给出Datanode当前使用的校验方式。 接下来DataXceiver.readBlock()方法会将数据节点上的数据块(block) 切分成若干个数据包(packet) , 然后依次将数据包发送给客户端。 客户端会在收到每个数据包时进行校验,如果校验和错误, 客户端会切断与当前数据节点的连接, 选择新的数据节点读取数据; 如果数据块内的所有数据包都校验成功, 客户端会给数据节点发送一个Status.CHECKSUM_OK响应, 表明读取成功。

writeSuccessWithChecksumInfo

Hadoop会在客户端读取DataNode上的数据时,使用DFSClient中的read函数先将数据读入到用户的数据缓冲区,然后再检查校验和。将他们与datanode中存储的校验和进行比较
每个datanode均持久保存有一个用于验证的校验和日志,所以它知道每个数据块的最后一次验证时间
客户端成功验证一个数据块后,会告诉这个datanode,datanode由此更新日志

对checkSum的检验

long firstChunkOffset = checksumInfo.getChunkOffset();

  if ( firstChunkOffset < 0 || firstChunkOffset > startOffset ||
      firstChunkOffset <= (startOffset - checksum.getBytesPerChecksum())) {
    throw new IOException("BlockReader: error in first chunk offset (" +
                          firstChunkOffset + ") startOffset is " +
                          startOffset + " for file " + file);
  }

  return new RemoteBlockReader2(file, block.getBlockPoolId(), block.getBlockId(),
      checksum, verifyChecksum, startOffset, firstChunkOffset, len, peer,
      datanodeID, peerCache);
}
对checksum的检验
1 startOffset是你要读取数据在block中的偏移量
2 firstChunkOffse是所传递的第一个chunk在block中的偏移量
3 startOffset和firstOffset在一个chunk中

对packet的检验

private void readNextPacket() throws IOException {
    //Read packet headers.
    packetReceiver.receiveNextPacket(in);

    PacketHeader curHeader = packetReceiver.getHeader();
    curDataSlice = packetReceiver.getDataSlice();
    assert curDataSlice.capacity() == curHeader.getDataLen();
     //1 检验长度
    
    if (LOG.isTraceEnabled()) {
      LOG.trace("DFSClient readNextPacket got header " + curHeader);
    }

    // Sanity check the lengths
    if (!curHeader.sanityCheck(lastSeqNo)) {
         throw new IOException("BlockReader: error in packet header " +
                               curHeader);
    }
    //2 检验最后一个packet是null  非lastPacket是非null  当前packet的序号是上一个packet序号+1
    if (curHeader.getDataLen() > 0) {
      int chunks = 1 + (curHeader.getDataLen() - 1) / bytesPerChecksum;
      int checksumsLen = chunks * checksumSize;

      assert packetReceiver.getChecksumSlice().capacity() == checksumsLen :
        "checksum slice capacity=" + packetReceiver.getChecksumSlice().capacity() + 
          " checksumsLen=" + checksumsLen;
      //3 检验chunkSum的长度,用chunk得到的chunk数量算出的长度与得到的比较
      lastSeqNo = curHeader.getSeqno();
      if (verifyChecksum && curDataSlice.remaining() > 0) {
        // N.B.: the checksum error offset reported here is actually
        // relative to the start of the block, not the start of the file.
        // This is slightly misleading, but preserves the behavior from
        // the older BlockReader.
        checksum.verifyChunkedSums(curDataSlice,
            packetReceiver.getChecksumSlice(),
            filename, curHeader.getOffsetInBlock());
          //4 对每个chunk计算出它的chunksum,与传来的进行比较
      }
      bytesNeededToFinish -= curHeader.getDataLen();
    }    
    
    // First packet will include some data prior to the first byte
    // the user requested. Skip it.
    if (curHeader.getOffsetInBlock() < startOffset) {
      int newPos = (int) (startOffset - curHeader.getOffsetInBlock());
      curDataSlice.position(newPos);
    }

    // If we've now satisfied the whole client read, read one last packet
    // header, which should be empty
    if (bytesNeededToFinish <= 0) {
      readTrailingEmptyPacket();
      if (verifyChecksum) {
        sendReadResult(Status.CHECKSUM_OK);
      } else {
        sendReadResult(Status.SUCCESS);
      }
    }
     //如果接收完毕,接收最后一个空packet,向DataNode发送检验码
  }

五次检验

1 检验长度

2 检验序号

3 检验chunkSum的长度

4 检验chunkSum与chunk是否匹配

5

读异常与恢复

读文件可能发生的异常有两种:

  1. 读取过程中 DataNode 挂了
  2. 读取到的文件数据损坏

HDFS 的文件块多副本分散存储机制保障了数据存储的可靠性,对于第一种情况 DataNode 挂了只需要失败转移到其他副本所在的 DataNode 继续读取,而对于第二种情况读取到的文件数据块若校验失败可认定为损坏,依然可以转移到读取其他完好的副本,并向 NameNode 汇报该文件 block 损坏,后续处理由 NameNode 通知 DataNode 删除损坏文件 block,并根据完好的副本来复制一份新的文件 block 副本。

因为读文件不涉及数据的改变,所以处理起来相对简单,恢复机制的透明性和易用性都非常好

安全模式的处理:看block是否完整,不完整的话,等待一段时间重新获取。

客户端挂了的话,看是否已经保存磁盘中。

NameNode挂了,有两个NameNode保证高可用。

Q&A

Q:一次获取多少block

A:默认是10个默认blcok大小的数据。

Q:所读取的数据超过open函数所获取的block大小,怎么办

A:在read方法里的getBlockAt方法里,会获取当前数据所在的block位置,如果超过范围,会调用getBlocksLocation,重新从NameNode获取block列表

Q:read方法能读多少数据

A:最大一个packet,在read方法里只会执行一次readNextPacket,如果缓冲区大小小于packet,会暂存,下次read方法不能执行readNextPacket

Q:DataNode一次发送多少数据

A:一个Block,按packet进行发送