- 大数据
- 数据库
- 后端
【免费下载链接】presto
The official home of the Presto distributed SQL query engine for big data
导读:本文围绕 Presto Release 0.91 中唯一的一条 General Changes——"清除
LazyBlockLoader引用以提前释放内存"——展开。它对应着 Presto 列式执行引擎中一项核心内存优化:LazyBlock(懒加载块)在被真正读取后才物化数据,而加载完成后及时断开对 loader 的强引用,可让底层 PageSource 持有的文件句柄、缓冲区等资源尽早被 GC 回收。读完本文,你将理解LazyBlock/LazyBlockLoader的完整实现机制、在各连接器(Hive ORC/Parquet/RCFile、Druid 等)中的落地方式,以及 ScanFilterAndProjectOperator 如何通过"记录型 loader"精确统计懒加载块的输入字节数,并了解对应测试用例的验证思路。
版本背景:一条变更与一个必须记住的警告
在 Presto 官方发布说明 release-0.91.rst 中,0.91 版本的发布记录极其克制,只包含一条变更,并附带一条醒目的警告:
Warning:此版本存在内存泄漏(memory leak),不应在生产环境使用。
General Changes:在加载完成后清除
LazyBlockLoader引用(ClearLazyBlockLoaderreference after load to free memory earlier),以便更早释放内存。
这条变更的落点在LazyBlockLoader:当LazyBlock完成数据加载后,立即置空其内部对 loader 的引用。这样做有两个直接收益:
- 更早释放内存:loader 实现类往往持有 PageSource、文件读取器、位置数组等重资源引用,尽早断开引用可以让这些对象随
LazyBlock一起被 GC 回收; - 更早触发底层资源释放:例如 ORC/Parquet 的流读取器在
close()后才能真正归还文件缓冲区。
需要特别强调的是:0.91 版本本身因内存泄漏被官方明确警告不可使用,这条变更属于"修一处、漏一处"的历史版本中的一页。它更值得被当作理解 Presto 懒加载块内存模型的一把钥匙,而不是作为可部署版本被引用。下文将以当前仓库中的源码(即修复后的演进形态)为准进行讲解。
LazyBlock 与 LazyBlockLoader:延迟物化的最小单元
Presto 以Page(若干Block的列式集合)为数据交换单位。在扫描大表时,若把每列的原始数据都立即解码成Block,会占用大量内存。LazyBlock的职责就是"占位":它只记录positionCount(行数),真正的数据块Block block在首次被访问时才由 loader 填充。
接口定义
LazyBlockLoader.java 是整个机制的契约,它极其精简:
package com.facebook.presto.common.block; public interface LazyBlockLoader<T extends Block> { void load(T block); }实现方通过load(T block)把真实数据写入传入的LazyBlock(通常调用lazyBlock.setBlock(...))。
LazyBlock 的核心状态机
LazyBlock.java 内部持有三个关键字段:
positionCount:预先可知的行数;loader:延迟加载器,构造时强制非空(requireNonNull(loader, "loader is null"));block:真实数据块,初始为null,加载后非空。
所有数据访问方法(getLong、getSlice、isNull、getSizeInBytes等)的第一步都是调用私有方法assureLoaded():
private void assureLoaded() { if (block != null) { return; } loader.load(this); if (block == null) { throw new IllegalArgumentException("Lazy block loader did not load this block"); } // clear reference to loader to free resources, since load was successful loader = null; }这段代码正是 0.91 变更的最终形态:
- 首次访问时调用
loader.load(this)触发物化; - 校验 loader 确实调用了
setBlock,否则抛出IllegalArgumentException防止静默丢失数据; - 加载成功后立即将
loader置空——注释原话是"clear reference to loader to free resources, since load was successful",即清除 loader 引用以便释放资源。这正是 Release 0.91 那条变更的直接实现。
此外LazyBlock提供isLoaded()(block != null)和getLoadedBlock()供上层判断是否已物化;setBlock只允许调用一次,重复设置会抛出IllegalStateException("block already set"),保证了状态机的单向性。
为什么 loader 引用必须被清除:引用链与 GC 分析
从内存模型看,loader 清除的意义远超"少一个引用"这么简单。以 ORC 为例,loader 实现(见下文OrcBlockLoader)通常持有:
- 对应的
SelectiveStreamReader(内部持有 ORC 数据源、解压缓冲); - 待读取的
positions数组; - 可选的类型转换函数
coercer。
只要LazyBlock.loader字段还指向这些对象,即便 Page 已经离开算子树、block已被上层使用,整套读取设施也无法被回收。0.91 的修复把引用生命周期收敛为"加载完成即失效",让 loader 及其持有的文件资源可以在物化后的第一个 GC 周期就被回收,这正是"free memory earlier"的底层含义。
同样的引用释放手法也出现在算子层:在 ScanFilterAndProjectOperator.java 的RecordingLazyBlockLoader.load()中:
public void load(LazyBlock block) { checkState(delegateLazyBlock != null, "delegateLazyBlock already loaded"); Block loadedBlock = delegateLazyBlock.getLoadedBlock(); delegateLazyBlock = null; // 立即断开对委托 LazyBlock 的引用 recordInputStats(); block.setBlock(loadedBlock); }加载完成后delegateLazyBlock = null与LazyBlock.assureLoaded()中loader = null互为呼应,构成"加载即断链"的统一约定。
连接器中的落地:从 ORC、Parquet 到 RCFile
LazyBlock是 Presto 各存储连接器扫描路径上的通用机制。当前仓库中,以下位置均可见new LazyBlock(...)+ 自定义LazyBlockLoader的经典组合:
| 连接器/模块 | 实现类 | 文件 |
|---|---|---|
| Hive ORC | OrcBatchPageSource | presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcBatchPageSource.java |
| Hive Parquet | ParquetPageSource.ParquetBlockLoader | presto-hive/src/main/java/com/facebook/presto/hive/parquet/ParquetPageSource.java |
| Hive RCFile | RcFilePageSource | presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSource.java |
| Hive 通用 | HivePageSource(类型强制转换场景) | presto-hive/src/main/java/com/facebook/presto/hive/HivePageSource.java |
| Druid | DruidSegmentPageSource | presto-druid/src/main/java/com/facebook/presto/druid/DruidSegmentPageSource.java |
| ORC 选择性读取 | OrcSelectiveRecordReader.OrcBlockLoader | presto-orc/src/main/java/com/facebook/presto/orc/OrcSelectiveRecordReader.java |
以 OrcSelectiveRecordReader.java 中的OrcBlockLoader为例,其load()展示了 loader 的典型工作流:
public void load(LazyBlock lazyBlock) { if (loaded) { return; } try { reader.read(offset, positions, positionCount); } catch (IOException e) { OrcSelectiveRecordReader.this.getOrcDataSourceId().attachToException(e); throw new UncheckedIOException(e); } Block block = reader.getBlock(positions, positionCount); if (coercer != null) { block = coercer.apply(block); } lazyBlock.setBlock(block); loaded = true; }要点:
- loader 内部有
loaded标志位,保证同一块数据只解码一次(LazyBlock本身也有block != null短路,双重防护); - 解码异常会被绑定到 ORC 数据源 ID(
attachToException),便于把 I/O 错误关联回具体文件; - 通过
coercer(类型强制转换函数)在物化阶段顺便完成类型适配,避免另一次全列拷贝。
类似地,ParquetPageSource.java 的ParquetBlockLoader用checkState(batchId == expectedBatchId)校验批次号一致性,防止跨批次误用缓存,并将ParquetCorruptionException映射为HIVE_BAD_DATA、IOException映射为HIVE_CURSOR_ERROR的PrestoException,体现了连接器对懒加载异常的规范处理。
算子层的字节统计:RecordingLazyBlockLoader 的设计
引入LazyBlock后产生一个统计难题:未加载的块无法知道自己的真实字节数(getSizeInBytes()会触发加载)。如果每次算子输入都统计所有列的字节,就会强制物化所有懒加载列,抵消延迟加载的意义。
ScanFilterAndProjectOperator.java 的recordProcessedInput给出了精妙解法:
private Page recordProcessedInput(Page page) { long blockSizeSum = 0L; Block[] blocks = null; for (int i = 0; i < page.getChannelCount(); ++i) { Block block = page.getBlock(i); // account processed bytes from lazy blocks only when they are loaded if (block instanceof LazyBlock && !((LazyBlock) block).isLoaded()) { if (blocks == null) { blocks = copyOfPageBlocks(page); } blocks[i] = new LazyBlock(page.getPositionCount(), new RecordingLazyBlockLoader((LazyBlock) block)); } else { blockSizeSum += block.getSizeInBytes(); } } return (blocks == null) ? page : new Page(page.getPositionCount(), blocks); }思路是"以懒对懒":对未加载的LazyBlock,不计算其字节数,而是用一个新的RecordingLazyBlockLoader把它包起来。当该块在后续PageProcessor中被真正访问、触发加载时,RecordingLazyBlockLoader.load()会先调用recordInputStats()把延迟统计的输入字节补上,再透传真实数据块。这样:
- 从未被消费的列永远不会被物化,也永远不会被统计字节(零成本);
- 被消费的列在物化瞬间精确记账,统计不丢失也不重复。
延迟投影:DictionaryBlock 的懒加载链
LazyBlock的价值还体现在表达式执行中的"懒加载传递"。在 DictionaryBlock.java 的createProjection中,如果新字典本身是LazyBlock,投影操作不会立即物化它,而是生成一个嵌套的LazyBlock:
if (newDictionary instanceof LazyBlock) { return new LazyBlock(positionCount, (block) -> { Block newDictionaryBlock = newDictionary.getBlock(0); Block newBlock = createProjection(newDictionaryBlock); block.setBlock(newBlock); }); }注释明确写着"be careful to not materialize it"。也就是说,从文件扫描到投影计算,懒加载状态可以沿表达式树逐层传递,直到真正需要数据的那一层才统一物化——这正是 Release 0.91 所保护的机制之所以重要的原因:链越长、loader 越晚清除,被拖住的资源就越多。
测试验证:懒加载语义与字节统计的正确性
TestScanFilterAndProjectOperator.java 用两个用例锁定了上述行为:
testPageSourceLazyLoad:构造new LazyBlock(100, lazyBlock -> { throw new AssertionError("Lazy block should not be loaded"); }),如果第 1 列(懒加载列)被物化,测试直接失败。这验证了"未被消费的懒加载列绝不物化"的语义;testPageSourceLazyBlock(注释为 "Tests that a page containing a LazyBlock is loaded and its bytes are counted by the operator"):通过CountingLazyPageSource+CountingLazyBlockLoader,验证包含LazyBlock的 Page 被加载后,其字节数会被算子正确统计,即RecordingLazyBlockLoader的补记账逻辑成立。
这两个测试一正一反,分别守护了"延迟"与"精确记账"两条约束,是理解这条内存优化变更最直观的对照实验。
小结:一行变更背后的内存治理哲学
Release 0.91 的这条 General Changes 只有一句话,但它揭示的是 Presto 内存治理的一条重要原则:懒加载不仅要"延迟物化",还要在物化后"及时断链"。从 LazyBlock.java 的loader = null,到 ScanFilterAndProjectOperator.java 的delegateLazyBlock = null,再到各连接器 loader 内部的loaded标志位,整个体系围绕"资源生命周期与数据生命周期解耦"展开。也正因如此,官方才会对 0.91 的内存泄漏问题给出如此郑重的警告——在内存优化密集的引擎里,任何一个引用周期的疏漏都可能演变成线上事故。理解这段历史,比记住某个版本号本身更有价值。
- 大数据
- 数据库
- 后端
【免费下载链接】presto
The official home of the Presto distributed SQL query engine for big data
相关推荐
Presto Release 0.188 深度解析:slice 负索引修复、join 内存 GC 优化与 db 资源组环境隔离
Presto Release 0.188 深度解析:slice 负索引修复、join 内存 GC 优化与 db 资源组环境隔离 本文基于当前开源仓库中 Pres
大数据数据库后端解密AI编程智能体的创新架构:从零构建高效开发环境
解密AI编程智能体的创新架构:从零构建高效开发环境 你是否曾为AI编程助手的高昂token成本而烦恼?是否希望有一个能持续运行、理解代码上下文、且成本可控的智能
人工智能AI Agent代码智能体CLI桌面应用MCP ClientsDeepSeekVitePress Frontmatter 完全指南:页面级元数据与行为控制
VitePress Frontmatter 完全指南:页面级元数据与行为控制 导读 Frontmatter(YAML 前置数据)是 VitePress 中每一篇
前端文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考