Paimon中BloomFilter在key查找中的应用

背景

本文基于 Paimon 0.9.0
之前也是一直想找一些例子以及场景来看 实战中 怎么构造BloomFilter以及怎么在文件层级进行过滤。刚好在Paimon中有这么个例子,这次可以全面来分析一下

分析

BloomFilter的构造

直接定位到KeyValueFileStoreWrite.createLookupLevels方法:

 return new LookupLevels<>(
                levels,
                keyComparatorSupplier.get(),
                keyType,
                valueProcessor,
                readerFactory::createRecordReader,
                file ->
                        ioManager
                                .createChannel(
                                        localFilePrefix(partitionType, partition, bucket, file))
                                .getPathFile(),
                lookupStoreFactory,
                bfGenerator(options),
                lookupFileCache);

其中bfGenerator这里就是BloomFilter的构造,代码如下:

static Function<Long, BloomFilter.Builder> bfGenerator(Options options) {
        Function<Long, BloomFilter.Builder> bfGenerator = rowCount -> null;
        if (options.get(CoreOptions.LOOKUP_CACHE_BLOOM_FILTER_ENABLED)) {
            double bfFpp = options.get(CoreOptions.LOOKUP_CACHE_BLOOM_FILTER_FPP);
            bfGenerator =
                    rowCount -> {
                        if (rowCount > 0) {
                            return BloomFilter.builder(rowCount, bfFpp);
                        }
                        return null;
                    };
        }
        return bfGenerator;
    }

...

public static Builder builder(long expectedRow, double fpp) {
       int numBytes = (int) Math.ceil(BloomFilter.optimalNumOfBits(expectedRow, fpp) / 8D);
       Preconditions.checkArgument(
               numBytes > 0,
               "The optimal bits should > 0. expectedRow: %s, fpp: %s",
               expectedRow,
               fpp);
       return new Builder(MemorySegment.wrap(new byte[numBytes]), expectedRow);
   }
...

Builder(MemorySegment buffer, long expectedEntries) {
            this.buffer = buffer;
            this.filter = new BloomFilter(expectedEntries, buffer.size());
            filter.setMemorySegment(buffer, 0);
            this.expectedEntries = expectedEntries;
        }

对于BloomFilter的来说,会涉及到两个方面,一个是用多少位来存储,一个是用多少个hash函数,这里都是有好多

  • 位数组大小的计算
    位数组的计算是根据预期的元素数量和允许的最大误判率来的,有一个计算公式:m = - (n * ln(p)) / (ln(2)^2)
    其中 n 为 预期的元素个数,
    p 为 误判率,也就是布隆过滤器将非集合元素错误地判断为集合元素的概率
    m 为位数
    n对应到Paimon中为文件的行数,
    p对应到到Paimon中为lookup.cache.bloom.filter.fpp,默认是0.05.
    BloomFilter.optimalNumOfBits(expectedRow, fpp)的计算也是根据这个来的
  • hash函数的个数的确认
    哈希函数个数的计算需要综合考虑误判率和空间利用率,在给定布隆过滤器大小和预计插入元素个数的情况下,可以使用以下公式来估算:
    k = (m / n) * ln2
    其中 m 为 位数
    n 为 插入的元素的个数
    k 为hash函数的个数
    m 对应到Paimon中就是 上述位数组大小的计算中m的指
    n 对应到Paimon中为文件的行数
    optimalNumOfHashFunctions 这个计算公式也就是根据这个来的

注意到Paimon在构造Builder的时候,用到了MemorySegment,也就是(MemorySegment.wrap(new byte[numBytes])
这里说明一下MemorySegment 这个是利用UNSAFE来操作字节数组的工具类,原理上和java.util.BitSet一样,都可以来操作对应的管理单元(BitSet对应的是Long类型,这里对应的是Byte类型),具体的可以参考深入理解SPARK SQL 中HashAggregateExec和ObjectHashAggregateExec以及UnsafeRow
只不过如果用MemorySegment可以更好的管理(比如申请和释放),且后续的位操作都是基于memorySegment来的,比如说putByte和getByte.

BloomFilter的使用

BloomFiltet的数据写入

直接定位到LookupLevels.createLookupFile 方法:

 LookupStoreWriter kvWriter =
                lookupStoreFactory.createWriter(localFile, bfGenerator.apply(file.rowCount()));
        LookupStoreFactory.Context context;
        try (RecordReader<KeyValue> reader = fileReaderFactory.apply(file)) {
            KeyValue kv;
            if (valueProcessor.withPosition()) {
                FileRecordIterator<KeyValue> batch;
                while ((batch = (FileRecordIterator<KeyValue>) reader.readBatch()) != null) {
                    while ((kv = batch.next()) != null) {
                        byte[] keyBytes = keySerializer.serializeToBytes(kv.key());
                        byte[] valueBytes =
                                valueProcessor.persistToDisk(kv, batch.returnedPosition());
                        kvWriter.put(keyBytes, valueBytes);
                    }
                    batch.releaseBatch();
                }

kvWriter 这里构造了HashLookupStoreWriter,对于每一个key和value都会调用该put方法(这样就把对应的key值传给了BloomFilter):

...
if (bloomFilter != null) {
            bloomFilter.addHash(MurmurHashUtils.hashBytes(key));
        }

而而方法最后都会把对应的Key值写入到bloomFilter中,这里调用的方法如下:

BloomFilter.java
 public void addHash(int hash1) {
        int hash2 = hash1 >>> 16;

        for (int i = 1; i <= numHashFunctions; i++) {
            int combinedHash = hash1 + (i * hash2);
            // hashcode should be positive, flip all the bits if it's negative
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }
            int pos = combinedHash % bitSet.bitSize();
            bitSet.set(pos);
        }
    }
    ...
org.apache.paimon.utils.BitSet.java
 public void set(int index) {
        checkArgument(index < bitLength && index >= 0);

        int byteIndex = index >>> 3;
        byte current = memorySegment.get(offset + byteIndex);
        current |= (1 << (index & BYTE_INDEX_MASK));
        memorySegment.put(offset + byteIndex, current);
    }
  • int byteIndex = index >>> 3; 找到对应的字节数组对应的字节,也就是index/8
  • current |= (1 << (index & BYTE_INDEX_MASK));是找到在对应的8位byte字节中的哪一位,也就是index%8
  • memorySegment.put(offset + byteIndex, current);底层会调用UNSAFE.putByte设置对应的值
BloomFiltet的数据读取

直接定位到LookupFile.get方法,该方法会最终调用HashLookupStoreReader.lookup:

  int keyLength = key.length;
        if (keyLength >= slots.length || keyCounts[keyLength] == 0) {
            return null;
        }

        int hashcode = MurmurHashUtils.hashBytes(key);
        if (bloomFilter != null && !bloomFilter.testHash(hashcode)) {
            return null;
        }

这里会调用bloomFilter.testHash 方法(如果不存在则返回null):

BloomFilter.java
 public boolean testHash(int hash1) {
        int hash2 = hash1 >>> 16;

        for (int i = 1; i <= numHashFunctions; i++) {
            int combinedHash = hash1 + (i * hash2);
            // hashcode should be positive, flip all the bits if it's negative
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }
            int pos = combinedHash % bitSet.bitSize();
            if (!bitSet.get(pos)) {
                return false;
            }
        }
        return true;
    }
org.apache.paimon.utils.BitSet.java
 public boolean get(int index) {
        checkArgument(index < bitLength && index >= 0);
        int byteIndex = index >>> 3;
        byte current = memorySegment.get(offset + byteIndex);
        return (current & (1 << (index & BYTE_INDEX_MASK))) != 0;
    }

这里的操作和之前的一样,只不过拿出来对应的Bit位,看是否为0。
这样,对于不存在的key就可以很快的跳过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值