DataBlockScanner是运行在数据节点DataNode上的一个后台线程。它为所有的块池管理块扫描。针对每个块池,一个BlockPoolSliceScanner对象将会被创建,其运行在一个单独的线程中,为该块池扫描、校验数据块。当一个BPOfferService服务变成活跃或死亡状态,该类中的blockPoolScannerMap将会更新。

我们先看下DataBlockScanner的成员变量,如下:

  1. // 所属数据节点DataNode实例
  2. private final DataNode datanode;
  3. // 所属存储FsDatasetSpi实例
  4. private final FsDatasetSpi<? extends FsVolumeSpi> dataset;
  5. // 配置信息Configuration实例
  6. private final Configuration conf;
  7. // 线程休眠周期,5s
  8. static final int SLEEP_PERIOD_MS = 5 * 1000;
  9. /**
  10. * Map to find the BlockPoolScanner for a given block pool id. This is updated
  11. * when a BPOfferService becomes alive or dies.
  12. * 存储块池ID到对应BlockPoolScanner实例的映射。
  13. * 当一个BPOfferService服务变成活跃或死亡状态,blockPoolScannerMap将会随之更新。
  14. */
  15. private final TreeMap<String, BlockPoolSliceScanner> blockPoolScannerMap =
  16. new TreeMap<String, BlockPoolSliceScanner>();
  17. // 数据块扫描线程
  18. Thread blockScannerThread = null;

首先是由构造函数确定的三个成员变量:所属数据节点DataNode实例datanode、所属存储FsDatasetSpi实例dataset、配置信息Configuration实例conf,对应构造函数如下:

  1. // 构造函数
  2. DataBlockScanner(DataNode datanode,
  3. FsDatasetSpi<? extends FsVolumeSpi> dataset,
  4. Configuration conf) {
  5. this.datanode = datanode;
  6. this.dataset = dataset;
  7. this.conf = conf;
  8. }

然后设定了一个静态变量,5s的线程休眠周期,即SLEEP_PERIOD_MS,另外两个重要的成员变量是:

1、TreeMap<String, BlockPoolSliceScanner> blockPoolScannerMap

存储块池ID到对应BlockPoolScanner实例的映射。当一个BPOfferService服务变成活跃或死亡状态,blockPoolScannerMap将会随之更新。

2、Thread blockScannerThread

数据块扫描线程。

既然DataBlockScanner实现了Runnable接口,那么它肯定是作为一个线程在DataNode节点上运行的,我们看下DataNode是如何对其进行构造及启动的,代码如下:

  1. /**
  2. * See {@link DataBlockScanner}
  3. */
  4. private synchronized void initDataBlockScanner(Configuration conf) {
  5. // 如果blockScanner不为null,直接返回
  6. if (blockScanner != null) {
  7. return;
  8. }
  9. // 数据块校验功能无法开启的原因
  10. String reason = null;
  11. assert data != null;
  12. // 如果参数dfs.datanode.scan.period.hours未配置,或者配置为0,说明数据块校验功能已关闭
  13. if (conf.getInt(DFS_DATANODE_SCAN_PERIOD_HOURS_KEY,
  14. DFS_DATANODE_SCAN_PERIOD_HOURS_DEFAULT) < 0) {
  15. reason = "verification is turned off by configuration";
  16. // SimulatedFSDataset不支持数据块校验
  17. } else if ("SimulatedFSDataset".equals(data.getClass().getSimpleName())) {
  18. reason = "verifcation is not supported by SimulatedFSDataset";
  19. }
  20. // 如果数据块校验功能无法开启的原因为null,构造DataBlockScanner实例,并调用其start()方法启动该线程
  21. if (reason == null) {
  22. blockScanner = new DataBlockScanner(this, data, conf);
  23. blockScanner.start();
  24. } else {
  25. // 否则在日志文件中记录周期性数据块校验扫描无法启用的原因
  26. LOG.info("Periodic Block Verification scan disabled because " + reason);
  27. }
  28. }

首先,如果blockScanner不为null,直接返回,说明之前已经初始化并启动了,然后,确定数据块校验功能无法开启的原因reason:

1、如果参数dfs.datanode.scan.period.hours未配置,或者配置为0,说明数据块校验功能已关闭;

2、SimulatedFSDataset不支持数据块校验;

如果数据块校验功能无法开启的原因为null,构造DataBlockScanner实例,并调用其start()方法启动该线程,否则在日志文件中记录周期性数据块校验扫描无法启用的原因。

DataBlockScanner线程启动的start()方法如下:

  1. public void start() {
  2. / 基于DataBlockScanner实例创建一个线程blockScannerThread
  3. blockScannerThread = new Thread(this);
  4. // 将线程blockScannerThread设置为后台线程
  5. blockScannerThread.setDaemon(true);
  6. // 启动线程blockScannerThread
  7. blockScannerThread.start();
  8. }

实际上它是基于DataBlockScanner实例创建一个线程blockScannerThread,将线程blockScannerThread设置为后台线程,然后启动线程blockScannerThread。

DataBlockScanner线程已创建,并启动,那么我们看下它是如何工作的,接下来看下它的run()方法,代码如下:

  1. // 线程核心run()方法
  2. @Override
  3. public void run() {
  4. // 当前块池ID,默认为空
  5. String currentBpId = "";
  6. // 第一次运行标志,默认当然应该为true
  7. boolean firstRun = true;
  8. // 如果所属数据节点DataNode实例datanode正常运行,且当前线程没有被中断
  9. while (datanode.shouldRun && !Thread.interrupted()) {
  10. //Sleep everytime except in the first iteration.
  11. // 如果不是第一次运行,线程休眠5s
  12. if (!firstRun) {
  13. try {
  14. Thread.sleep(SLEEP_PERIOD_MS);
  15. } catch (InterruptedException ex) {
  16. // Interrupt itself again to set the interrupt status
  17. // 如果发生InterruptedException异常,中断blockScannerThread线程,然后跳过,继续下一轮循环
  18. blockScannerThread.interrupt();
  19. continue;
  20. }
  21. } else {
  22. // 第一次运行时先将firstRun标志设置为false
  23. firstRun = false;
  24. }
  25. // 获取下一个块池切片扫描器BlockPoolSliceScanner实例bpScanner
  26. BlockPoolSliceScanner bpScanner = getNextBPScanner(currentBpId);
  27. // 如果bpScanner为null,跳过,继续下一轮循环
  28. if (bpScanner == null) {
  29. // Possible if thread is interrupted
  30. continue;
  31. }
  32. // 设置当前块池ID,即currentBpId,从块池切片扫描器BlockPoolSliceScanner实例bpScanner中获取
  33. currentBpId = bpScanner.getBlockPoolId();
  34. // If BPOfferService for this pool is not alive, don't process it
  35. // 如果当前块池对应的心跳服务BPOfferService不是活跃的,不对它进行处理,调用removeBlockPool()方法从blockPoolScannerMap中移除数据,
  36. // 并关闭对应BlockPoolSliceScanner,然后跳过,执行下一轮循环
  37. if (!datanode.isBPServiceAlive(currentBpId)) {
  38. LOG.warn("Block Pool " + currentBpId + " is not alive");
  39. // Remove in case BP service died abruptly without proper shutdown
  40. removeBlockPool(currentBpId);
  41. continue;
  42. }
  43. // 调用块池切片扫描器BlockPoolSliceScanner实例bpScanner的scanBlockPoolSlice()方法,
  44. // 扫描对应块池里的数据块,进行数据块校验
  45. bpScanner.scanBlockPoolSlice();
  46. }
  47. // Call shutdown for each allocated BlockPoolSliceScanner.
  48. // 退出循环后,遍历blockPoolScannerMap中的每个BlockPoolSliceScanner实例bpss,
  49. // 挨个调用对应shutdown()方法,停止块池切片扫描器BlockPoolSliceScanner
  50. for (BlockPoolSliceScanner bpss: blockPoolScannerMap.values()) {
  51. bpss.shutdown();
  52. }
  53. }

run()方法逻辑比较清晰,大体如下:

1、首先初始化当前块池ID,即currentBpId,默认为空,再确定第一次运行标志firstRun,默认当然应该为true;

2、接下来进入一个while循环,循环的条件是如果所属数据节点DataNode实例datanode正常运行,且当前线程没有被中断:

2.1、处理第一次运行标志位firstRun:

2.1.1、如果不是第一次运行,线程休眠5s:即firstRun为false,这时如果发生InterruptedException异常,中断blockScannerThread线程,然后跳过,继续下一轮循环;

2.1.2、第一次运行时先将firstRun标志设置为false;

2.2、获取下一个块池切片扫描器BlockPoolSliceScanner实例bpScanner,通过调用getNextBPScanner()方法,传入当前块池ID,即currentBpId来实现,首次循环,currentBpId为空,后续会传入之前处理的值,下面会对其进行更新;

2.3、如果bpScanner为null,跳过,继续下一轮循环;

2.4、设置当前块池ID,即currentBpId,从块池切片扫描器BlockPoolSliceScanner实例bpScanner中获取;

2.5、如果当前块池对应的心跳服务BPOfferService不是活跃的,不对它进行处理,调用removeBlockPool()方法从blockPoolScannerMap中移除数据,并关闭对应BlockPoolSliceScanner,然后跳过,执行下一轮循环;

2.6、调用块池切片扫描器BlockPoolSliceScanner实例bpScanner的scanBlockPoolSlice()方法,扫描对应块池里的数据块,进行数据块校验;

3、退出循环后,遍历blockPoolScannerMap中的每个BlockPoolSliceScanner实例bpss,挨个调用对应shutdown()方法,停止块池切片扫描器BlockPoolSliceScanner。

我们接下来看下比较重要的getNextBPScanner()方法,代码如下:

  1. /**
  2. * Find next block pool id to scan. There should be only one current
  3. * verification log file. Find which block pool contains the current
  4. * verification log file and that is used as the starting block pool id. If no
  5. * current files are found start with first block-pool in the blockPoolSet.
  6. * However, if more than one current files are found, the one with latest
  7. * modification time is used to find the next block pool id.
  8. * 寻找下一个块池ID以进行scan。
  9. * 此时应该只有一个当前验证日志文件。
  10. */
  11. private BlockPoolSliceScanner getNextBPScanner(String currentBpId) {
  12. String nextBpId = null;
  13. // 如果所属数据节点DataNode实例datanode正常运行,且当前blockScannerThread线程没有被中断
  14. while (datanode.shouldRun && !blockScannerThread.isInterrupted()) {
  15. // 等待初始化
  16. waitForInit();
  17. synchronized (this) {
  18. // 当blockPoolScannerMap大小大于0,即存在BlockPoolSliceScanner实例时,做以下处理:
  19. if (getBlockPoolSetSize() > 0) {
  20. // Find nextBpId by the minimum of the last scan time
  21. // lastScanTime用于记录上次浏览时间
  22. long lastScanTime = 0;
  23. // 遍历blockPoolScannerMap集合,取出每个块池ID,即bpid
  24. for (String bpid : blockPoolScannerMap.keySet()) {
  25. // 根据块池ID,即bpid,取出其对应BlockPoolSliceScanner实例的上次浏览时间t
  26. final long t = getBPScanner(bpid).getLastScanTime();
  27. // 如果t不为0,且如果块池ID为null,或者t小于lastScanTime,则将t赋值给lastScanTime,bpid赋值给nextBpId
  28. // 也就是计算最早的上次浏览时间lastScanTime,和对应块池ID,即nextBpId
  29. if (t != 0L) {
  30. if (bpid == null || t < lastScanTime) {
  31. lastScanTime =  t;
  32. nextBpId = bpid;
  33. }
  34. }
  35. }
  36. // nextBpId can still be null if no current log is found,
  37. // find nextBpId sequentially.
  38. // 如果对应块池ID,即nextBpId为null,则取比上次处理的块池currentBpId高的key作为nextBpId,
  39. // 如果还不能取出的话,那么取第一个块池ID,作为nextBpId
  40. if (nextBpId == null) {
  41. nextBpId = blockPoolScannerMap.higherKey(currentBpId);
  42. if (nextBpId == null) {
  43. nextBpId = blockPoolScannerMap.firstKey();
  44. }
  45. }
  46. // 如果nextBpId不为空,那么从blockPoolScannerMap中获取其对应BlockPoolSliceScanner实例返回
  47. if (nextBpId != null) {
  48. return getBPScanner(nextBpId);
  49. }
  50. }
  51. }
  52. // 记录warn日志,No block pool is up, going to wait,然后等待
  53. LOG.warn("No block pool is up, going to wait");
  54. try {
  55. // 线程休眠5s
  56. Thread.sleep(5000);
  57. } catch (InterruptedException ex) {
  58. LOG.warn("Received exception: " + ex);
  59. blockScannerThread.interrupt();
  60. return null;
  61. }
  62. }
  63. return null;
  64. }

它的主要作用就是寻找下一个块池ID以进行scan,其存在一个整体的while循环,循环的条件为如果所属数据节点DataNode实例datanode正常运行,且当前blockScannerThread线程没有被中断,循环内做以下处理:

1、调用waitForInit()方法等待初始化;

2、当前对象上使用synchronized进行同步,当blockPoolScannerMap大小大于0,即存在BlockPoolSliceScanner实例时,做以下处理:

2.1、设定lastScanTime用于记录上次浏览时间,默认值为0;

2.2、遍历blockPoolScannerMap集合,取出每个块池ID,即bpid,计算最早的上次浏览时间lastScanTime,和对应块池ID,即nextBpId:

2.2.1、根据块池ID,即bpid,取出其对应BlockPoolSliceScanner实例的上次浏览时间t;

2.2.2、如果t不为0,且如果块池ID为null,或者t小于lastScanTime,则将t赋值给lastScanTime,bpid赋值给nextBpId,也就是计算最早的上次浏览时间lastScanTime,和对应块池ID,即nextBpId;

2.3、如果对应块池ID,即nextBpId为null,则取比上次处理的块池currentBpId高的key作为nextBpId,如果还不能取出的话,那么取第一个块池ID,作为nextBpId;

2.4、如果nextBpId不为空,那么从blockPoolScannerMap中获取其对应BlockPoolSliceScanner实例返回;

3、如果blockPoolScannerMap大小等于0,或者上述2找不到的话,记录warn日志,No block pool is up, going to wait,然后等待5s后继续下一轮循环;

最后,实在找不到就返回null。

可见,getNextBPScanner()方法优先选取最早处理过的块池,找不到的话再按照之前处理过的块池ID增长的顺序,找下一个块池ID,按照块池ID大小顺序到尾部的话,再折回取第一个。

其中等待初始化的waitForInit()方法比较简单,代码如下:

  1. // Wait for at least one block pool to be up
  2. private void waitForInit() {
  3. // 如果BlockPoolSliceScanner的个数小于数据节点所有BpOS个数,或者BlockPoolSliceScanner的个数小于1,一直等待
  4. // BpOS你可以理解为DataNode上每个块池或命名空间对应的一个实例,它处理该命名空间到对应活跃或备份状态NameNode的心跳。
  5. while ((getBlockPoolSetSize() < datanode.getAllBpOs().length)
  6. || (getBlockPoolSetSize() < 1)) {
  7. try {
  8. // 线程休眠5s
  9. Thread.sleep(SLEEP_PERIOD_MS);
  10. } catch (InterruptedException e) {
  11. // 如果发生InterruptedException异常,中断blockScannerThread线程,然后返回
  12. blockScannerThread.interrupt();
  13. return;
  14. }
  15. }
  16. }

它本质上是等所有块池都被上报至blockPoolScannerMap集合后,才认为已完成初始化,然后再挑选块池ID,否则线程休眠5s,继续等待。代码注释比较详细,这里不再赘述!

获取到块池ID,并获取到其对应的块池切片扫描器BlockPoolSliceScanner实例bpScanner了,接下来就是调用bpScanner的scanBlockPoolSlice()方法,扫描该块池的数据块,并做数据块校验工作了。这方面的内容,请阅读《HDFS源码分析数据块校验之BlockPoolSliceScanner》一文,这里不再做介绍。

到了这里,各位看官可能有个疑问,选取块池所依赖的blockPoolScannerMap集合中的数据是哪里来的呢?答案就在处理数据节点心跳的BPServiceActor线程中,在完成数据块汇报、处理来自名字节点NameNode的相关命令等操作后,有如下代码被执行:

  1. // Now safe to start scanning the block pool.
  2. // If it has already been started, this is a no-op.
  3. // 现在可以安全地扫描块池,如果它已经启动,这是一个空操作。
  4. if (dn.blockScanner != null) {
  5. dn.blockScanner.addBlockPool(bpos.getBlockPoolId());
  6. }

很简单,数据节点汇报数据块给名字节点,并执行来自名字节点的相关命令后,就可以通过数据节点DataNode中成员变量blockScanner的addBlockPool()方法,添加块池,代码如下:

  1. public synchronized void addBlockPool(String blockPoolId) {
  2. // 如果blockPoolScannerMap集合中存在块池blockPoolId,直接返回
  3. if (blockPoolScannerMap.get(blockPoolId) != null) {
  4. return;
  5. }
  6. // 根据块池blockPoolId、数据节点datanode、存储dataset、配置信息conf等构造BlockPoolSliceScanner实例bpScanner
  7. BlockPoolSliceScanner bpScanner = new BlockPoolSliceScanner(blockPoolId,
  8. datanode, dataset, conf);
  9. // 将块池blockPoolId与bpScanner的映射关系存储到blockPoolScannerMap中
  10. blockPoolScannerMap.put(blockPoolId, bpScanner);
  11. // 记录日志信息
  12. LOG.info("Added bpid=" + blockPoolId + " to blockPoolScannerMap, new size="
  13. + blockPoolScannerMap.size());
  14. }

逻辑很简单,首先需要看看blockPoolScannerMap集合中是否存在块池blockPoolId,存在即返回,否则根据块池blockPoolId、数据节点datanode、存储dataset、配置信息conf等构造BlockPoolSliceScanner实例bpScanner,将块池blockPoolId与bpScanner的映射关系存储到blockPoolScannerMap中,最后记录日志信息。

我们在上面也提到了如果当前块池对应的心跳服务BPOfferService不是活跃的,那么会调用removeBlockPool()方法,移除对应的块池,代码如下:

  1. public synchronized void removeBlockPool(String blockPoolId) {
  2. / 根据块池blockPoolId,从blockPoolScannerMap中移除数据,并得到对应BlockPoolSliceScanner实例bpss
  3. BlockPoolSliceScanner bpss = blockPoolScannerMap.remove(blockPoolId);
  4. // 调用bpss的shutdown()方法,关闭bpss
  5. if (bpss != null) {
  6. bpss.shutdown();
  7. }
  8. // 记录日志信息
  9. LOG.info("Removed bpid="+blockPoolId+" from blockPoolScannerMap");
  10. }

代码很简单,不再赘述。

总结

DataBlockScanner是运行在数据节点DataNode上的一个后台线程,它负责管理所有块池的数据块扫描工作。当数据节点DataNode发送心跳给名字节点NameNode进行数据块汇报并执行完返回的命令时,会在DataBlockScanner的内部集合blockPoolScannerMap中注册块池ID与为此新创建的BlockPoolSliceScanner对象的关系,然后DataBlockScanner内部线程blockScannerThread周期性的挑选块池currentBpId,并获取块池切片扫描器BlockPoolSliceScanner实例bpScanner,继而调用其scanBlockPoolSlice()方法,扫描对应块池里的数据块,进行数据块校验。块池选择的主要依据就是优先选择扫描时间最早的,也就是自上次扫描以来最长时间没有进行扫描的,按照这一依据选择不成功的话,则默认按照块池ID递增的顺序循环选取块池。

HDFS源码分析数据块校验之DataBlockScanner的更多相关文章

  1. HDFS源码分析数据块复制监控线程ReplicationMonitor(二)

    HDFS源码分析数据块复制监控线程ReplicationMonitor(二)

  2. HDFS源码分析数据块复制监控线程ReplicationMonitor(一)

    ReplicationMonitor是HDFS中关于数据块复制的监控线程,它的主要作用就是计算DataNode工作,并将复制请求超时的块重新加入到待调度队列.其定义及作为线程核心的run()方法如下: ...

  3. HDFS源码分析数据块汇报之损坏数据块检测checkReplicaCorrupt()

    无论是第一次,还是之后的每次数据块汇报,名字名字节点都会对汇报上来的数据块进行检测,看看其是否为损坏的数据块.那么,损坏数据块是如何被检测的呢?本文,我们将研究下损坏数据块检测的checkReplic ...

  4. HDFS源码分析数据块复制选取复制源节点

    数据块的复制当然需要一个源数据节点,从其上拷贝数据块至目标数据节点.那么数据块复制是如何选取复制源节点的呢?本文我们将针对这一问题进行研究. 在BlockManager中,chooseSourceDa ...

  5. HDFS源码分析数据块之CorruptReplicasMap

    CorruptReplicasMap用于存储文件系统中所有损坏数据块的信息.仅当它的所有副本损坏时一个数据块才被认定为损坏.当汇报数据块的副本时,我们隐藏所有损坏副本.一旦一个数据块被发现完好副本达到 ...

  6. HDFS源码分析数据块复制之PendingReplicationBlocks

    PendingReplicationBlocks实现了所有正在复制的数据块的记账工作.它实现以下三个主要功能: 1.记录此时正在复制的块: 2.一种对复制请求进行跟踪的粗粒度计时器: 3.一个定期识别 ...

  7. HDFS源码分析之数据块及副本状态BlockUCState、ReplicaState

    关于数据块.副本的介绍,请参考文章<HDFS源码分析之数据块Block.副本Replica>. 一.数据块状态BlockUCState 数据块状态用枚举类BlockUCState来表示,代 ...

  8. HDFS源码分析心跳汇报之数据块汇报

    在<HDFS源码分析心跳汇报之数据块增量汇报>一文中,我们详细介绍了数据块增量汇报的内容,了解到它是时间间隔更长的正常数据块汇报周期内一个smaller的数据块汇报,它负责将DataNod ...

  9. HDFS源码分析心跳汇报之数据块增量汇报

    在<HDFS源码分析心跳汇报之BPServiceActor工作线程运行流程>一文中,我们详细了解了数据节点DataNode周期性发送心跳给名字节点NameNode的BPServiceAct ...

随机推荐

  1. ELK故障:elk在运行一段时间后,没有数据。

    故障排查: 1. 查看kafka.logstash.elasticsearch进程是否运行正常,显示正常. 2. 使用logstash在前台运行,有日志输出 3. 查看kafka的topic的offs ...

  2. POJ 3686 The Windy's (费用流)

    [题目链接] http://poj.org/problem?id=3686 [题目大意] 每个工厂对于每种玩具的加工时间都是不同的, 并且在加工完一种玩具之后才能加工另一种,现在求加工完每种玩具的平均 ...

  3. 利用.net4.0的dynamic特性制造的超级简单的微信SDK

    1.基础支持API /*-------------------------------------------------------------------------- * BasicAPI.cs ...

  4. python 实现创建文件夹和创建日志文件

    一.实现创建文件夹和日志 #!/usr/bin/env python # -*- coding:utf-8 -*- # Author: nulige import os import datetime ...

  5. 报错kernel:NMI watchdog: BUG: soft lockup - CPU#0 stuck for 26s

    近期在服务器跑大量高负载程序,造成cpu soft lockup.如果确认不是软件的问题. 解决办法: #追加到配置文件中 echo 30 > /proc/sys/kernel/watchdog ...

  6. ES6里关于函数的拓展(三)

    一.箭头函数 在ES6中,箭头函数是其中最有趣的新增特性.顾名思义,箭头函数是一种使用箭头(=>)定义函数的新语法,但是它与传统的JS函数有些许不同,主要集中在以下方面: 1.没有this.su ...

  7. 使用老版本的java api提交hadoop作业

    还是使用之前的单词计数的例子 自定义Mapper类 import java.io.IOException; import org.apache.hadoop.io.LongWritable; impo ...

  8. 【转】Understanding Inversion of Control, Dependency Injection and Service Locator Print

    原文:https://www.dotnettricks.com/learn/dependencyinjection/understanding-inversion-of-control-depende ...

  9. Hadoop之——分布式集群安装过程简化版

    转载请注明出处:http://blog.csdn.net/l1028386804/article/details/46352315 1.hadoop的分布式安装过程 1.1 分布结构 主节点(1个,是 ...

  10. postman里面的mockserver使用方法

    转载:http://blog.csdn.net/Cloud_Huan/article/details/78326159 首先说下mockserver是干啥的,从英文翻译理解就是模拟一个服务器,通俗点说 ...