HFile写入Cell浅析(一)

本文详细介绍了HBase中HFile的写入过程,包括从MemStore flush开始,到最终将Cell数据写入到HFile的具体步骤。重点关注了StoreFile.Writer、HFile.Writer等组件的作用及其实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        在HBase中,无论是MemStore的flush,还是HFile的Compact,都会涉及到新HFile的产生,那么HFile是如何产生的呢?我们又是如何将数据Cell写入到HFile中的呢?本文,我们将会大家简单介绍下HFile写入Cell的主体流程。

        在《HFile文件格式》一文中,我们简单给大家介绍了HFile文件内容的组织形式,理解这篇文章,将会对理解本文有所帮助,请读者自行阅读。

        HFile文件Cell的写入,发起的一个地方,就是MemStore flush时,StoreFlusher的performFlush()方法,如下:

  /**
   * Performs memstore flush, writing data from scanner into sink.
   * 执行memstore的刷新,将数据从scanner写入到sink
   * 
   * @param scanner Scanner to get data from.
   * @param sink Sink to write data to. Could be StoreFile.Writer.
   * @param smallestReadPoint Smallest read point used for the flush.
   */
  protected void performFlush(InternalScanner scanner,
      Compactor.CellSink sink, long smallestReadPoint) throws IOException {
    int compactionKVMax =
      conf.getInt(HConstants.COMPACTION_KV_MAX, HConstants.COMPACTION_KV_MAX_DEFAULT);
    List<Cell> kvs = new ArrayList<Cell>();
    boolean hasMore;
    do {
      hasMore = scanner.next(kvs, compactionKVMax);
      if (!kvs.isEmpty()) {
    	// 循环Cell列表,调用Compactor.CellSink的sink方法,append数据到磁盘
        for (Cell c : kvs) {
          // If we know that this KV is going to be included always, then let us
          // set its memstoreTS to 0. This will help us save space when writing to
          // disk.
        	// 如果我们知道这个KV是包括总,然后让我们设置它memstoreTS为0。这将帮助我们节省空间在写入磁盘。
          sink.append(c);
        }
        kvs.clear();
      }
    } while (hasMore);
  }
        它会循环的将Cell写入Compactor.CellSink类型的sink,那么这个sink是什么呢?在performFlush()方法的上层调用者DefaultStoreFlusher的flushSnapshot()方法中,首先会调用HStore的createWriterInTmp()方法生成一个StoreFile.Writer实例writer,然后将这个writer作为参数sink传入performFlush()方法,如下:

        // Write the map out to the disk
        // 创建Writer
        writer = store.createWriterInTmp(
            cellsCount, store.getFamily().getCompression(), false, true, true);
        writer.setTimeRangeTracker(snapshot.getTimeRangeTracker());
        IOException e = null;
        try {
          performFlush(scanner, writer, smallestReadPoint);
        } catch (IOException ioe) {
          e = ioe;
          // throw the exception out
          throw ioe;
        } finally {
          if (e != null) {
            writer.close();
          } else {
            finalizeWriter(writer, cacheFlushId, status);
          }
        }
        我们再看这个StoreFile.Writer类型的writer是如何生成的,进入HStore的createWriterInTmp()方法,代码如下:

    // 创建StoreFile的StoreFile,需要使用上述信息,比如文件系统、文件路径、合并器、最大keyvalue数目、有利节点等
    StoreFile.Writer w = new StoreFile.WriterBuilder(conf, writerCacheConf,
        this.getFileSystem())// 文件系统
            .withFilePath(fs.createTempName())// 文件路径
            .withComparator(comparator)// 合并器
            .withBloomType(family.getBloomFilterType())
            .withMaxKeyCount(maxKeyCount)// 最大keyvalue数目
            .withFavoredNodes(favoredNodes)// 有利节点
            .withFileContext(hFileContext)// HFile上下文信息
            .build();
        而在StoreFile.WriterBuilder的build()方法最后,会new一个StoreFile.Writer实例,在其构造方法中,会生成一个HFile.Writer实例writer,如下:

      writer = HFile.getWriterFactory(conf, cacheConf)
          .withPath(fs, path)
          .withComparator(comparator)
          .withFavoredNodes(favoredNodes)
          .withFileContext(fileContext)
          .create();
        而这个HFile.Writer实例writer则是通过HFile中WriterFactory获取的,如下:

  /**
   * Returns the factory to be used to create {@link HFile} writers
   */
  public static final WriterFactory getWriterFactory(Configuration conf,
      CacheConfig cacheConf) {
    int version = getFormatVersion(conf);
    switch (version) {
    case 2:
      return new HFileWriterV2.WriterFactoryV2(conf, cacheConf);
    case 3:
      return new HFileWriterV3.WriterFactoryV3(conf, cacheConf);
    default:
      throw new IllegalArgumentException("Cannot create writer for HFile " +
          "format version " + version);
    }
  }
        HBase中包含两种类型的WriterFactoryV2,分别为HFileWriterV2.WriterFactoryV2和HFileWriterV3.WriterFactoryV3,今天我们以HFileWriterV2.WriterFactoryV2为例,其creaet的HFile.Writer实例其实为HFileWriterV2类型,如下:

    @Override
    public Writer createWriter(FileSystem fs, Path path, 
        FSDataOutputStream ostream,
        KVComparator comparator, HFileContext context) throws IOException {
      context.setIncludesTags(false);// HFile V2 does not deal with tags at all!
      return new HFileWriterV2(conf, cacheConf, fs, path, ostream, 
          comparator, context);
      }
    }
        那么,最开始的sink调用append循环写入Cell,实际上最终调用的是HFileWriterV2的append()方法,如下:

    public void append(final Cell cell) throws IOException {
      appendGeneralBloomfilter(cell);
      appendDeleteFamilyBloomFilter(cell);
      writer.append(cell);// 调用HFile的Writer的append()方法,将数据写入文件
      trackTimestamps(cell);// 记录本次Put操作的时间戳
    }
        这个writer的实例化就是我们上面讲到的HFileWriterV2的实例化。下面,我们重点看下HFileWriterV2的append()方法,如下:

  /**
   * Add key/value to file. Keys must be added in an order that agrees with the
   * Comparator passed on construction.
   *
   * @param cell Cell to add. Cannot be empty nor null.
   * @throws IOException
   */
  @Override
  public void append(final Cell cell) throws IOException {
    
	// 从Cell中获取数据value、偏移量voffset、长度vlength
	byte[] value = cell.getValueArray();
    int voffset = cell.getValueOffset();
    int vlength = cell.getValueLength();
    
    // checkKey uses comparator to check we are writing in order.
    // 检测给定Cell,确保key是有序的,dupKey为true说明key没有换,false说明key已更改
    boolean dupKey = checkKey(cell);
    
    // 检测value,确保value不为null
    checkValue(value, voffset, vlength);
    
    if (!dupKey) {// 换key时才检测block边界
      checkBlockBoundary();
    }

    if (!fsBlockWriter.isWriting()) {
      // 申请新块
      newBlock();
    }

    // 写入Cell
    fsBlockWriter.write(cell);

    // 累加Key长度totalKeyLength和Value长度totalValueLength
    totalKeyLength += CellUtil.estimatedSerializedSizeOfKey(cell);
    totalValueLength += vlength;

    // Are we the first key in this block?
    // 标记该数据块中写入的第一个key,firstCellInBlock
    if (firstCellInBlock == null) {
      // If cell is big, block will be closed and this firstCellInBlock reference will only last
      // a short while.
      firstCellInBlock = cell;
    }

    // TODO: What if cell is 10MB and we write infrequently?  We'll hold on to the cell here
    // indefinetly?
    // 标记上次写入Cell,即lastCell赋值为当前cell
    lastCell = cell;
    
    // 条目计数加1
    entryCount++;
    
    // 更新maxMemstoreTS
    this.maxMemstoreTS = Math.max(this.maxMemstoreTS, cell.getSequenceId());
  }
        逻辑很简单,大体如下:

        1、首先从Cell中获取数据value、偏移量voffset、长度vlength;

        2、然后调用checkKey()方法,检测给定Cell,确保key是有序的,dupKey为true说明key没有换,false说明key已更改;

        3、接着调用checkValue()方法,检测value,确保value不为null;

        4、换key时才检测block边界,此时调用的是checkBlockBoundary()方法;

        5、如果需要申请新的数据块的话,调用newBlock()方法申请新的数据块,判断的依据是fsBlockWriter的isWriting()返回为false,表明文件系统数据块写入者暂停写入;

        6、调用fsBlockWriter的write()方法写入Cell;

        7、累加Key长度totalKeyLength和Value长度totalValueLength;

        8、如果需要,标记该数据块中写入的第一个key,firstCellInBlock;

        9、标记上次写入Cell,即lastCell赋值为当前cell;

        10、条目计数entryCount加1;

        11、更新maxMemstoreTS。

        检测block边界的checkBlockBoundary()方法如下:

  /**
   * At a block boundary, write all the inline blocks and opens new block.
   *
   * @throws IOException
   */
  protected void checkBlockBoundary() throws IOException {
    
	// 如果fsBlockWriter已写入大小小于hFileContext中定义的块大小,直接返回
	if (fsBlockWriter.blockSizeWritten() < hFileContext.getBlocksize())
      return;

	// 结束上一个块
    finishBlock();
    
    // 写入InlineBlocks
    writeInlineBlocks(false);
    
    // 申请新块
    newBlock();
  }
        它会首先判断fsBlockWriter已写入大小,如果fsBlockWriter已写入大小小于hFileContext中定义的块大小,说明block未越界,直接返回,否则的话,说明block已经达到或者超过设定的阈值,此时,需要做以下处理:

        1、调用finishBlock()方法,结束上一个block;

        2、调用writeInlineBlocks()方法,写入InlineBlocks;

        3、调用newBlock()方法,申请新块。

        未完待续!










<think>好的,我现在需要回答用户关于如何在C语言中次性写入整个文件的问题。用户之前已经询问过如何读取整个文件,现在转向写入操作。首先,我得回忆C语言中文件操作的相关函数,比如fopen、fwrite、fclose等。 根据用户提供的引用内容,他们之前讨论过使用标准库函数如fread、fgets,以及Windows API如WriteFile。但用户现在明确要求次性写入整个文件,所以我应该优先考虑标准C库的方法,因为跨平台更通用。不过,可能需要同时提供标准库和Windows API两种方法,因为引用里提到了WriteFile。 首先,标准库的方法。使用fwrite可以次性写入整个缓冲区。需要确保文件以正确的模式打开,比如"wb"用于二进制写入。用户可能有个内存中的缓冲区,比如字符串或二进制数据,需要整个写入文件。示例代码需要包含打开文件、写入、错误处理、关闭文件这些步骤。 然后,Windows API的方法,使用CreateFile和WriteFile,这可能在用户之前的引用中出现过。但用户现在的问题可能更倾向于标准库,不过为了全面性,可以两种都提供。 还要注意错误处理,比如检查文件是否成功打开,写入是否成功。用户示例中的读取代码有错误检查,写入时也应该有类似的检查。例如,fwrite的返回值应该等于要写入的元素数量,否则可能有错误。 另外,用户提到“次性写入整个文件”,这意味着不需要分块写入,而是将整个数据块次性写入。因此,数据应该已经在内存中准备好,比如个字符串或数组。示例代码可能需要构造这样的数据,或者从用户输入获取,但为了简单起见,可能直接使用个静态字符串。 考虑到用户之前的回答中提到了文本模式和二进制模式的区别,这里需要明确使用哪种模式。例如,如果写入文本数据,用"w";如果是二进制数据,用"wb"。 现在需要组织代码结构,确保包含必要的头文件,main函数结构清晰,错误处理得当。例如,使用fopen后检查返回值,fwrite后检查写入的字节数,最后关闭文件。 可能还需要解释两种方法的优缺点,比如标准库方法跨平台,而Windows API特定于Windows但可能更底层。同时,注意缓冲区的大小计算,特别是处理字符串时末尾的NULL字符是否需要写入。 最后,生成相关问题,可能涉及性能优化、错误处理、不同操作系统的实现差异等。要确保问题与用户当前主题相关,并引导进步学习。</think>以下是使用C语言实现次性写入整个文件的两种常用方法及详细说明: --- ### 方法1:标准C库函数(跨平台) ```c #include <stdio.h> #include <stdlib.h> #include <string.h> int write_entire_file(const char* filename, const void* data, size_t data_size) { FILE* fp = fopen(filename, "wb"); // 二进制写入模式 if (!fp) { perror("文件打开失败"); return 0; } size_t written = fwrite(data, 1, data_size, fp); fclose(fp); // 必须关闭以刷新缓冲区 return (written == data_size) ? 1 : 0; } int main() { const char* content = "这是要次性写入文件的完整内容\n第二行数据"; size_t content_size = strlen(content); // 不包含字符串末尾的\0 if (write_entire_file("output.txt", content, content_size)) { printf("文件写入成功"); } else { printf("文件写入失败"); } return 0; } ``` --- ### 方法2:Windows API实现(仅限Windows) ```c #include <windows.h> #include <stdio.h> int win_write_entire_file(const char* filename, const char* data, DWORD data_size) { HANDLE hFile = CreateFileA( filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, // 总是新建文件 FILE_ATTRIBUTE_NORMAL, NULL ); if (hFile == INVALID_HANDLE_VALUE) { printf("文件创建失败 (错误码:%lu)\n", GetLastError()); return 0; } DWORD written; BOOL success = WriteFile( hFile, data, data_size, &written, NULL ); CloseHandle(hFile); return (success && (written == data_size)) ? 1 : 0; } int main() { const char* text = "通过Windows API写入的完整文件内容"; DWORD size = strlen(text); // 计算数据长度 if (win_write_entire_file("win_output.txt", text, size)) { printf("Windows API写入成功"); } return 0; } ``` --- ### 关键点解析 1. **标准库方法**(`fwrite`): - 使用二进制模式`"wb"`避免换行符转换[^1] - 通过`fwrite`实现原子写入操作 - `fclose`操作会强制刷新系统缓冲区[^2] - 支持文本/二进制数据,**跨平台兼容** 2. **Windows API方法**(`WriteFile`): - 直接系统调用,**性能更高** - `CREATE_ALWAYS`会覆盖已有文件 - 需要处理Windows错误码`GetLastError()` - 适合需要精细控制写入过程的场景[^3] 3. **通用注意事项**: - 二进制数据需精确计算`data_size` - 文本数据建议明确指定编码(如UTF-8) - 大文件(>4GB)需使用`fwrite`的64位版本`_fwrite_nolock` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值