HDFS数据快速拷贝方案:FastCopy

123 篇文章 35 订阅

前言


我们在使用HDFS的时候,往往有的时候需要做一些临时的数据拷贝操作,如果是在同一个集群中的,我们直接用HDFS内部自带的cp这些命令即可;如果是跨集群的时候或者说待拷贝数据量规模非常大的时候,我们还可以使用DistCp工具。但是这是否意味着我们使用这些工具在拷贝数据的时候依然是高效的呢?答案其实并不是这样的。在许多比较早使用Hadoop的公司,估计都或多或少遇到过拷贝大规模数据效率不高的情况。比如说,FaceBook在其内部的Hadoop版本中开发了一种叫做FastCopy的数据快速拷贝工具。目前在JIRA上也有相应的记录:HDFS-2139(Fast copy for HDFS)。本节我们要讲述的主题正是FastCopy工具。

FastCopy的原理介绍


FastCopy与传统的数据拷贝之间的一个主要不同点在于它尽可能地让数据拷贝发生在本地,以此减少跨节点间的数据传输。并且在FastCopy的本地数据拷贝过程中,还能通过对文件创建一个新的硬链接的方式,而无须做真正的数据拷贝操作。HDFS硬链接的相关内容可以阅读本人之前的一篇文章:HDFS符号链接和硬链接。HDFS内部的硬链接在FaceBook内部也已经早已实现。

下面我们来学习FastCopy快速拷贝工具的一个主要原理:

1)查询待拷贝文件的所有block块信息。
2)获取这些源文件块信息所在的位置信息。
3)对于源文件的每个块,在NameNode内部对应创建一个空的目标块,这些目标块的存储位置尽可能与源块最后一致。
4)然后命令DataNode做一个本地的块拷贝操作。
5)然后等待块拷贝完成操作,然后上报到NameNode上。

对于上述过程中的第4步,可以直接利用硬链接来做。OK,这里的过程是一个FastCopy工具拷贝数据内部的过程,那么我们从大一点的角度来观察,FastCopy工具的总流程是怎样的呢,答案如下:

1.首先输入待拷贝的目标路径,这里可以为纯文件或目录。
2.第一步骤中输入的路径会转化为一个个FastCopy的请求。
3.这些请求会提交到一个线程池中去执行。
4.根据拷贝过程中的源块,目标块的所在节点位置,分别执行普通方式的DataCopy或是本地方式的LocalCopy两种拷贝操作。

此过程原理图如图1-1所示。




图 1-1 FastCopy数据拷贝过程

FastCopy原理部分的内容主要在于上面2点。上面的步骤细节大家在后面的关键代码分析中可以进行对应地查找。

FastCopy核心代码分析


在核心代码分析部分,我们将主要关注2个模块的实现:

  • FastCopy工具如何尽可能地让块在本地进行复制。
  • FastCopy在数据拷贝时具体是如何执行的。

首先是第一个点的内容:在FastCopy中,是如何保证块尽量在本地复制呢?要想解答这个问题,我们先回到之前FastCopy的快速拷贝原理,其中有这么一个过程:

对于源文件的每个块,在NameNode内部对应创建一个空的目标块,这些目标块的存储位置尽可能与源块最后一致。

在这步过程的执行时,会将查询到的源块的位置信息优先作为目标创建块的位置信息。这样就保证了目标拷贝块和源快是在同一个节点上了。这里为什么指明说是优先呢,而不是绝对肯定的说法呢?因为这里还要考虑到目标盘上的存储空间够不够用的问题,如果目标存储的盘的可用空间不足,那么这个位置将不可用。然后NameNode将会选择下一个存储位置。

此部分代码如下:

    /**
     * Copy the file.
     * @return result of the operation
     */
    private CopyResult copy() throws Exception {
      // 获取源文件信息,并准备创建空目标文件
      HdfsFileStatus srcFileStatus = srcNamenode.getFileInfo(src);
      if (srcFileStatus == null) {
        throw new FileNotFoundException("File : " + src + " does not exist");
      }
      LOG.info("Start to copy " + src + " to " + destination);
      try {
        ...
        LinkedList<LocatedBlock> blocksList = new LinkedList<LocatedBlock>();
         LocatedBlock previousAdded = null;
         do {
           lastStart = lastEnd;
           // 获取源文件的块对象
           LocatedBlocks blocks = srcNamenode.getBlockLocations(src, lastStart, addition);
           ...
           lastEnd = lastBlock.getStartOffset() + lastSize;
           // 遍历此文件的块对象列表
           for (LocatedBlock lb : blocks.getLocatedBlocks()) {
             if (previousAdded == null 
                 || !previousAdded.getBlock().equals(lb.getBlock())) {
               // 将块对象加入到块链表最后
               blocksList.add(lb);
               previousAdded = lb;
             }
           }
         } while (lastEnd < fileLen);

     ...

         EnumSetWritable<CreateFlag> flagWritable = new EnumSetWritable<CreateFlag>(flag);

         // 在NameNode内部创建目标文件
         HdfsFileStatus dstFileStatus = dstNamenode.create(destination, srcFileStatus.getPermission(),
             clientName, flagWritable, true,
             srcFileStatus.getReplication(), srcFileStatus.getBlockSize(),
             CryptoProtocolVersion.supported());

         // Instruct each datanode to create a copy of the respective block.
         int blocksAdded = 0;
         ExtendedBlock previous = null;
         LocatedBlock destinationLocatedBlock = null;
         // Loop through each block and create copies.
         // 遍历之前源文件块列表
         for (LocatedBlock srcLocatedBlock : blocksList) { 
           UserGroupInformation.getCurrentUser().addToken(srcLocatedBlock.getBlockToken());
           String[] favoredNodes = new String[srcLocatedBlock.getLocations().length];
           // 获取源文件块的位置信息
           for (int i = 0; i < srcLocatedBlock.getLocations().length; i++) {
             favoredNodes[i] = srcLocatedBlock.getLocations()[i].getHostName()
                 + ":" + srcLocatedBlock.getLocations()[i].getXferPort();
           }
           LOG.info("favoredNodes for " + srcLocatedBlock + ":" 
               + Arrays.toString(favoredNodes));

          for (int sleepTime = 2000, retries = 10; retries > 0; retries -= 1) {
             try {
               // 在NameNode最后创建新的目标块,以之前的源文件块的位置信息传入,作为优先选择的存储位置
               destinationLocatedBlock = dstNamenode.addBlock(destination,
                   clientName, previous, null, dstFileStatus.getFileId(),
                   favoredNodes);
               break;
             } catch (RemoteException e) {
             ...
           }
           if (destinationLocatedBlock == null) {
             throw new IOException("get null located block from namendoe");
           }

           blocksAdded++;

           // 拷贝真实数据
           copyBlock(srcLocatedBlock, destinationLocatedBlock);

           // 等待数据的拷贝
           waitForBlockCopy(blocksAdded);
       ...
         }

         terminateExecutor();

        // Wait for all blocks of the file to be copied.
         waitForFile(src, destination, previous, dstFileStatus.getFileId());

       } catch (IOException e) {
         LOG.error("failed to copy src : " + src + " dst : " + destination, e);
         // 如果此过程发生IO异常,则清除此次目标文件
        dstNamenode.delete(destination, false);
        throw e;
      } finally {
        shutdown();
      }
      return CopyResult.SUCCESS;
     }

接下来我们来看第二个关键部分的内容,DataNode节点上如何实现快速拷贝,假设此时前面在NameNode创建块的动作都已经完成了,最后就差DataNode节点的块复制操作。

FastCopy的复杂请求,最后触发到DataNode的对应方法copyBlock方法,代码如下:

   public void copyBlock(ExtendedBlock src, ExtendedBlock dst, DatanodeInfo dstDn)
       throws IOException {
     ...
     long onDiskLength = data.getLength(src);
     // 复制之前判断当前源块的长度是否一致,以此判断块是否损坏
     if (src.getNumBytes() > onDiskLength) {
       // Shorter on-disk len indicates corruption so report NN the corrupt block
       String msg = "copyBlock: Can't replicate block " + src
           + " because on-disk length " + onDiskLength
           + " is shorter than provided length " + src.getNumBytes();
       LOG.info(msg);
       throw new IOException(msg);
     }
     LOG.info(getDatanodeInfo() + " copyBlock: Starting thread to transfer: " +
         "block:"  +  src + " from " + this.getDatanodeUuid() + " to " + dstDn.getDatanodeUuid() + 
         "(" +dstDn + ")");
     Future<?> result;
     // 判断目标块的节点位置与源块节点是否一致
     if (this.getDatanodeUuid().equals(dstDn.getDatanodeUuid())) {
       // 如果是同一个节点,则是一次本地拷贝
       result = blockCopyExecutor.submit(new LocalBlockCopy(src, dst));
     } else {
       // 否则,则是普通的一次数据拷贝
       result = blockCopyExecutor.submit(new DataCopy(dstDn, src, dst));
     }
     try {
       // 等待拷贝过程5分钟
       result.get(5 * 60, TimeUnit.SECONDS);
     } catch (Exception e) {
       LOG.error(e);
       throw new IOException(e);
     }
   }

从上面的执行过程,我们可以看出,最后是2类方式的拷贝:LocalBlockCopy和DataCopy。

首先是LocalBlockCopy的本地拷贝方式,代码如下:

class LocalBlockCopy implements Callable<Boolean> {
     // 源块
     private ExtendedBlock srcBlock = null;
     // 目标块
     private ExtendedBlock dstBlock = null;

     ...

     public Boolean call() throws Exception {
       try {
         dstBlock.setNumBytes(srcBlock.getNumBytes());
         // 对源块创建一个新的硬链接
         data.hardLinkOneBlock(srcBlock, dstBlock);
         FsVolumeSpi v = (FsVolumeSpi)(getFSDataset().getVolume(dstBlock));
         // 关闭块操作
         closeBlock(dstBlock, DataNode.EMPTY_DEL_HINT, v.getStorageID());
        ...
       } catch (Exception e) {
         LOG.warn("Local block copy for src : " + srcBlock.getBlockName()
             + ", dst : " + dstBlock.getBlockName() + " failed", e);
         throw e;
       }
       return true;
     }
}

还有一种是普通方式的拷贝,会有节点间的数据传输,代码如下:

   private class DataCopy implements Runnable {
     // 目标块所在节点
     final DatanodeInfo target;
     // 源块
     final ExtendedBlock src;
     // 目标块
     final ExtendedBlock dst;
     ...

     @Override
     public void run() {
       ...
       try {
         final String dnAddr = target.getXferAddr(connectToDnViaHostname);
         InetSocketAddress curTarget = NetUtils.createSocketAddr(dnAddr);
         if (LOG.isDebugEnabled()) {
           LOG.debug("Connecting to datanode " + dnAddr);
         }
         // 首先与目标节点建立连接的过程
         sock = newSocket();
         NetUtils.connect(sock, curTarget, dnConf.socketTimeout);
         sock.setSoTimeout(dnConf.socketTimeout);

         ...

         long writeTimeout = dnConf.socketWriteTimeout;
         OutputStream unbufOut = NetUtils.getOutputStream(sock, writeTimeout);
        InputStream unbufIn = NetUtils.getInputStream(sock);
         DataEncryptionKeyFactory keyFactory =
           getDataEncryptionKeyFactoryForBlock(dst);
         IOStreamPair saslStreams = saslClient.socketSend(sock, unbufOut,
           unbufIn, keyFactory, accessToken, bpReg);
         unbufOut = saslStreams.out;
         unbufIn = saslStreams.in;
         // 新建输入、输出流对象
         out = new DataOutputStream(new BufferedOutputStream(unbufOut,
             HdfsConstants.SMALL_BUFFER_SIZE));
         in = new DataInputStream(unbufIn);
         blockSender = new BlockSender(src, 0, src.getNumBytes(), 
             false, false, true, DataNode.this, null, cachingStrategy);
         DatanodeInfo srcNode = new DatanodeInfo(bpReg);
         // 执行写块操作
         new Sender(out).writeBlock(dst, StorageType.DEFAULT, accessToken,
             "", new DatanodeInfo[] {target}, new StorageType[] {StorageType.DEFAULT}, srcNode,
             BlockConstructionStage.PIPELINE_SETUP_CREATE, 
             0, 0, 0, 0, blockSender.getChecksum(), cachingStrategy,
             false, false, null);

         // 用blockSender对象读取本地数据,并传输数据到目标节点中
         blockSender.sendBlock(out, unbufOut, null);
     ...
       } catch (IOException ie) {
         LOG.warn(bpReg + ":Failed to transfer " + src + " to " +
             target + " " + dst + " got ", ie);
           // check if there are any disk problem
         checkDiskErrorAsync();
       } finally {
         // 关闭各个对象
         xmitsInProgress.getAndDecrement();
         IOUtils.closeStream(blockSender);
         IOUtils.closeStream(out);
         IOUtils.closeStream(in);
         IOUtils.closeSocket(sock);
       }
     }
   }

如果前面本地拷贝中的硬链接功能不用的话,本人觉得这个DataCopy的方式同样是可以复用的。

以上部分代码只是FastCopy工具代码中的一部分,详细代码可以查阅本文末尾的参考资料。

以上就是本文的全部的内容了,希望大家能够好好体会FastCopy是如何让块尽量地在本地进行拷贝的,这是很核心的一个点。

参考资料


[1].Fast copy for HDFS
[2].https://issues.apache.org/jira/secure/attachment/12784877/HDFS-2139-For-2.7.1.patch

FastCopy ver2.08 2011/02/28 SHIROUZU Hiroaki FastCopy is the Fastest Copy/Delete Software on Windows. It can copy/delete unicode and over MAX_PATH(260byte) pathname files. It always run by multi-threading. It don't use MFC, it is compact and don't requre mfcxx.dll. FastCopy is BSD license, you can modify and use. License: --------------------------------------------------------------- Copyright 2004-2011 SHIROUZU Hiroaki All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY SHIROUZU Hiroaki ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SHIROUZU Hiroaki OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------------------------------------------------- Usage丗 Please see fastcopy.chm Build: FastCopy/Install/ShellExt ... VC4.1 ShellExt64 ... x64 compiler
评论 4 您还未登录,请先 登录 后发表或查看评论
MD5是目前最热门的加密算法,我们通常用MD5值来验证文件的完整性。例如在一些比较正规的下载网站,通常会提供软件的MD5值,这样我们就可以对下载回来的文件用MD5校检软件(如HashX等)做一次MD5校验,以确保我们获得的文件与该站点提供的文件为同一文件。但当两个不同文件的MD5值完全一样时,你还会信任MD5吗? 找出破解MD5加密方法的专家是我国山东大学的王小云教授,这则新闻在以前的软件版块曾详细报道过。但之后MD5的破解一直没有进展,直到最近,国外的科学家研究出了新的MD5碰撞破解方法,可以让两个不同文件的MD5值完全一样,而之前我们一直认为一个文件的MD5值在世界上是独一无二的,这就像一个人克隆了你的指纹然后冒充你一样恐怖! 为了验证MD5值的独一无二性,我们来做一个简单的试验 在桌面上新建一个文本文档,文件名为“test.txt”,内容为“OfficeBa”。然后将这个文本文档拖动到校验工具HashX中,点击左上角的“Hash File”按钮,得到其MD5值为051cb2917a5b70505e1687dee449c765,然后为文档中的“OfficeBa”加上双引号,保存后再通过HashX进行校检,发现MD5值变成了9ab117400993b70bc9945a9b15749d5d了。可见,一个极细微的变动都会导致文件的MD5值不同! 那么我们能让两个程序文件的MD5一致,却又都能正常运行,并且可以做完全不同的事情么?答案是“可以!”。要让两个不同文件的MD5值相同,可以通过一款名为fastcoll的小工具来完成我们同样以刚才的test.txt来做试验 -h [--help] 显示选项 -q [--quiet] 简化 -i [-ihv] arg 使用指定的初始值,默认是md5初始值 -p [-prefixfile] arg 使用给定的前缀计算初始值,仍然把数据复制到输出文件中(必须是个文件名) -o [--out] arg 指定输出文件名,此选项必须是最后一个参数,而且两个文件名必须同时指定 默认的是 -o msg1.bin msg2.bin 把解压出来的fastcoll_v1.0.0.5.exe与test.txt放在同一目录,然后在“命令提示符”中输入fastcoll_v1.0.0.5.exe -i test.txt -p test.txt -o cbi.exe cbi2.exe”并回车,在同目录中会生成名为cbi.exe和cbi2.exe文件,我们用HashX校验他们的MD5值,可以发现是完全一样的,但是在HashX中用“SHA-1”加密算法进行校验的时候,结果竟然是不同的(SHA-1加密算法生成的结果也是独一无二的)!可见这已经是完全不同的两个文件,但是他们的MD5值竟然完全相同。 如果黑客从网上下载一个工具,给其捆绑上木马,然后通过工具让其MD5值和原文件一样。那么当用户下载了文件后用MD5校验工具进行校验时就会发现带毒文件和原文件MD5值完全一样,就会放心地去运行,结果可想而知。所以,MD5加密已经不再可信!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

Android路上的人

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值