Hadoop2.8.5 数据节点 DataNode

数据节点 DataNode 在 HDFS 文件系统中处于从属的地位, 但是其结构却比处于主导地位的查名节点 NameNode 更复杂。这是因为:虽然 NameNode 起着目录的作用,但是文件的内容却是存储在 DataNode 上的,读写文件时一旦知道了哪一个块在什么节点上,或者指定存放在什么节点上,下面就不需要 NameNode 的介入了。而块的存取,却是颇为复杂的操作。再说 NameNode 是靠“听汇报”来掌握什么 DataNode 上存储着哪一些块的,这一来倒好像DataNode 才是主动的, NameNode 反倒是被动的了。而且,新版 Hadoop 的设计还允许集群中有多个 NameNode ,形成“联邦( Federal )”,一个 DataNode 可以为多个 NameNode 存储数据块。

1. DataNode

数据节点 DataNode 在 HDFS 文件系统中处于从属的地位,但是其结构却比处于主导地位的查名节点 NameNode 更复杂。

public class DataNode extends ReconfigurableBase implements InterDatanodeProtocol, ClientDatanodeProtocol,
        TraceAdminProtocol, DataNodeMXBean {
   
  private BlockPoolManager blockPoolManager; // 同一文件卷的块属于相同 BlockPool
  volatile FsDatasetSpi<? extends FsVolumeSpi> data = null;//数据块的集合,实际上是个 FsVolumeImpl
  private DataStorage storage = null; //代表着数据块的存储空间
  private DatanodeHttpServer httpServer = null;
  DataXceiverServer xserver = null;
  private  BlockScanner blockScanner;//数据块扫描线程
  private DirectoryScanner directoryScanner = null;//目录扫描线程
  Daemon dataXceiverServer = null; //数据收发线程,通过局域网收发数据
  Daemon localDataXceiverServer; //通过 Unix 域 socket 在本节点上收发数据的线程
  ThreadGroup threadGroup = null;//一组 dataXceiverServer 线程
  public RPC.Server ipcServer; //提供 RPC 服务
  List<StorageLocation> dataDirs;//本地宿主文件系统中用于 HDFS 块存储的目录
  
}

BlockPoolManager中有两个映射表( Map )和一个 BPOfferService 的列表。一个 BPOfferService 相当于面向一个
具体 FSNamesystem 的联络站, BP 就是 BlockPool 的缩写,所以这是“由 BlockPool 提供的服务”的意思。两个映射表的存在是为迅速找到具体的 BPOfferService ,一个是按 NameserviceId 映射查找,另一个是按 BlockPoolId 映射查找。然后,进一步在每个 BPOfferService 内部又有一个 BPServiceActor 的列表,每个 BPServiceActor 相当于专责与某个 NameNode 联系的联络员。

class BlockPoolManager {
  //按 NameserviceId 查找
  private final Map<String, BPOfferService> bpByNameserviceId = Maps.newHashMap();
  //按 BlockPoolId 查找
  private final Map<String, BPOfferService> bpByBlockPoolId = Maps.newHashMap();
  //对于每个 Nameservice 都有个联络组
  private final List<BPOfferService> offerServices =new CopyOnWriteArrayList<>();
  private final DataNode dn;
  
}

核心的部件当然是这里的 FsDataset,代表着数据块的集合,实现了对数据块集合的管理。

class FsDatasetImpl implements FsDatasetSpi<FsVolumeImpl> {
	final DataStorage dataStorage; //代表着实际的存储
	private final FsVolumeList volumes; //数据块可能属于不同的文件卷
	final Map<String, DatanodeStorage> storageMap; //按 storageUuid 查找
	final FsDatasetAsyncDiskService asyncDiskService; /实现对磁盘的异步读写
	Daemon lazyWriter //一个 LazyWriter 线程,仅在真有必要时才写磁盘
	final FsDatasetCache cacheManager; //对于缓冲块的管理
	final ReplicaMap volumeMap; //可以根据 BlockPoolId 和块号找到其复份的 ReplicaInfo
}

数据块最终总是存储在某种存储介质上, DataStorage 就代表着这样的存储介质、存储空间。不过 HDFS 所看到的存储介质并非物理的、实体的存储介质,例如磁盘,而是宿主机的文件系统,是宿主机文件系统中的若干目录。这些目录可以在磁盘上,也可以在 RAMDisk 上,还可以在别的介质上。 DataStorage 类是对 Storage 类的扩充,我们在 NameNode 那一边看到的 NNStorage 也是对 Storage 类的扩充。所以, DataStorage 和 NNStorage 分别是 DateNode和 NameNode 上的 Storage ,二者都由宿主文件系统提供,只是使用方式不同。

一个 DataNode 上存储着一些什么块,并不是由 NameNode 告诉它的,而是反过来由DataNode 自己扫描,得知本节点上有些什么以后向 NameNode 报告的。而且这样的扫描和报告要周期地反复进行。 DataNode 上有两个线程,即 BlockScanner 和 DirectoryScanner ,就是专门从事这个工作的。

DataNode 上 的 节 点 间 通 信 也 比 NameNode 上 复 杂,因 为 DataNode 不 仅 要 面 对NameNode ,还有DataNode 互相之间的通信,特别是数据块的收发。 DataNode 之间的通信是对等通信,没 有 固 定 的 Client 和 Server ,每 个 DataNode 都 既 是 Client 又 是 Server ,所 以DataNode 上专门有一组 dataXceiverServer 线程。当然, DataNode 上也要有 RPC 服务端,即RPC. Server 。

DataNode(final Configuration conf, final List<StorageLocation> dataDirs, final SecureResources resources) {
	 try {
      hostName = getHostName(conf);
      LOG.info("Configured hostname is " + hostName);
      startDataNode(conf, dataDirs, resources);
    } catch (IOException ie) {
      shutdown();
      throw ie;
    }
}
void startDataNode(Configuration conf, List<StorageLocation> dataDirs,SecureResources resources) {

    // settings global for all BPs in the Data Node
    this.secureResources = resources;
    synchronized (this) {
      this.dataDirs = dataDirs;
    }
    this.conf = conf;
    this.dnConf = new DNConf(conf);
    //创建数据存储
    storage = new DataStorage();
    // global DN settings
    registerMXBean();
    //创建跨节点数据收发线程和本地数据收发线程
    initDataXceiver(conf);
    //创建节点间通信底层的服务端
    initIpcServer(conf);
    metrics = DataNodeMetrics.create(conf, getDisplayName());
    metrics.getJvmMetrics().setPauseMonitor(pauseMonitor);

    blockRecoveryWorker = new BlockRecoveryWorker(this);
    //创建 BlockPoolManager
    blockPoolManager = new BlockPoolManager(this);
    blockPoolManager.refreshNamenodes(conf);

    // Create the ReadaheadPool from the DataNode context so we can
    // exit without having to explicitly shutdown its thread pool.
    readaheadPool = ReadaheadPool.getInstance();
    saslClient = new SaslDataTransferClient(dnConf.conf, 
        dnConf.saslPropsResolver, dnConf.trustedChannelResolver);
    saslServer = new SaslDataTransferServer(dnConf, blockPoolTokenSecretManager);
    startMetricsLogger(conf);
  }
void initBlockPool(BPOfferService bpos) throws IOException {
    ......
    // In the case that this is the first block pool to connect, initialize
    // the dataset, block scanners, etc.
    initStorage(nsInfo);
    ......
 }
private void initStorage(final NamespaceInfo nsInfo) throws IOException {
    final FsDatasetSpi.Factory<? extends FsDatasetSpi<?>> factory
        = FsDatasetSpi.Factory.getFactory(conf);
    ......
    //创建数据块集合 FsDatasetImpl
    synchronized(this)  {
      if (data == null) {
        data = factory.newInstance(this, storage, conf);
      }
    }
  }
 public static DataNode createDataNode(String args[], Configuration conf,SecureResources resources){
    DataNode dn = instantiateDataNode(args, conf, resources);
    if (dn != null) {
      dn.runDatanodeDaemon();
    }
    return dn;
  }
  
 //启动各项服务 
 public void runDatanodeDaemon() throws IOException {
    blockPoolManager.startAll(); //启动 BlockPoolManager
    // start dataXceiveServer
    dataXceiverServer.start(); //启动数据收发线程
    if (localDataXceiverServer != null) {
      localDataXceiverServer.start(); //启动本地数据收发线程
    }
    ipcServer.setTracer(tracer); //启动 ipc 服务线程
    ipcServer.start();
    startPlugins(conf); //启动插件(如果有的话)
  }

2. 数据存储

DataStorage 代表着宿主系统上可以用来存储 HDFS 数据块的一个目录分支,HDFS 就以此作为一个文件卷,用一个 FsVolumeImpl 类对象为代表。

class FsVolumeImpl implements FsVolumeSpi {
  FsDatasetImpl dataset //所支撑的 FsDatasetImpl 对象
  String storageID // storageID 就是文件卷所在 Storage 的 ID
  StorageType storageType //可以是 DISK 、SSD 、 ARCHIVE 或 RAM _ DISK
  Map<String , BlockPoolSlice> bpSlices  //本文件卷上所存储的所有 BlockPoolSlice
  File currentDir ; // current 目录的路径
  DF usage //用 unix 的 df 命令获取关于文件卷的统计
  ThreadPoolExecutor cacheExecutor  //用于本文件卷的 CachingTask 线程池
}

FsVolumeImpl 代表着 HDFS 落实在宿主系统中的一个文件卷,这个文件卷的载体可以是磁盘,也可以是 SSD 卡,也可以是某种外储设备例如 U 盘,还可以是建立在内存中的RamDisk ,所以文件卷的 StorageType 可以是 DISK , SSD , ARCHIVE 或 RAM _ DISK。每个 FsVolumeImpl 有个线程池 cacheExecutor ,这些线程都是用来运行缓存任务的,目的在于从文件卷中装载数据块文件加以缓存,以提供内存中的缓冲存储,提高读出效率。

一个文件卷中可以存储着属于多个 BlockPoolSlice 的数据块, bpSlices 就是这些 BlockPoolSlice 的集合。但是这并不意味着一个BlockPoolSlice 的所有数据块都必须存储在同一个文件卷中。FsVolumeImpl 是从 HDFS 的视角
来代表和管理一个文件卷,而 DataStorage 则是从设备和介质的视角来代表和管理一个(虚拟的)存储设备,一个设备上可以有多个文件卷。

一个 DataNode 上可以有多个文件卷,所以还得有个全局性的对象来代表和管理本节点上所有数据块的集合。这就是 FsDatasetImpl 。FsDatasetImpl 代表着存储在一个 DataNode 上的所有数据块(复份)及其物理存储的集合,概念上与 DataStorage 比较接近,事实上 DataStorage 是 FsDatasetImpl 内部的一个成分。

class FsDatasetImpl implements FsDatasetSpi<FsVolumeImpl> {

  DataNode datanode //在哪一个 DataNode 上
  DataStorage dataStorage //一个 DataStorage ,代表着节点上所有的数据块
  FsVolumeList volumes // FsVolumeImpl 的 List ,节点上可以有多个文件卷
  Map<String , DatanodeStorage> storageMap  //从 StorageID 到 DatanodeStorage 的 MAP ,
  // DatanodeStorage 反映一个 DataNode 上 Storage 的状态信息
  ReplicaMap volumeMap //用于数据块复份的管理
  //根据 BlockpoolID 可以找到从 blockId 到具体 ReplicaInfo 的 MAP
  FsDatasetAsyncDiskService asyncDiskService //这是一个线程池,每个线程负责一个文件卷的异步磁盘操作
  FsDatasetCache cacheManager //管理缓冲在内存中的数据块复份
  DaemonlazyWriter // LazyWriter 线程,将 RamDisk 的内容存盘成为一个 checkpoint
  
}

首先,这些数据块可以存储在多个文件卷上,而 FsDatasetImpl 内部的 FsVolumeList 就是这些文件卷的集合,这样就把各个文 件 卷 的 FsVolumeImpl 给 整 合 了 进 来。然 后 是 storageMap ,通 过 这 个 MAP 凭StorageUuid 就可以快速找到相应的 DatanodeStorage ,这是 DataNode 向 NameNode 提交状态报告用的。还有 volumeMap ,这是个 ReplicaMap ,这是一个 MAP 的 MAP ,在此 MAP 中用BlockpoolID 可 以 找 到 从 blockId 到 具 体 ReplicaInfo 的 MAP 。也 就 是 说,先 用 bpid 在volumeMap 中找到一个 MAP ,然后在这个 MAP 中用 blockId 找到这个块的复份。

表着数据块的数据结构Block ,定义了两个常数分别用于数据块文件名的前缀和元数据( meta )文件的后缀,但是
类的结构成分中没有文件名,而只有块号 blockId ,因为文件名是可以按固定的规则根据块号生成的。这样,从数据(不包括操作)的角度看,块号、当前长度和世代标记(相当于“重大版本号”)这三个要素的组合就是对于 Block 的抽象,代表着一个 Block。

class Block implements Writable , Comparable<Block> {}
 static final String BLOCK _ FILE _ PREFIX="blk _ "
 static final String METADATA _ EXTENSION=".meta"
 long blockId //块号
 long numBytes //块的当前大小
 long generationStamp //世代标记,相当于版本号
 public String getBlockName (){
   return BLOCK _ FILE _ PREFIX+String.valueOf ( blockId )
}

DataNode 所以为的“块”,从 NameNode 看来只是这个块的一个复份,即 Replica。ReplicaInfo为抽象类,处于不同状态的 Replica 定义了不同的类。

abstract class ReplicaInfo extends Block implements Replica {}

class FinalizedReplica extends ReplicaInfo {}
class ReplicaUnderRecovery extends ReplicaInfo {}
class ReplicaWaitingToBeRecovered {}
class ReplicaInPipelineextendsReplicaInfo {}

3. 数据维护

在 HDFS 中并不是由 NameNode 根据持久存储的文件系统元数据确定一个块的几个复份分别存储在哪一些节点上,再让访问文件的客户前去读取; NameNode 根本就不持久存储数据块的位置信息,而是由 DataNode 通过心跳提交报告,说明当地有着一些什么数据块(bpid 和 blockId )的复份。 DataNode 上有个 DirectoryScanner 线程,过一会儿就来扫描清点一下。扫描的结果主要用于向 NameNode 提交报告,一方面也可用于本节点上存储系统的维护。

public class DirectoryScanner implements Runnable {
  FsDatasetSpi< ? > dataset //实际是个 FsDatasetImpl 对象
  ExecutorService reportCompileThreadPool //用于编制报告的线程池
  ScheduledExecutorService masterThread //主线程,实际上就是 DirectoryScanner
  // ScanInfoPerBlockPool 是对 HashMap<String , LinkedList<ScanInfo>> 的扩充
  //用来盛放当地数据块存储的变化
  ScanInfoPerBlockPool diffs=newScanInfoPerBlockPool ()
  //扫描,所得差异在 diffs 集合中
  private void scan() {
    clear();
    //汇总的结果,这是整个节点的 ScanInfoPerBlockPool
    Map<String, ScanInfo[]> diskReport = getDiskReport();

    // Hold FSDataset lock to prevent further changes to the block map
    try(AutoCloseableLock lock = dataset.acquireDatasetLock()) {
      //diskReport
      for (Entry<String, ScanInfo[]> entry : diskReport.entrySet()) {
        String bpid = entry.getKey();
        ScanInfo[] blockpoolReport = entry.getValue();
        Stats statsRecord = new Stats(bpid);
        stats.put(bpid, statsRecord);
        //为对应于具体 BlockPool 的 BP 目录创建一个空白的 diffRecord
        LinkedList<ScanInfo> diffRecord = new LinkedList<ScanInfo>();
        diffs.put(bpid, diffRecord);
        statsRecord.totalBlocks = blockpoolReport.length;
        //从文件卷的 volumeMap 中获取该 BP 目录下所有已 Finalized 的数据块文件清单
        final List<FinalizedReplica> bl = dataset.getFinalizedBlocks(bpid);
        Collections.sort(bl); // Sort based on blockId
        int d = 0; // index for blockpoolReport
        int m = 0; // index for memReprot
        while (m < bl.size() && d < blockpoolReport.length) {
        //获取 BlockPool 中有记载的数据块复份清单
          FinalizedReplica memBlock = bl.get(m);
          ScanInfo info = blockpoolReport[d];
          if (info.getBlockId() < memBlock.getBlockId()) {
            if (!dataset.isDeletingBlock(bpid, info.getBlockId())) {
              // Block is missing in memory
              statsRecord.missingMemoryBlocks++;
              addDifference(diffRecord, statsRecord, info);
            }
            d++;
            continue;
          }
          if (info.getBlockId() > memBlock.getBlockId()) {
            // Block is missing on the disk
            addDifference(diffRecord, statsRecord,
                          memBlock.getBlockId(), info.getVolume());
            m++;
            continue;
          }
          // Block file and/or metadata file exists on the disk
          // Block exists in memory
          if (info.getBlockFile() == null) {
            // Block metadata file exits and block file is missing
            addDifference(diffRecord, statsRecord, info);
          } else if (info.getGenStamp() != memBlock.getGenerationStamp()
              || info.getBlockFileLength() != memBlock.getNumBytes()) {
              // 世代标记不符,长度不符
            // Block metadata file is missing or has wrong generation stamp,
            // or block file length is different than expected
            statsRecord.mismatchBlocks++;
            addDifference(diffRecord, statsRecord, info);
          } else if (info.getBlockFile().compareTo(memBlock.getBlockFile()) != 0) {
            // volumeMap record and on-disk files don't match.
            statsRecord.duplicateBlocks++;
            addDifference(diffRecord, statsRecord, info);
          }
          d++;

          if (d < blockpoolReport.length) {
            // There may be multiple on-disk records for the same block, don't increment
            // the memory record pointer if so.
            ScanInfo nextInfo = blockpoolReport[Math.min(d, blockpoolReport.length - 1)];
            if (nextInfo.getBlockId() != info.blockId) {
              ++m;
            }
          } else {
            ++m;
          }
        }
        while (m < bl.size()) {
          FinalizedReplica current = bl.get(m++);
          addDifference(diffRecord, statsRecord,
                        current.getBlockId(), current.getVolume());
        }
        while (d < blockpoolReport.length) {
          if (!dataset.isDeletingBlock(bpid, blockpoolReport[d].getBlockId())) {
            statsRecord.missingMemoryBlocks++;
            addDifference(diffRecord, statsRecord, blockpoolReport[d]);
          }
          d++;
        }
        LOG.info(statsRecord.toString());
      } //end for
    } //end synchronized

}

DataNode 上对每个 FSNamesystem 即每个 NameNode 都有个 BlockPool ,实际上是 BlockPoolSlice ,里面记录着这个 NameNode 存放在本节点上的所有数据块复份,即数据块文件。所以,BlockPool 就像账本,而文件卷就像仓库,前者在内存中,后者在磁盘上或别的存储介质上。然而“账”与“实”可能不符,所以就要通过扫描来发现双方的差异,再用此差异来调整 BlockPool 的内容,这就好像一次仓库盘点。不过这个 scan ()函数并非只针对一个 BlockPool ,而是针对一个文件卷。

FsDatasetImpl.checkAndUpdate(......){
   //对记录于 BlockPool 集合中的一项具体的差异进行验证
   //向 DataNode 提交坏块报告,最终会报告给 NameNode
   datanode.reportBadBlocks ( newExtendedBlock ( bpid , corruptBlock ))
}

DirectoryScanner 的作用类似于清点仓库,搞清本地存储着一些什么数据块(复份),但并不确定所存储的数据块文件仍旧完好无损。所以 HDFS 中还有一个 BlockScanner ,它的作用是周期地轮流读一遍本地的数据文件,读的过程中加以 CRC 之类的校验,如果能顺利读出而不发生异常,就说明数据文件完好。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值