DFSClient技术内幕(写入数据——数据写入过程)

来源:互联网 发布:有人转让我的淘宝店铺 编辑:程序博客网 时间:2024/06/07 15:16
  以下是本人研究源代码成果, 此文僅献给我和我的小伙伴们,不足之处,欢迎斧正-------------------------------------致谢道格等人!
注:hadoop版本0.20.2,有童鞋表示看代码头晕,所以本文采用纯文字描述,哥还特意为你们把字体调调颜色噢 ^ o ^
上一篇文章,我们一起讨论了数据管道的建立,创建OutputStream下面我们一起讨论关于数据写入的详细过程,该过程对比前面,稍稍复杂一点点。。
=====================================================================
比较重要的一段话:
由于DFSOutputStream没有重写write()方法,而是复用了其父类FSDOutputSummer抽象类中的实现 ,write()方法会调用write1()方法,
尽可能地通过writeChecksumChunk()写出数据,这里的write()1方法和FSInputChecker.read1()的作用类似,主要是为了提高处理效率,避免数据拷贝。FSDOutputSummer.writeChecksumChunk()用于写出一个校验块的数据包括原始数据和校验信息,它会调用DFSOutputStream实现的writeChunk()方法。
---------------------------------------------------------------------------------------------------------------------------- 

//写入数据入口 

  public synchronized void write(byte b[], int off, int len)
  throws IOException {
    if (off < 0 || len < 0 || off > b.length - len) {
      throw new ArrayIndexOutOfBoundsException();
    }

    for (int n=0;n<len;n+=write1(b, off+n, len-n)) {
    }
  }
------------------------------------------------------------------------------------------------------------------------------------------------
 
/**
   * 将 b中 开始位置为off,长度为len的数据块写入文件中
   * 
   */
  private int write1(byte b[], int off, int len) throws IOException {
    if(count==0 && len>=buf.length) {
      
      // 如果buf缓冲区中无数据,同时写入的数据长度大于缓冲区大小
      // 则直接调用writeChecksumChunk写入数据,避免了外部拷贝
      final int length = buf.length;
      sum.update(b, off, length);
      writeChecksumChunk(b, off, length, false);
      return length;
    }
    
    // 
    int bytesToCopy = buf.length-count;
    bytesToCopy = (len<bytesToCopy) ? len : bytesToCopy;
    sum.update(b, off, bytesToCopy);
    //将b中数据复制到buf中
    System.arraycopy(b, off, buf, count, bytesToCopy);
    count += bytesToCopy;
    if (count == buf.length) {
      //当缓冲区满了之后,调用flushBuffer();
      flushBuffer();
    } 
    return bytesToCopy;
  }
------------------------------------------------------------------------------------------------------------------------------------------------
 
  /**
   * 首先  生成数据块对应的校验和,然后通过writeChunk方法将数据块校验和写入到输出流中
   */
  private void writeChecksumChunk(byte b[], int off, int len, boolean keep)
  throws IOException {
    int tempChecksum = (int)sum.getValue();
    if (!keep) {
      sum.reset();
    }
    int2byte(tempChecksum, checksum);
    writeChunk(b, off, len, checksum);
  }
 --------------------------------------------------------------------------------------------------------------------------------------------------
   /**
     * 
     */
    @Override
    protected synchronized void writeChunk(byte[] b, int offset, int len, byte[] checksum) 
                                                          throws IOException {
      //首先判断是否打开了到DataNode的输出流   
      checkOpen();
      isClosed();
      
      //取得要写入存储校验和缓冲区的长度
      int cklen = checksum.length;
      
      //取得校验和数据
      int bytesPerChecksum = this.checksum.getBytesPerChecksum(); 
      
      //如果要写入的数据的字节数大于 已取得校验和数据的长度的值,则会抛出对应的异常
      if (len > bytesPerChecksum) {
        throw new IOException("writeChunk() buffer size is " + len +
                              " is larger than supported  bytesPerChecksum " +
                              bytesPerChecksum);
      }
      
      //如果要写入的数据的校验和的字节数 大于系统规定的校验和的长度,则会抛出对应的异常
      if (checksum.length != this.checksum.getChecksumSize()) {
        throw new IOException("writeChunk() checksum size is supposed to be " +
                              this.checksum.getChecksumSize() + 
                              " but found to be " + checksum.length);
      }

      synchronized (dataQueue) {
  
        // 如果数据队列和确认队列已超过缓冲区能容纳的最大值(队列过多),那么数据的写入操作就需要进行等待
        while (!closed && dataQueue.size() + ackQueue.size()  > maxPackets) {
          try {
            dataQueue.wait();
          } catch (InterruptedException  e) {
          }
        }
        //如果输出流已经关闭或者有lastException,则需要抛出lastException
        isClosed();
        
        //如果当前正在被写入的Packet为空,则需要创建一个新的Packet
        if (currentPacket == null) {
            //非常关键的一步:打包数据,将数据若干个chunk打包成一个Packet 
            //初始化一个数据包 
          currentPacket = new Packet(packetSize, chunksPerPacket, 
                                     bytesCurBlock);
          if (LOG.isDebugEnabled()) {
            LOG.debug("DFSClient writeChunk allocating new packet seqno=" + 
                      currentPacket.seqno +
                      ", src=" + src +
                      ", packetSize=" + packetSize +
                      ", chunksPerPacket=" + chunksPerPacket +
                      ", bytesCurBlock=" + bytesCurBlock);
          }
        }
        
        //将数据和对应的校验和写入到Packet中
        currentPacket.writeChecksum(checksum, 0, cklen);
        currentPacket.writeData(b, offset, len);
        //记录下Packet中的Chunk数量和写入到当前Block中的数据的字节数
        currentPacket.numChunks++;
        bytesCurBlock += len;

        // 如果Packet满了,则将其添加到写入队列中
       
        if (currentPacket.numChunks == currentPacket.maxChunks ||
            bytesCurBlock == blockSize) {
          if (LOG.isDebugEnabled()) {
            LOG.debug("DFSClient writeChunk packet full seqno=" +
                      currentPacket.seqno +
                      ", src=" + src +
                      ", bytesCurBlock=" + bytesCurBlock +
                      ", blockSize=" + blockSize +
                      ", appendChunk=" + appendChunk);
          }
         /**
          * 如果当前的Block已被填满,则将正在被写入的Packet标记为Block中的最后一个Packet,
          * 并将Block的统计信息进行重置
          */
          if (bytesCurBlock == blockSize) {
            currentPacket.lastPacketInBlock = true;
            bytesCurBlock = 0;
            lastFlushOffset = -1;
          }
          //将最后一个Packet加入到 数据队列中,将当前被写入的数据包设置为null
          dataQueue.addLast(currentPacket);
          dataQueue.notifyAll();
          currentPacket = null;
 
         /**
          * 设置为不允许向当前Block添加Packet,然后重置数据块缓冲区校验和,
          */
          if (appendChunk) {
            appendChunk = false;
            resetChecksumChunk(bytesPerChecksum);
          }
          //TODO step5
          //如果是将数据添加到打开文件的末尾,那么数据在被写入之后,需要调用computePacketChunkSize来重置Chunk的大小
          // 
writePacketSize     数据包最多能达到64K字节 
          int psize = Math.min((int)(blockSize-bytesCurBlock), writePacketSize);
          computePacketChunkSize(psize, bytesPerChecksum);
        }
      }
      //LOG.debug("DFSClient writeChunk done length " + len +
      //          " checksum length " + cklen);
    }
----------------------------------------------------------------------------------------------------------------------------------------------
下面分析下一个比较重要的类Packet数据包,它属于DFSOutputStream内部类 
 
//Packet构造器
      Packet(int pktSize, int chunksPerPkt, long offsetInBlock) {
        //该packet是否为Block中的最后一个packet 
        this.lastPacketInBlock = false;
        //packet当前所含的chunk的数量
        this.numChunks = 0;
        //packet当前所含的chunk的数量 
        this.offsetInBlock = offsetInBlock;
        // 缓冲区在Block中的序列号 
        this.seqno = currentSeqno;
        //当前正在被DataStreamer发送的Packet在整个数据Block中的序列 
        currentSeqno++;
        //数据缓冲区
        buffer = null;
        //字节缓冲区 
        buf = new byte[pktSize];
        //校验和的开始位置
        checksumStart = DataNode.PKT_HEADER_LEN + SIZE_OF_INTEGER;
        //校验和的当前位置 
        checksumPos = checksumStart;
        //数据的开始位置 
        dataStart = checksumStart + chunksPerPkt * checksum.getChecksumSize();
        //数据的当前位置 
        dataPos = dataStart;
        //packet最多可以包含的chunk的数量
        maxChunks = chunksPerPkt;
      }
----------------------------------------------------------------------------------------------------------------------------------------------------
向数据packed中写入校验和信息
/**
       * 向数据packed中写入校验和信息
       * @param inarray
       * @param off
       * @param len
       */
      void  writeChecksum(byte[] inarray, int off, int len) {
        if (checksumPos + len > dataStart) {
          throw new BufferOverflowException();
        }
        System.arraycopy(inarray, off, buf, checksumPos, len);
        checksumPos += len;
      } 
向数据packed中写入数据信息
/**
       * 向数据packet中写入数据信息
       * @param inarray
       * @param off
       * @param len
       */
      void writeData(byte[] inarray, int off, int len) {
        if ( dataPos + len > buf.length) {
          throw new BufferOverflowException();
        }
        System.arraycopy(inarray, off, buf, dataPos, len);
        dataPos += len;
      } 

  protected synchronized void resetChecksumChunk(int size) {
    //重置校验和 
    sum.reset();
    //重置 数据缓冲区
    this.buf = new byte[size];
     
    this.count = 0;
  } 
==================================================================================================
上面我们已经将数据打包成了若干数据包,在这个过程中,已经有一个家伙等待很久了。。
他就是数据发送器:DataStreamer ,现在数据数据开始等待数据发送器取数据,发送数据
public void run() {
      //当·线程没有关闭,而且客户端正在运行着,则开始循环发送Packet
        while (!closed && clientRunning) {

         //如果在发送Packet过程中,处理DataNode返回的响应的ResponseProcessor线程发生了错误,那么需要将response关闭掉
          if (hasError && response != null) {
            try {
            //TODO
              response.close();
              //当主线程处理完其他的事务后,需要用到子线程的处理结果,这个时候就要用到join()
              response.join();
              response = null;
            } catch (InterruptedException  e) {
            }
          }
          
          Packet one = null;
          synchronized (dataQueue) {

            // 调用processDatanodeError来处理任何可能的IO错误
            boolean doSleep = processDatanodeError(hasError, false);
            
            // 然后DataStreamer会在dataQueue上进行等待,一直到dataQueue上出现需要发送的Packet为止
            while ((!closed && !hasError && clientRunning 
                   && dataQueue.size() == 0) || doSleep) {
              try {
                    
             到这里,数据发送器需要等待,下面我们回到主线程,数据打包,将数据打包并加入到数据队列 的过程
             
                dataQueue.wait(1000);
              } catch (InterruptedException  e) {
              }
              doSleep = false;
            }
            
            //如果在等待数据包的过程中,DataStreamer线程被关闭,或者发生了IO错误,
            //或者客户端停止了运行,那么将直接跳过此次发送Packet的循环
            if (closed || hasError || dataQueue.size() == 0 || !clientRunning) {
              continue;
            }
            我们现在从这里开始。。。
            try {
              // 获取一个Packet等待发送
              one = dataQueue.getFirst();
              long offsetInBlock = one.offsetInBlock;
  
              //如果到DataNode的blockStream输出流还没被打开,
              //那么首先需要调用nextBlockOutputStream方法来连接起与DataNode的连接
              //然后启动ResponseProcessor线程
              if (blockStream == null) {
                LOG.debug("Allocating new block");
                nodes = nextBlockOutputStream(src); 
                this.setName("DataStreamer for file " + src +
                             " block " + block);
                response = new ResponseProcessor(nodes);
        启动响应处理线程 。。。
                response.start();
              }
              
              //如果Packet在数据Block中的偏移量大于Block的大小,那么会抛出对应的异常
              if (offsetInBlock >= blockSize) {
                throw new IOException("BlockSize " + blockSize +
                                      " is smaller than data size. " +
                                      " Offset of packet in block " + 
                                      offsetInBlock +
                                      " Aborting file " + src);
              }
              
              //取得Packet中的所有信息:取数据
              ByteBuffer buf = one.getBuffer();
              
              // 数据包即将发送则,将该包从等待发送队列中移除,添加到数据确认队列中,等待确认
              dataQueue.removeFirst();
              dataQueue.notifyAll();
              synchronized (ackQueue) {
                ackQueue.addLast(one);
                ackQueue.notifyAll();
              } 
              
              // 通过blockStream 将ByteBuffer里面的所有信息都发送给 远程的DataNode
              blockStream.write(buf.array(), buf.position(), buf.remaining());
              
              //如果该Packet是最后一个,那么写入0,通知DataNode数据传输完成
              if (one.lastPacketInBlock) {
                blockStream.writeInt(0); // indicate end-of-block 
              }
              //刷新blockStream,确保写入
              blockStream.flush();
              if (LOG.isDebugEnabled()) {
                LOG.debug("DataStreamer block " + block +
                          " wrote packet seqno:" + one.seqno +
                          " size:" + buf.remaining() +
                          " offsetInBlock:" + one.offsetInBlock + 
                          " lastPacketInBlock:" + one.lastPacketInBlock);
              }
            } catch (Throwable e) {
              LOG.warn("DataStreamer Exception: " + 
                       StringUtils.stringifyException(e));
              if (e instanceof IOException) {
                setLastException((IOException)e);
              }
              hasError = true;
            }
          }

          if (closed || hasError || !clientRunning) {
            continue;
          }

          //当Block的最后一个Packet发送出去后,DataStreamer会一直等待ackQueue队列为空,
          //即与所有Packet对应的响应都已经被接受了,然后执行清理工作,
          //首先关闭response线程,然后关闭socket连接
          if (one.lastPacketInBlock) {
            synchronized (ackQueue) {
              while (!hasError && ackQueue.size() != 0 && clientRunning) {
                try {
                 到这里,数据发送器需要等待,下面我们进入重要的另一个环节,数据确认
                    数据确认线程早已经启动,一直处于等待状态,现在它获得了执行机会 
                  ackQueue.wait();   // 等待确认队列接收来自DataNode的响应,并做相应处理(运行)
                } catch (InterruptedException  e) {
                }
              }
            }
            LOG.debug("Closing old block " + block);
            this.setName("DataStreamer for file " + src);

            response.close();        // ignore all errors in Response
            try {
              response.join();
              response = null;
            } catch (InterruptedException  e) {
            }

            if (closed || hasError || !clientRunning) {
              continue;
            }

            synchronized (dataQueue) {
              try {
                blockStream.close();
                blockReplyStream.close();
              } catch (IOException e) {
              }
              nodes = null;
              response = null;
              blockStream = null;
              blockReplyStream = null;
            }
          }
          
          //汇报数据的写入进度信息
          if (progress != null) { progress.progress(); }

          // This is used by unit test to trigger race conditions.
          if (artificialSlowdown != 0 && clientRunning) {
            try { 
              Thread.sleep(artificialSlowdown); 
            } catch (InterruptedException e) {}
          }
        }
      } 
-----------------------------------------------------------------------------------------------------------------------------------
数据包数据添加到数据buffer的详细过程分析:
    /**
       *当需要将packet发送到数据节点时,返回Packet中封装的所有数据(此时,不能再向Packet写入数据)
       */
      ByteBuffer getBuffer() {
    //当buffer不为null,直接返回
        if (buffer != null) {
          return buffer;
        }
        
        /**计算Packet中数据的实际长度**/
        int dataLen = dataPos - dataStart;
        
        //计算出Packet中校验和的实际长度
        int checksumLen = checksumPos - checksumStart;
        
        //如果校验和和数据段不连续,则使用校验和来填充校验和与数据段之间的间隔
        if (checksumPos != dataStart) {
         
          System.arraycopy(buf, checksumStart, buf, 
                           dataStart - checksumLen , checksumLen); 
        }
        
        //计算Packet的总长度(4+数据长度+校验和长度)
        int pktLen = SIZE_OF_INTEGER + dataLen + checksumLen;
        
        //将Packet的所有数据写入到buffer中
        buffer = ByteBuffer.wrap(buf, dataStart - checksumPos,
                                 DataNode.PKT_HEADER_LEN + pktLen);
        //将packet中的buf置空
        buf = null;
        buffer.mark();
        
        
        //将packet的长度信息写入到buffer中
        buffer.putInt(pktLen);  // pktSize
        
        //将packet在Block中的偏移信息写入到buffer中
        buffer.putLong(offsetInBlock); 
        
        //将Packet在Block中的序列号信息写入到buffer中
        buffer.putLong(seqno);
        
        //将该packet是否是Block中最后一个Packet的标识信息写入到buffer中
        buffer.put((byte) ((lastPacketInBlock) ? 1 : 0));

        //将该Packet中实际的数据信息(不包括校验和)的长度写入到buffer中
        buffer.putInt(dataLen); 
        
        //重置buffer
        buffer.reset();
        return buffer;
      } 
-----------------------------------------------------------------------------------------------------------------------------------------
响应处理线程ResponseProcessor详细过程分析: 很简单等待来自DataNode管道中行的DataNode的响应
如果成功,就将对应的数据包从确认队列删除
如果失败,就记录下该DataNode的下标,抛出异常 


 private class ResponseProcessor extends Thread {
      /**ResponseProcessor线程是否被关闭的标识**/
      private volatile boolean closed = false;
      
      /**等待发送响应的DataNode集合**/
      private DatanodeInfo[] targets = null;
      
      /**标识某个Packet是否为Block中的最后一个Packet**/
      private boolean lastPacketInBlock = false;
      
      
      /**
       * 响应处理器ResponseProcessor 继承自Thread
       * DataStreamer会为写入的每个Block启动一个ResponseProcessor线程,
       * 该线程主要用于等待来自DataNode管道中的DataNode的响应
       * 如果是成功的响应,则将对应的Packet从ackQueue删除,
       * 如果是失败的响应,则需要记录下出错的DataNode,并设置对应的标志位.
       * 
       * 在构造方法中完成对目标DataNode集合的初始化
       * @author Administrator
       *
       */
      ResponseProcessor (DatanodeInfo[] targets) {
        this.targets = targets;
      }

      public void run() {
    //初始化管道响应对象。  PipelineAck是DataTransferProtocol接口中的一个静态内部类
        this.setName("ResponseProcessor for block " + block);
        PipelineAck ack = new PipelineAck();
        
        
       //只要该线程没有被关闭,客户端也正在运行,同时处理的Packet也不是Block中的最后一个Packet,
        //那么就循环处理来自DataNode的响应
        while (!closed && clientRunning && !lastPacketInBlock) {
       
          try {
        //从管道流中读取一个响应
            ack.readFields(blockReplyStream, targets.length);
            if (LOG.isDebugEnabled()) {
              LOG.debug("DFSClient " + ack);
            }
            
            //取得返回的响应所对应的Packet的序列号
            long seqno = ack.getSeqno();
            
            //如果返回的序列号为心跳Packet对应的序列号
            //则终止此次循环,进入下一次循环处理
            //TODO
            if (seqno == PipelineAck.HEART_BEAT.getSeqno()) {
              continue;
            } else if (seqno == -2) {
              // This signifies that some pipeline node failed to read downstream
              // and therefore has no idea what sequence number the message corresponds
              // to. So, we don't try to match it up with an ack.
              assert ! ack.isSuccess();
            } else {
              Packet one = null;
              synchronized (ackQueue) {
            //从确认队列中取出第一个Packet
                one = ackQueue.getFirst();
              }
              
              //如果DataNode返回的序列号和 从确认队列中取出的Packet的序列号不一致,
              //那么会抛出对应异常
              if (one.seqno != seqno) {
                throw new IOException("Responseprocessor: Expecting seqno " + 
                                      " for block " + block +
                                      one.seqno + " but received " + seqno);
              }
              lastPacketInBlock = one.lastPacketInBlock;
            }

            // 循环处理DataNode管道中所有DataNode返回的响应,
            // 如果某个DataNode返回失败的响应,
            // 则用errorIndex记录下出错的DataNode的索引,并抛出对应的异常
            for (int i = 0; i < targets.length && clientRunning; i++) {
              short reply = ack.getReply(i);
              if (reply != DataTransferProtocol.OP_STATUS_SUCCESS) {
                errorIndex = i; // first bad datanode
                throw new IOException("Bad response " + reply +
                                      " for block " + block +
                                      " from datanode " + 
                                      targets[i].getName());
              }
            }

            synchronized (ackQueue) {
            //将成功返回响应的Packet从确认队列中删除
              ackQueue.removeFirst();
              ackQueue.notifyAll();
            }
          } catch (Exception e) {
            if (!closed) {
              hasError = true;
              if (e instanceof IOException) {
                setLastException((IOException)e);
              }
              LOG.warn("DFSOutputStream ResponseProcessor exception " + 
                       " for block " + block +
                        StringUtils.stringifyException(e));
              closed = true;
            }
          }
          
          //最后通知dataQueue和ackQueue的所有监听者
          synchronized (dataQueue) {
            dataQueue.notifyAll();
          }
          synchronized (ackQueue) {
            ackQueue.notifyAll();
          }
        }
      }

      void close() {
        closed = true;
        this.interrupt();
      }
    } 

至此,响应处理线程工作完毕,已被设置为关闭状态 ,我们再回到数据发送线程,比较简单,本人就用文字描述下
关闭响应线程, 取得响应线程运行结果,关闭连接到DataNode的输出流,关闭接收DataNode响应信息的输入流

  response.close();        // ignore all errors in Response
  try {
              response.join();
              response = null;
  } catch (InterruptedException  e) {
   }

   if (closed || hasError || !clientRunning) {
              continue;
  }

  synchronized (dataQueue) {
   try {
                blockStream.close();
                blockReplyStream.close();
   } catch (IOException e) {
   }
              nodes = null;
              response = null;
              blockStream = null;
              blockReplyStream = null;
     }
   }
          
          //汇报数据的写入进度信息
          if (progress != null) { progress.progress(); }

          // This is used by unit test to trigger race conditions.
          if (artificialSlowdown != 0 && clientRunning) {
            try { 
              Thread.sleep(artificialSlowdown); 
            } catch (InterruptedException e) {}
          }

------------------------------------------------------------------------------------------------------------------------------------------------------
数据发送线程的关闭方法:
// close方法关于关闭DataStreamer线程,首先将线程的关闭标识closed设置为true,
      // 然后分别通知dataQueue和ackQueue队列上的所有等待者
      void close() {
        closed = true;
        synchronized (dataQueue) {
          dataQueue.notifyAll();
        }
        synchronized (ackQueue) {
          ackQueue.notifyAll();
        }
        this.interrupt();
      } 
=============================================================================================
DFSClient的关闭方法:
/**
  * 终止leasechecker守护线程从而释放掉客户端所持有的租约,并调用RPC的stopProxy方法来断开与NameNode的RPC连接
  */
  public synchronized void close() throws IOException {
    if(clientRunning) {
       //先关闭租约线程,释放租约
      leasechecker.close();
      clientRunning = false;
      try {
        leasechecker.interruptAndJoin();
      } catch (InterruptedException ie) {
      }
  
      //关闭到namenode的连接
      RPC.stopProxy(rpcNamenode);
    }
  } 
-----------------------------------------------------------------------------------------------------------------------------------------
DFSOutputStream的关闭方法: 

 @Override
    public void close() throws IOException {
      if (closed) {
        IOException e = lastException;
        if (e == null)
          return;
        else
          throw e;
      }
      closeInternal();
      leasechecker.remove(src);
      
      if (s != null) {
        s.close();
        s = null;
      }
    } 
closeInternal();有兴趣可以参考源码,
===============================================================================================
至此,DFSClient分布式文件系统客户端分析完毕!
       客户端是应用访问hadoop分布式文件系统的起点,作为文件系统界面,DFSClient提供了
处理文件和目录相关事务和基于流的文件数据读写等典型文件系统应用开发接口,同时也支持对HDFS进行文件系统管理
        从实现的角度来看,文件和目录,系统管理相关事务等都不需要和数据节点直接打交道,它们的实现比较简单,
通过远程接口访问名字节点提供的服务即可。支持文件数据读写的输入/输出流比较复杂,为了保证数据的完整性,他们都必须处理数据检验带来的基于块的校验和基于字节流的读写差异
        对于读取数据,在确定数据块的位置后,就可以通过数据节点的流式接口获取数据,但客户端需要处理校验错误信息上报名字节点,失败重试,数据节点返回数据的边界等细节,
       对于写数据,正常的流程涉及打开或创建文件,新数据块的分配,数据管道的建立,数据组包的发送,数据包应答处理,租约更新,关闭文件等要点,同时还要对应数据流管道建立失败,写入数据时数据节点出错等故障,是HDFS上最复杂的流程之一,也是掌握HDFS原理的难点
      客户端提供了一个很好的观测点,让我们了解到了名字节点和数据节点,客户端之间的配合关系,展示了HDFS的实现全景!!

最后致谢hadoop之父Doug Cutting为我们奉献了神一样的代码!!

 

 













0 0