Hardwood 1.0:给 Java 读 Parquet 一个更轻的入口

2026-07-03 28 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

Hardwood 到达 1.0,是 Java 生态里处理 Parquet 文件的一次值得注意的尝试:它由 Gunnar Morling 启动,目标是用多线程读取和“零强制外部依赖”降低接入成本。和成熟但依赖较重的 Apache Parquet Java 实现相比,Hardwood 目前更像一个专注的读取器:先把读路径做轻、做快,写入能力留给后续版本。

为什么“零强制依赖”很有吸引力

在 JVM 服务里接 Parquet,经常不是单独跑一个工具,而是嵌进已有的数据同步、批处理、搜索索引构建或离线分析链路。此时依赖树会直接影响发布包大小、冲突概率和安全扫描噪音。

Hardwood 的卖点不是“替代所有 Parquet 工具”,而是把一个常见场景切窄:如果你的服务只需要读 Parquet,并且希望少引依赖,它提供了一个更轻的候选项。

这类库尤其适合几种地方:

  • JVM 后端需要读取少量到中等规模的 Parquet 文件做导入。
  • 批处理任务已经有自己的线程池、日志、指标体系,不希望 Parquet 读取层带入一串传递依赖。
  • 应用运行在受限环境里,例如函数计算、镜像体积敏感的容器、插件化系统。

需要注意的是,“零强制依赖”不等于没有工程成本。你仍然要验证格式兼容性、性能曲线、异常处理和生产可观测性。

多线程读取:收益来自数据形态,而不是口号

摘要里明确提到 Hardwood 采用多线程方式。对 Parquet 这类列式格式来说,多线程读取可能带来明显收益,但收益会被文件结构和硬件条件限制。

更容易受益的场景包括:

  • 文件较大,读取和解码可以被拆分。
  • 机器有足够 CPU 核心和磁盘吞吐。
  • 下游处理不会马上变成瓶颈,例如每行都同步写远端数据库。

不容易受益的场景也很常见:

  • 文件很小,线程调度成本盖过了解码收益。
  • 存储很慢,瓶颈在网络或磁盘读取。
  • 下游处理是单线程、锁竞争严重或有全局排序。

所以更实际的采用方式是:先把 Hardwood 放在清晰的读取边界后面,再用你自己的数据和部署环境跑基准。不要只看单机样例吞吐。

可以这样实践:把 Hardwood 隔离在读取适配器后面

由于 Hardwood 当前聚焦读取,写入支持还在后续版本预期中,建议不要让业务代码直接散落调用读取库。下面是一个可复制运行的最小 Java 项目骨架:它先定义业务侧接口,真实 Hardwood 调用放在一个适配器里。示例中的 HardwoodParquetReader 使用占位实现,接入时只需要替换 TODO 区域为 Hardwood v1 的实际 API。

保存为 ParquetImportDemo.java 后可直接运行:

import java.nio.file.Path;
import java.util.List;
import java.util.Map;

public class ParquetImportDemo {
    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Usage: java ParquetImportDemo <file.parquet>");
            System.exit(2);
        }

        ParquetRows reader = new HardwoodParquetReader();
        long rows = 0;

        for (Map<String, Object> row : reader.read(Path.of(args[0]))) {
            // Replace this with validation, indexing, batching, or transformation.
            System.out.println(row);
            rows++;
        }

        System.out.printf("Imported %,d rows%n", rows);
    }
}

interface ParquetRows {
    Iterable<Map<String, Object>> read(Path file) throws Exception;
}

final class HardwoodParquetReader implements ParquetRows {
    @Override
    public Iterable<Map<String, Object>> read(Path file) throws Exception {
        // Assumption: keep all Hardwood-specific code here.
        // Replace this block with Hardwood v1's actual reader API after adding the dependency.
        // For example, map each Parquet record into Map<String, Object> or your domain object.
        System.err.println("TODO: wire Hardwood reader for " + file.toAbsolutePath());
        return List.of();
    }
}

运行:

javac ParquetImportDemo.java
java ParquetImportDemo ./sample.parquet

如果用 Gradle,可以先把依赖集中在一个模块里。下面的坐标需要替换为 Hardwood v1 在你仓库中使用的实际 Maven 坐标:

plugins {
    java
    application
}

repositories {
    mavenCentral()
}

dependencies {
    // Replace with the published Hardwood v1 coordinates used by your organization.
    implementation("<group-id>:<artifact-id>:1.0.0")
}

application {
    mainClass.set("ParquetImportDemo")
}

然后用依赖树确认它是否符合你对“轻依赖”的期待:

./gradlew dependencies --configuration runtimeClasspath

这条命令在引入新库时很实用:它能让你看到实际进入运行时 classpath 的内容,而不是只相信宣传语。

采用前的检查清单

Hardwood 1.0 当前最明确的边界是“只读”。如果你的链路需要读后再写 Parquet,短期内仍要保留其他写入方案,或把写入接口设计成可替换实现。

建议上线前检查这些点:

  • 用生产样本文件测试,不只用玩具数据。
  • 覆盖你实际遇到的列类型、空值、嵌套结构和压缩方式。
  • 对比 Apache Parquet Java 的读取结果,确认语义一致。
  • 分别压测小文件、大文件、本地盘、对象存储路径。
  • 观察线程数、CPU、GC、磁盘吞吐和下游处理耗时。
  • 把读取库封装在适配器后面,避免将来切换或升级时改动业务代码。

Hardwood 的价值在于给 Java Parquet 读取提供了一个更轻的选项。它不是一个“现在就覆盖全部 Parquet 生命周期”的承诺,而是一个明确的工程取舍:先把读取做好,把依赖压低,把写入留给后续版本。对只读链路来说,这已经足够值得单独评估。


相关推荐