HDFS中两种random read比较

本文深入探讨Hadoop文件系统的两种读取模式:pread和seek+read。详细解析了这两种模式下如何确定读取的数据块范围,如何选择数据节点进行连接,并通过具体代码解释了它们的工作流程。此外,还对比了两种模式的效率差异。
部署运行你感兴趣的模型镜像

code version: hadoop-0.19.1

 

首先说pread。pread会明确的把要读的size传给datanode(在new BlockReader的时候)

 

 /**
     * Read bytes starting from the specified position.
     * 
     * @param position start read from this position
     * @param buffer read buffer
     * @param offset offset into buffer
     * @param length number of bytes to read
     * 
     * @return actual number of bytes read
     */
    @Override
    public int read(long position, byte[] buffer, int offset, int length)
      throws IOException {
      // sanity checks
      checkOpen();
      if (closed) {
        throw new IOException("Stream closed");
      }
      long filelen = getFileLength();
      if ((position < 0) || (position >= filelen)) {
        return -1;
      }
      int realLen = length;
      if ((position + length) > filelen) {
        realLen = (int)(filelen - position);
      }
      
      // determine the block and byte range within the block
      // corresponding to position and realLen
      List<LocatedBlock> blockRange = getBlockRange(position, realLen);
      int remaining = realLen;
      for (LocatedBlock blk : blockRange) {
        long targetStart = position - blk.getStartOffset();
        long bytesToRead = Math.min(remaining, blk.getBlockSize() - targetStart);
        fetchBlockByteRange(blk, targetStart, targetStart + bytesToRead - 1, buffer, offset);
        remaining -= bytesToRead;
        position += bytesToRead;
        offset += bytesToRead;
      }
      assert remaining == 0 : "Wrong number of bytes read.";
      if (stats != null) {
        stats.incrementBytesRead(realLen);
      }
      return realLen;
    }
     

 

private void fetchBlockByteRange(LocatedBlock block, long start,
                                     long end, byte[] buf, int offset) throws IOException {
      //
      // Connect to best DataNode for desired Block, with potential offset
      //
      Socket dn = null;
      int numAttempts = block.getLocations().length;
      IOException ioe = null;
      
      while (dn == null && numAttempts-- > 0 ) {
	long prepareRealReadStart = System.currentTimeMillis();
        DNAddrPair retval = chooseDataNode(block);
        DatanodeInfo chosenNode = retval.info;
        InetSocketAddress targetAddr = retval.addr;
        BlockReader reader = null;
            
        try {
          dn = socketFactory.createSocket();
          NetUtils.connect(dn, targetAddr, socketTimeout);
          dn.setSoTimeout(socketTimeout);
              
          int len = (int) (end - start + 1);
              
          
          reader = BlockReader.newBlockReader(dn, src, 
                                              block.getBlock().getBlockId(),
                                              block.getBlock().getGenerationStamp(),
                                              start, len, buffersize, 
                                              verifyChecksum, clientName);
          int nread = reader.readAll(buf, offset, len);
          if (nread != len) {
            throw new IOException("truncated return from reader.read(): " +
                                  "excpected " + len + ", got " + nread);
          }
          return;
        } catch (ChecksumException e) {
          ioe = e;
          LOG.warn("fetchBlockByteRange(). Got a checksum exception for " +
                   src + " at " + block.getBlock() + ":" + 
                   e.getPos() + " from " + chosenNode.getName());
          reportChecksumFailure(src, block.getBlock(), chosenNode);
        } catch (IOException e) {
          ioe = e;
          LOG.warn("Failed to connect to " + targetAddr + 
                   " for file " + src + 
                   " for block " + block.getBlock().getBlockId() + ":"  +
                   StringUtils.stringifyException(e));
        } finally {
          IOUtils.closeStream(reader);
          IOUtils.closeSocket(dn);
          dn = null;
        }
        // Put chosen node into dead list, continue
        addToDeadNodes(chosenNode);
      }
      throw (ioe == null) ? new IOException("Could not read data") : ioe;
    }

 

pread 的过程:

根据要读的数据的offset和readLen,计算出要读的blockRange,即有哪些block在要读的范围内。具体的getBlockRange()这个函数中要判断要读的blocks是否在维护的locatedBlocks这个block cache中,如果不在,要问namenode查询,然后再放入到cache中。

然后在针对获得的blockRange中每个block读取数据,选取datanode,创建连接,对每个block都要重新生成一个BlockReader(这种实现比较废柴啊!)

 

然后看seek+read,read会把当前位置到block结束的长度传给datanode (也是在new BlockReader的时候)这样DataNode就可以read ahead,然后由于TCP_WINDOW的buffer作用(hadoop code里面是128K),可以加快连续读的性能。

 

    /**
     * Seek to a new arbitrary location
     */
    @Override
    public synchronized void seek(long targetPos) throws IOException {
      if (targetPos > getFileLength()) {
        throw new IOException("Cannot seek after EOF");
      }
      boolean done = false;
      if (pos <= targetPos && targetPos <= blockEnd) {
        //
        // If this seek is to a positive position in the current
        // block, and this piece of data might already be lying in
        // the TCP buffer, then just eat up the intervening data.
        //
        int diff = (int)(targetPos - pos);
        if (diff <= TCP_WINDOW_SIZE) {
          try {
            pos += blockReader.skip(diff);
            if (pos == targetPos) {
              done = true;
            }
          } catch (IOException e) {//make following read to retry
            LOG.debug("Exception while seek to " + targetPos + " from "
                      + currentBlock +" of " + src + " from " + currentNode + 
                      ": " + StringUtils.stringifyException(e));
          }
        }
      }
      if (!done) {
        pos = targetPos;
        blockEnd = -1;
      }
    }

这个 seek其实是不做什么事情的(我的测试中做上万次seek,平均时间是0)。它主要移动pos这个游标:如果在当前block中,就移动到正确位置,否则,就把pos设成目标位置,但是blockEnd置成-1.这样其实最终的seek任务是在后面的read里面实现的。

看read的code:

/**
     * Read the entire buffer.
     */
    @Override
    public synchronized int read(byte buf[], int off, int len) throws IOException {
      checkOpen();
      if (closed) {
        throw new IOException("Stream closed");
      }
      if (pos < getFileLength()) {
        int retries = 2;
        while (retries > 0) {
          try {
            if (pos > blockEnd) {
	      currentNode = blockSeekTo(pos);
	    }
            int realLen = Math.min(len, (int) (blockEnd - pos + 1));
	     int result = readBuffer(buf, off, realLen);
	    	                
            if (result >= 0) {
              pos += result;
            } else {
              // got a EOS from reader though we expect more data on it.
              throw new IOException("Unexpected EOS from the reader");
            }
            if (stats != null && result != -1) {
              stats.incrementBytesRead(result);
            }
            return result;
          } catch (ChecksumException ce) {
            throw ce;            
          } catch (IOException e) {
            if (retries == 1) {
              LOG.warn("DFS Read: " + StringUtils.stringifyException(e));
            }
            blockEnd = -1;
            if (currentNode != null) { addToDeadNodes(currentNode); }
            if (--retries == 0) {
              throw e;
            }
          }
        }
      }
      return -1;
    }

 如果上面seek的时候要seek的位置在同一个block,现在就只需直接读好了。如果不在,刚才知道blockEnd会被置成-1,现在就要做一次真正的seek操作,函数blockSeekTo()实现这一功能。

看blockSeekTo()干了哪些事情:

/**
     * Open a DataInputStream to a DataNode so that it can be read from.
     * We get block ID and the IDs of the destinations at startup, from the namenode.
     */
    private synchronized DatanodeInfo blockSeekTo(long target) throws IOException {
      if (target >= getFileLength()) {
        throw new IOException("Attempted to read past end of file");
      }

      if ( blockReader != null ) {
        blockReader.close(); 
        blockReader = null;
      }
      
      if (s != null) {
        s.close();
        s = null;
      }

      //
      // Compute desired block
      //
      LocatedBlock targetBlock = getBlockAt(target);
      assert (target==this.pos) : "Wrong postion " + pos + " expect " + target;
      long offsetIntoBlock = target - targetBlock.getStartOffset();

      //
      // Connect to best DataNode for desired Block, with potential offset
      //
      DatanodeInfo chosenNode = null;
      while (s == null) {
        DNAddrPair retval = chooseDataNode(targetBlock);
        chosenNode = retval.info;
        InetSocketAddress targetAddr = retval.addr;
        try {
          s = socketFactory.createSocket();
          NetUtils.connect(s, targetAddr, socketTimeout);
          s.setSoTimeout(socketTimeout);
          Block blk = targetBlock.getBlock();
          
          blockReader = BlockReader.newBlockReader(s, src, blk.getBlockId(), 
              blk.getGenerationStamp(),
              offsetIntoBlock, blk.getNumBytes() - offsetIntoBlock,
              buffersize, verifyChecksum, clientName);
          return chosenNode;
        } catch (IOException ex) {
          // Put chosen node into dead list, continue
          LOG.debug("Failed to connect to " + targetAddr + ":" 
                    + StringUtils.stringifyException(ex));
          addToDeadNodes(chosenNode);
          if (s != null) {
            try {
              s.close();
            } catch (IOException iex) {
            }                        
          }
          s = null;
        }
      }
      return chosenNode;
    }

 发现这个函数跟上面的fetchBlockByteRange一样废柴,先把先前的blockReader close掉,然后再创建到目的datanode的新连接,主要不同的地方在哪里呢,在于getBlockRange和getBlockAt的区别,上面的pread模式提供了第二个参数readLen,找目标blocks的时候找这个范围内的就可以了,而seek+read这种模式假设的是seek的时候并不知道后面要读的长度,所以用了一个缺省的prefetchSize,缺省是10个block size大小。

 

那综合考虑两种read,作为random read的实现,第一种pread无论何时都要重新创建连接,第二种当要读的数据在当前block的时候可以重用上次的连接,理论上应该第二种效率高些。

您可能感兴趣的与本文相关的镜像

Kotaemon

Kotaemon

AI应用

Kotaemon 是由Cinnamon 开发的开源项目,是一个RAG UI页面,主要面向DocQA的终端用户和构建自己RAG pipeline

<think>我们使用Spark读取OFS(如GooseFS)上的Parquet文件,并随机抽取100条记录显示。 步骤: 1. 初始化SparkSession 2. 使用Spark读取指定路径的Parquet文件,生成DataFrame 3. 使用DataFrame的sample方法进行随机抽样,或者使用orderBy(rand())然后limit(100) 4. 显示结果 注意:由于是随机抽样,每次执行结果可能不同。 这里我们使用两种方法: 方法一:使用sample(withReplacement, fraction) 按比例抽样,但可能抽到的不是正好100条,所以我们可以设置一个较大的比例确保能抽到100条以上,然后取前100条(但这样不是严格随机,因为前100条可能来自同一个分区)。或者我们可以使用fraction=100.0/总条数,但总条数需要先计算,这样效率不高。 方法二:使用orderBy(rand()).limit(100) 这样可以保证随机性且正好100条,但是会进行全局排序,性能可能较差,特别是数据量大的时候。 方法三(推荐):使用rand()生成随机数,然后过滤出随机数小于某个阈值的记录,直到达到100条?但这样也不精确。 实际上,我们可以使用DataFrame的sample方法并设置精确的样本大小(但sample方法只能按比例,不能精确指定数量)。所以我们可以使用: df.sample(false, 0.1).limit(100) # 0.1是抽样比例,需要根据数据量调整,确保大于100条,然后取前100条。但这样不是严格随机,因为limit会取前100条,可能分布不均匀。 另一种更准确的方法是使用分层抽样,但这里不需要分层。 在Spark 3.0+中,我们可以使用`df.orderBy(rand()).limit(100)`,虽然全局排序,但数据量不是特别大时是可以接受的。 如果数据量很大,我们可以采用以下方法: - 先计算总行数 - 然后生成100个[0, 总行数)之间的随机数(不重复),然后根据这些随机数取行。但这种方法需要为每一行生成一个行号,而Spark DataFrame没有内置的行号,而且行号生成开销大。 因此,对于一般情况,我们采用`orderBy(rand()).limit(100)`,它简单且随机性好。 代码示例: ```python from pyspark.sql import SparkSession from pyspark.sql.functions import rand # 初始化SparkSession spark = SparkSession.builder \ .appName("RandomSampleFromParquet") \ .getOrCreate() # 指定OFS路径,例如:ofs://path/to/your/parquet parquet_path = "ofs://path/to/your/parquet" # 读取Parquet文件 df = spark.read.parquet(parquet_path) # 随机抽取100条记录 # 方法:使用rand()函数生成随机数列,然后按此列排序,取前100条 random_sample = df.orderBy(rand()).limit(100) # 显示结果 random_sample.show(100, truncate=False) # 显示100条,不截断 # 停止SparkSession spark.stop() ``` 注意:如果数据量非常大,全局排序可能效率较低。如果对性能要求高,可以考虑以下优化: - 使用`df.sample(withReplacement=False, fraction=0.01).limit(100)`,先按比例抽样减少数据量,然后再排序取100条。但这样可能因为第一次抽样导致最终结果不是完全随机(但近似随机)。 根据实际需求选择方法。 另外,如果OFS路径是GooseFS,需要确保Spark集群已经配置好GooseFS的访问。通常需要将GooseFS的客户端jar包放在Spark的jars目录下,并在Spark配置中指定文件系统为GooseFS(如`spark.hadoop.fs.ofs.impl=com.tencent.cloud.goosefs.hadoop.GooseFileSystem`)。 如果已经配置好,那么直接使用`ofs://`路径即可。 如果未配置,需要先配置。 以上代码假设环境已经配置好。 相关问题: 1. 如何配置Spark访问GooseFS? 2. 在数据量非常大的情况下,如何优化随机抽样的性能? 3. 除了显示100条记录,如何将随机抽样的结果保存到文件? 4. 使用rand()和orderBy进行随机抽样是否会导致数据倾斜?</think>### Spark读取OFS路径Parquet文件并随机输出100条记录 #### 解决方案步骤: 1. **初始化SparkSession** 配置Spark访问OFS(如GooseFS)路径,需添加`fs.ofs.impl`参数: ```python from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("OFS_Parquet_Random_Sample") \ .config("spark.hadoop.fs.ofs.impl", "com.tencent.cloud.goosefs.hadoop.GooseFileSystem") \ .getOrCreate() ``` 2. **读取Parquet文件** 使用`spark.read.parquet()`加载数据: ```python ofs_path = "ofs://your-bucket-name/path/to/parquet_files" df = spark.read.parquet(ofs_path) ``` 3. **随机抽样100条记录** 通过`orderBy(rand())`实现高效随机抽样: ```python from pyspark.sql.functions import rand random_sample = df.orderBy(rand()).limit(100) ``` 4. **输出结果** 显示抽样数据(完整内容不截断): ```python random_sample.show(100, truncate=False) ``` #### 完整代码示例: ```python from pyspark.sql import SparkSession from pyspark.sql.functions import rand # 初始化SparkSession(OFS配置) spark = SparkSession.builder \ .appName("OFS_Parquet_Random_Sample") \ .config("spark.hadoop.fs.ofs.impl", "com.tencent.cloud.goosefs.hadoop.GooseFileSystem") \ .getOrCreate() # 读取Parquet文件 df = spark.read.parquet("ofs://your-bucket-name/data/user_logs/year=2023") # 随机抽样并显示100条 df.orderBy(rand()).limit(100).show(100, truncate=False) spark.stop() ``` #### 关键说明: 1. **OFS路径格式** - 使用 `ofs://` 协议头(例如 `ofs://bucket-name/dir/file.parquet`) - 需确保集群已配置GooseFS客户端[^1] 2. **随机抽样原理** - `orderBy(rand())` 为每条记录生成随机数并排序 - `limit(100)` 取前100条,避免全表扫描 3. **Parquet优化特性** - 列式存储减少I/O,仅读取所需列[^2] - 支持谓词下推(如分区过滤)提升性能 #### 注意事项: - **GooseFS缓存**:首次读取可能触发异步缓存(`asyncCache`),后续访问加速[^1] - **数据倾斜处理**:若数据分布不均,可改用分层抽样: ```python df.sampleBy("category", fractions={0: 0.1, 1: 0.05}).limit(100) ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值