惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog
I
InfoQ
Y
Y Combinator Blog
The Last Watchdog
The Last Watchdog
博客园_首页
The Cloudflare Blog
博客园 - 【当耐特】
Engineering at Meta
Engineering at Meta
罗磊的独立博客
月光博客
月光博客
V
V2EX
大猫的无限游戏
大猫的无限游戏
腾讯CDC
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
B
Blog RSS Feed
Webroot Blog
Webroot Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
Netflix TechBlog - Medium
量子位
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
C
CERT Recently Published Vulnerability Notes
人人都是产品经理
人人都是产品经理
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Cyberwarzone
Cyberwarzone
G
Google Developers Blog
H
Heimdal Security Blog
MyScale Blog
MyScale Blog
The Register - Security
The Register - Security
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
T
Tenable Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
O
OpenAI News
C
Check Point Blog
Forbes - Security
Forbes - Security
SecWiki News
SecWiki News
K
Kaspersky official blog
The GitHub Blog
The GitHub Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
F
Full Disclosure
阮一峰的网络日志
阮一峰的网络日志

博客园 - Aitozi

An Empirical Evaluation of Columnar Storage Formats Lance 写入链路:Merge Into、Compaction 与 Stable Row ID 从本地目录理解 Lance Dataset:Manifest、Fragment 与 Blob 论文解读:Lance 如何通过自适应结构编码提升列式存储随机访问 中国最大广告机器简史 学习Facebook,超越Meta|字节跳动 第3集 Paimon merge into 实现原理 Paimon Deletion Vector Flink Batch Hash Aggregate 理解 Paimon changelog producer 笔记工具 FlinkSQL类型系统 二叉堆原理与实现 SkipList原理与实现 Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores Paimon Compaction实现 Paimon读取流程 Paimon的写入流程 Calcite sql2rel 过程 用rust 写一个jar包 class冲突检测工具 rust 中 str 与 String; &str &String 好奇心: 保持对未知世界用不停息的热情 Apache hudi 核心功能点分析
Paimon lookup store 实现
Aitozi · 2024-10-29 · via 博客园 - Aitozi

Lookup Store 主要用于 Paimon 中的 Lookup Compaction 以及 Lookup join 的场景. 会将远程的列存文件在本地转化为 KV 查找的格式.

Hash

https://github.com/linkedin/PalDB

Sort

https://github.com/dain/leveldb
https://github.com/apache/paimon/pull/3770

Pasted image 20241029081723
整体文件结构:

Pasted image 20241029230800|182

相比于 Hash file 的优势

  • 一次写入, 避免了文件merge
  • 顺序写入, 保持原先的 key 的顺序, 后续如果按照 key 的顺序查找, 可提升缓存效率

SortLookupStoreWriter

SortLookupStoreWriter#put

put

@Override
public void put(byte[] key, byte[] value) throws IOException {
	dataBlockWriter.add(key, value);
	if (bloomFilter != null) {
		bloomFilter.addHash(MurmurHashUtils.hashBytes(key));
	}

	lastKey = key;

	// 当BlockWriter写入达到一定阈值, 默认是 cache-page-size=64kb.
	if (dataBlockWriter.memory() > blockSize) {
		flush();
	}

	recordCount++;
}

flush

private void flush() throws IOException {  
    if (dataBlockWriter.size() == 0) {  
        return;  
    }  
	// 将data block写入数据文件, 并记录对应的position和长度
    BlockHandle blockHandle = writeBlock(dataBlockWriter);  
    MemorySlice handleEncoding = writeBlockHandle(blockHandle);
    // 将BlockHandle 写入index writer, 这也通过是一个BlockWriter写的
    indexBlockWriter.add(lastKey, handleEncoding.copyBytes());  
}

writeBlock

private BlockHandle writeBlock(BlockWriter blockWriter) throws IOException {
	// close the block
	// 获取block的完整数组, 此时blockWriter中的数组并不会被释放, 而是会继续复用
	MemorySlice block = blockWriter.finish();

	totalUncompressedSize += block.length();

	// attempt to compress the block
	BlockCompressionType blockCompressionType = BlockCompressionType.NONE;
	if (blockCompressor != null) {
		int maxCompressedSize = blockCompressor.getMaxCompressedSize(block.length());
		byte[] compressed = allocateReuseBytes(maxCompressedSize + 5);
		int offset = encodeInt(compressed, 0, block.length());
		int compressedSize =
				offset
						+ blockCompressor.compress(
								block.getHeapMemory(),
								block.offset(),
								block.length(),
								compressed,
								offset);

		// Don't use the compressed data if compressed less than 12.5%,
		if (compressedSize < block.length() - (block.length() / 8)) {
			block = new MemorySlice(MemorySegment.wrap(compressed), 0, compressedSize);
			blockCompressionType = this.compressionType;
		}
	}

	totalCompressedSize += block.length();

	// create block trailer
	// 每一块block会有一个trailer, 记录压缩类型和crc32校验码
	BlockTrailer blockTrailer =
			new BlockTrailer(blockCompressionType, crc32c(block, blockCompressionType));
	MemorySlice trailer = BlockTrailer.writeBlockTrailer(blockTrailer);

	// create a handle to this block
	// BlockHandle 记录了每个block的其实position和长度
	BlockHandle blockHandle = new BlockHandle(position, block.length());

	// write data
	// 将数据追加写入磁盘文件
	writeSlice(block);

	// write trailer: 5 bytes
	// 写出trailer
	writeSlice(trailer);

	// clean up state
	blockWriter.reset();

	return blockHandle;
}

close

public LookupStoreFactory.Context close() throws IOException {
	// flush current data block
	flush();

	LOG.info("Number of record: {}", recordCount);

	// write bloom filter
	@Nullable BloomFilterHandle bloomFilterHandle = null;
	if (bloomFilter != null) {
		MemorySegment buffer = bloomFilter.getBuffer();
		bloomFilterHandle =
				new BloomFilterHandle(position, buffer.size(), bloomFilter.expectedEntries());
		writeSlice(MemorySlice.wrap(buffer));
		LOG.info("Bloom filter size: {} bytes", bloomFilter.getBuffer().size());
	}

	// write index block
	// 将index数据写出至文件
	BlockHandle indexBlockHandle = writeBlock(indexBlockWriter);

	// write footer
	// Footer 记录bloomfiler + index
	Footer footer = new Footer(bloomFilterHandle, indexBlockHandle);
	MemorySlice footerEncoding = Footer.writeFooter(footer);
	writeSlice(footerEncoding);

	// 最后关闭文件
	// close file
	fileOutputStream.close();

	LOG.info("totalUncompressedSize: {}", MemorySize.ofBytes(totalUncompressedSize));
	LOG.info("totalCompressedSize: {}", MemorySize.ofBytes(totalCompressedSize));
	return new SortContext(position);
}

BlockWriter

add

public void add(byte[] key, byte[] value) {
	int startPosition = block.size();
	// 写入key长度
	block.writeVarLenInt(key.length);
	// 写入key
	block.writeBytes(key);
	// 写入value长度
	block.writeVarLenInt(value.length);
	// 写入value
	block.writeBytes(value);
	int endPosition = block.size();

	// 使用一个int数组记录每个KV pair的起始位置作为索引
	positions.add(startPosition);
	// 是否对齐. 是否对齐取决于每个KV对的长度是否一样
	if (aligned) {
		int currentSize = endPosition - startPosition;
		if (alignedSize == 0) {
			alignedSize = currentSize;
		} else {
			aligned = alignedSize == currentSize;
		}
	}
}
  • 这里的 block 对应于一块可扩容的 MemorySegment, 也就是 byte[] , 当写入长度超过当前数组的长度时, 就会扩容

finish

public MemorySlice finish() throws IOException {
	if (positions.isEmpty()) {
		throw new IllegalStateException();
	}
	// 当通过BlockWriter写出的数据长度都是对齐的时, 就不需要记录各个Position的index了, 只需要记录一个对齐长度, 读取时自己可以计算.
	if (aligned) {
		block.writeInt(alignedSize);
	} else {
		for (int i = 0; i < positions.size(); i++) {
			block.writeInt(positions.get(i));
		}
		block.writeInt(positions.size());
	}
	block.writeByte(aligned ? ALIGNED.toByte() : UNALIGNED.toByte());
	return block.toSlice();
}

小结

整个文件的写出过程非常简单, 就是按 block 写出, 并且记录每个 block 的位置, 作为 index.

SortLookupStoreReader

读取的过程, 主要就是为了查找 key 是否存在, 以及对应的 value 或者对应的行号.

public byte[] lookup(byte[] key) throws IOException {
	// 先通过bloomfilter提前进行判断
	if (bloomFilter != null && !bloomFilter.testHash(MurmurHashUtils.hashBytes(key))) {
		return null;
	}

	MemorySlice keySlice = MemorySlice.wrap(key);
	// seek the index to the block containing the key
	indexBlockIterator.seekTo(keySlice);

	// if indexIterator does not have a next, it means the key does not exist in this iterator
	if (indexBlockIterator.hasNext()) {
		// seek the current iterator to the key
		// 根据从index block中读取到的key value的位置(BlockHandle), 读取对应的value block
		BlockIterator current = getNextBlock();
		// 在value的iterator中再次二分查找寻找对应block中是否存在match的key, 如果存在则返回对应的数据
		if (current.seekTo(keySlice)) {
			return current.next().getValue().copyBytes();
		}
	}
	return null;
}
  • 查找一次 key 会经历两次二分查找(index + value).

BlockReader

// 从block创建一个iterator
public BlockIterator iterator() {
	BlockAlignedType alignedType =
			BlockAlignedType.fromByte(block.readByte(block.length() - 1));
	int intValue = block.readInt(block.length() - 5);
	if (alignedType == ALIGNED) {
		return new AlignedIterator(block.slice(0, block.length() - 5), intValue, comparator);
	} else {
		int indexLength = intValue * 4;
		int indexOffset = block.length() - 5 - indexLength;
		MemorySlice data = block.slice(0, indexOffset);
		MemorySlice index = block.slice(indexOffset, indexLength);
		return new UnalignedIterator(data, index, comparator);
	}
}

SliceCompartor

这里面传入了 keyComparator, 用于进行 key 的比较. 用于在 index 中进行二分查找. 这里的比较并不是直接基于原始的数据, 而是基于 MemorySlice 进行排序.

比较的过程会将 key 的各个字段从 MemorySegment 中读取反序列化出来, cast 成 Comparable 进行比较.

public SliceComparator(RowType rowType) {
	int bitSetInBytes = calculateBitSetInBytes(rowType.getFieldCount());
	this.reader1 = new RowReader(bitSetInBytes);
	this.reader2 = new RowReader(bitSetInBytes);
	this.fieldReaders = new FieldReader[rowType.getFieldCount()];
	for (int i = 0; i < rowType.getFieldCount(); i++) {
		fieldReaders[i] = createFieldReader(rowType.getTypeAt(i));
	}
}

@Override
public int compare(MemorySlice slice1, MemorySlice slice2) {
	reader1.pointTo(slice1.segment(), slice1.offset());
	reader2.pointTo(slice2.segment(), slice2.offset());
	for (int i = 0; i < fieldReaders.length; i++) {
		boolean isNull1 = reader1.isNullAt(i);
		boolean isNull2 = reader2.isNullAt(i);
		if (!isNull1 || !isNull2) {
			if (isNull1) {
				return -1;
			} else if (isNull2) {
				return 1;
			} else {
				FieldReader fieldReader = fieldReaders[i];
				Object o1 = fieldReader.readField(reader1, i);
				Object o2 = fieldReader.readField(reader2, i);
				@SuppressWarnings({"unchecked", "rawtypes"})
				int comp = ((Comparable) o1).compareTo(o2);
				if (comp != 0) {
					return comp;
				}
			}
		}
	}
	return 0;
}

查找的实现就是二分查找的过程, 因为写入的 key 是有序写入的.

public boolean seekTo(MemorySlice targetKey) {
	int left = 0;
	int right = recordCount - 1;

	while (left <= right) {
		int mid = left + (right - left) / 2;

		// 对于aligned iterator, 就直接seek record * recordSize
		// 对于unaligned iterator, 就根据writer写入的索引表来跳转
		seekTo(mid);
		// 读取一条key value pair
		BlockEntry midEntry = readEntry();
		int compare = comparator.compare(midEntry.getKey(), targetKey);

		if (compare == 0) {
			polled = midEntry;
			return true;
		} else if (compare > 0) {
			polled = midEntry;
			right = mid - 1;
		} else {
			left = mid + 1;
		}
	}

	return false;
}

小结

查找过程

  • 先过一遍 bloom filter
  • index 索引查找对应 key 的 block handle
  • 根据第二步的 handle, 读取对应的 block, 在 block 中查找对应的 key value.