1.简介

1.1 介绍

Bloom filter是1970年引入的一种数据结构,在过去的十年中,由于它为网络主机之间的组成员信息传输提供了带宽效率,因此被网络研究界采用。发送者将信息编码成一个比特向量,即Bloom过滤器,它比传统的表示形式更紧凑。Bloom本身的计算和空间成本在元素数量上是线性的。接收器使用过滤器来测试各种元素是否是集合的成员。虽然过滤器偶尔会返回假阳性,但它永远不会返回假阴性。Hbase正是利用这一点,在数据查询时判断数据是否在当前Hfile中,可以有效减少数据的读取,减少IO操作。

1.2 Bloom filter最优的大小计算

Bloom过滤器对插入到其中的元素的数量非常敏感。对于HBase来说,条目的数量取决于存储在列中的数据的大小。当前默认区域大小为256MB,因此条目计数~=256MB/(列的平均值大小)。尽管有这个经验法则,但是由于压缩,我们并没有有效的方法来计算压缩后的条目计数。因此,通常使用动态bloom过滤器来添加额外的空间,而不是允许错误率增长。

Bloom filter最优的大小计算公示为:bloom size m = -(n * ln(err) / (ln(2)^2) ~= n * ln(err) / ln(0.6185)

  • m表示Bloom filter中的位数(bitSize)
  • n表示插入bloomfilter中的元素数(maxKeys)
  • k表示使用的哈希函数数(nbHash)
  • e表示Bloom所需的误报率(err)

但且仅当k=m/n ln(2)时,误报概率最小。

Hbase中Bloom filter的设置是在创建列族时通过setBloomFilterType方法设定,Hbase支持ROW、ROWCOL、ROWPREFIX_FIXED_LENGTH三种类型的Bloom filter,创建列族时默认设置为ROW,对所有插入数据的rowkey写入到Bloom filter中。

2.hbase中布隆过滤器的实现

2.1 Bloom filter接口实现

Hbase的布隆过滤器由父接口BloomFilterBase类定义,包含2个子继承接口BloomFilter和BloomFilterWriter。

BloomFilter负责读取、判断,BloomFilterWriter负责将数据写入布隆过滤器。

2.1.1 read类

BloomFilter负责数据的读取判断其中定义了三个方法

//检查所定义的keyCell是否包含
boolean contains(Cell keyCell, ByteBuff bloom, BloomType type);
boolean contains(byte[] buf, int offset, int length, ByteBuff bloom);

//是否允许Bloom filter自动load数据,默认实现为true
boolean supportsAutoLoading();

BloomFilter最终的实现类是CompoundBloomFilter类,CompoundBloomFilter的核心方法是contains方法。

public boolean contains(Cell keyCell, ByteBuff bloom, BloomType type) {
    
    //如果根索引不包含keyCell,返回false,根索引在Hfile创建时构建,不是对所有rowkey
    int block = index.rootBlockContainingKey(keyCell);
    if (block < 0) {
      return false; // This key is not in the file.
    }
    boolean result;

    //获得Bloom的Block
    HFileBlock bloomBlock = getBloomBlock(block);
    try {
      ByteBuff bloomBuf = bloomBlock.getBufferReadOnly();
     
    //通过BloomFilterUtil的contains方法判断

      result = BloomFilterUtil.contains(keyCell, bloomBuf, bloomBlock.headerSize(),
          bloomBlock.getUncompressedSizeWithoutHeader(), hash, hashCount, type);
    } finally {
      // After the use return back the block if it was served from a cache.
      reader.returnBlock(bloomBlock);
    }
    if (numPositivesPerChunk != null && result) {
      // Update statistics. Only used in unit tests.
      ++numPositivesPerChunk[block];
    }
    return result;
  }

BloomFilterUtil.contains方法中,通过不同的 BloomType,构建不同的BloomHashKey,然后读取bloomBuf中的bitvals,计算cell

对应类型的HashKey,判断在bitvals中是否为1.

public static boolean contains(Cell cell, ByteBuff bloomBuf, int bloomOffset, int bloomSize,
      Hash hash, int hashCount, BloomType type) {
    HashKey<Cell> hashKey = type == BloomType.ROWCOL ? new RowColBloomHashKey(cell)
        : new RowBloomHashKey(cell);
  
  //最终的判断方法,实现还是比较简单
    return contains(bloomBuf, bloomOffset, bloomSize, hash, hashCount, hashKey);
  }

2.1.2write类

 BloomFilterWriter类定义了一些写入方法

//在数据写入磁盘之前,压缩Bloom filter  
void compactBloom();

//获得一个meta data 的Writer,写入Bloom TYpe 、数据大小等
Writable getMetaWriter();

//获取一个 Bloom bits 的Writer
Writable getDataWriter();

// previous cell written
Cell getPrevCell();
BloomFilterWriter的最终实现类为CompoundBloomFilterWriter。子类CompoundBloomFilterWriter的核心方法是append方法,负责将添加的数据写入到BloomFilter
@Override
  public void append(Cell cell) throws IOException {
    if (cell == null)
      throw new NullPointerException();

    enqueueReadyChunk(false);

    if (chunk == null) {
      if (firstKeyInChunk != null) {
        throw new IllegalStateException("First key in chunk already set: "
            + Bytes.toStringBinary(firstKeyInChunk));
      }

     //第一添加时需要allocateNewChunk,Chunk动态添加,完成hash等
      // This will be done only once per chunk
      if (bloomType == BloomType.ROWCOL) {
        firstKeyInChunk =
            PrivateCellUtil
                .getCellKeySerializedAsKeyValueKey(PrivateCellUtil.createFirstOnRowCol(cell));
      } else {
        firstKeyInChunk = CellUtil.copyRow(cell);
      }
      allocateNewChunk();
    }

    //Chunk 实现具体的hash计算,和bit置位
    chunk.add(cell);
    this.prevCell = cell;
    ++totalKeyCount;
  }

BloomFilterChunk继承自BloomFilterBase,实现了BloomFilter的读写,其中add方法实现写入

public void add(Cell cell) {
    /*
     * For faster hashing, use combinatorial generation
     * http://www.eecs.harvard.edu/~kirsch/pubs/bbbf/esa06.pdf
     */
    int hash1;
    int hash2;
    HashKey<Cell> hashKey;
    
// 计算2次hash 写入位

  if (this.bloomType == BloomType.ROWCOL) {
      hashKey = new RowColBloomHashKey(cell);
      hash1 = this.hash.hash(hashKey, 0);
      hash2 = this.hash.hash(hashKey, hash1);
    } else {
      hashKey = new RowBloomHashKey(cell);
      hash1 = this.hash.hash(hashKey, 0);
      hash2 = this.hash.hash(hashKey, hash1);
    }
    setHashLoc(hash1, hash2);
  }

get方法完成数据查询,和之前BloomFilterUtil.contains方法一致

static boolean get(int pos, ByteBuffer bloomBuf, int bloomOffset) {
    
    //实现位查找
    int bytePos = pos >> 3; //pos / 8
    int bitPos = pos & 0x7; //pos % 8
    // TODO access this via Util API which can do Unsafe access if possible(?)
    byte curByte = bloomBuf.get(bloomOffset + bytePos);
    curByte &= BloomFilterUtil.bitvals[bitPos];
    return (curByte != 0);
  }

3.布隆过滤器的使用

对于之前创建的布隆过滤器的使用,hbse中体现在两个地方,一个是构建scannner时,判断scanner的是否包含所需要的数据列或者列族.

在构建StoreFileScanner时,会通过shouldUseScanner方法判断,时都用到当前Scanner,其中用到了reader.passesBloomFilter的方法。
public boolean shouldUseScanner(Scan scan, HStore store, long oldestUnexpiredTS) {
    // if the file has no entries, no need to validate or create a scanner.
    byte[] cf = store.getColumnFamilyDescriptor().getName();
    TimeRange timeRange = scan.getColumnFamilyTimeRange().get(cf);
    if (timeRange == null) {
      timeRange = scan.getTimeRange();
    }

   //从时间范围、startkey和endkey范围、bloomfilter判断
    return reader.passesTimerangeFilter(timeRange, oldestUnexpiredTS) && reader
        .passesKeyRangeFilter(scan) && reader.passesBloomFilter(scan, scan.getFamilyMap().get(cf));
  }
StoreFileReader在创建StoreFileScanner的时候创建,主要用来读取hfile文件。 passesBloomFilter方法当前Hfile的bloomFilter的类型,构建具体的bloomFilter。bloomFilter的类型是创建表时,列族中定义的。
boolean passesBloomFilter(Scan scan, final SortedSet<byte[]> columns) {
  byte[] row = scan.getStartRow();
  switch (this.bloomFilterType) {
    case ROW:
      if (!scan.isGetScan()) {
        return true;
      }
      return passesGeneralRowBloomFilter(row, 0, row.length);

    case ROWCOL:
      if (!scan.isGetScan()) {
        return true;
      }
      if (columns != null && columns.size() == 1) {
        byte[] column = columns.first();
        // create the required fake key
        Cell kvKey = PrivateCellUtil.createFirstOnRow(row, HConstants.EMPTY_BYTE_ARRAY, column);
        return passesGeneralRowColBloomFilter(kvKey);
      }

      // For multi-column queries the Bloom filter is checked from the
      // seekExact operation.
      return true;
    case ROWPREFIX_FIXED_LENGTH:
      return passesGeneralRowPrefixBloomFilter(scan);
    default:
      return true;
  }
}

 passesGeneralRowBloomFilter方法中this.generalBloomFilter是创建reader时构建的BloomFilter。

private boolean passesGeneralRowBloomFilter(byte[] row, int rowOffset, int rowLen) {
    BloomFilter bloomFilter = this.generalBloomFilter;
    if (bloomFilter == null) {
      return true;
    }

    // Used in ROW bloom
    byte[] key = null;
    if (rowOffset != 0 || rowLen != row.length) {
      throw new AssertionError(
          "For row-only Bloom filters the row must occupy the whole array");
    }
    key = row;

   //判断row是否在本hfile中
    return checkGeneralBloomFilter(key, null, bloomFilter);
  }
checkGeneralBloomFilter方法中调用contains完成最终的判断。