从 1BRC 到 Hardwood:Java 高性能数据处理的实战路径

2026-05-25 33 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

Gunnar Morling 是 Confluent 的技术专家、Java Champion,也是 One Billion Rows Challenge(1BRC)的发起人。他在最近一期播客中聊了一个很实际的问题:Java 在数据密集场景下到底能不能做到足够快、足够轻?从 1BRC 的极限优化实验,到用 AI 原生方式开发 Apache Hardwood——一个几乎零依赖的 Java Parquet 解析器,他给出的答案是:能,但路径要选对。

1BRC:把 Java 逼到极限

1BRC 的任务很简单:从 10 亿行气象数据中计算每个城市的最低、最高温度和平均温度。听起来像数据库该干的事,但规则要求纯 Java、单线程、不依赖外部框架。这个约束把所有参与者逼到了 JVM 的底层——ByteBuffer 直接映射文件、SIMD 向量化、手动内存布局对齐。

Morling 提到,最终冠军方案的处理速度达到了每秒数 GB,远超很多人对 Java 的预期。关键不是语言本身慢,而是大多数人写 Java 时默认走的路径太重:堆上分配对象、GC 承压、IO 走 InputStream 包装链。1BRC 证明了一件事——当你绕过这些默认路径,直接操作内存和字节,Java 的性能天花板和 C/Rust 并没有本质差距。

轻量依赖不是口号,是工程选择

播客中另一个核心话题是 Apache Hardwood。这是 Morling 正在开发的 Java Parquet 解析器,目标是"最小依赖"——不拉 Hadoop 生态那套重型依赖链,直接解析 Parquet 的列式存储格式。

Parquet 是数据工程领域的事实标准列式格式,但 Java 生态里解析 Parquet 的主流路径要依赖 hadoop-commonhadoop-mapreduce-client-core 等一长串包,光是依赖传递就能拉进几十个 jar。Hardwood 的思路是:只解析格式本身,不碰 Hadoop 的文件系统抽象、不碰分布式调度,把依赖树控制在个位数以内。

这个选择背后有明确的工程判断:如果你只是要从本地或 S3 上的一个 .parquet 文件里读几列数据,为什么要加载整个 Hadoop 运行时?

用 AI 原生方式开发解析器

播客标题里提到的"AI natively developing"值得展开。Morling 不是让 AI 写完整个项目,而是把 AI 当作解析器开发的加速器——Parquet 的 Thrift IDL 定义了文件元数据的结构,AI 可以快速生成对应的 Java 读取骨架;字节偏移、列 chunk 定位这些机械但容易出错的细节,AI 辅助比手写更高效。

他的实践方式大致是:先让 AI 根据 Parquet 规范生成结构定义和基础解析逻辑,再人工审查和优化关键路径(比如字典解码、RLE 解压缩)。这不是"AI 写代码人不管",而是"AI 做粗活人做精调"。

实战:用最小依赖读取 Parquet 文件

Hardwood 还在开发中,但思路可以立刻用到你的项目里。下面演示一个不依赖 Hadoop 生态、直接从 Parquet 文件读取数据的最小化方案——使用 parquet-avro 的精简依赖配置,或者更激进地直接操作字节。

先看一个用 Apache Parquet 的轻量读取方式(只拉必要依赖,避免 Hadoop 传染):

<!-- pom.xml — 只引入 parquet-avro 和 hadoop-common 的最小集 -->
<dependencies>
  <dependency>
    <groupId>org.apache.parquet</groupId>
    <artifactId>parquet-avro</artifactId>
    <version>1.13.1</version>
  </dependency>
  <!-- hadoop-common 是当前不可避免的,但排除重型传递依赖 -->
  <dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-common</artifactId>
    <version>3.3.6</version>
    <exclusions>
      <exclusion>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-hdfs-client</artifactId>
      </exclusion>
      <exclusion>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
      </exclusion>
      <!-- 根据你的实际需要继续排除 -->
    </exclusions>
  </dependency>
</dependencies>

对应的 Java 读取代码:

import org.apache.avro.generic.GenericRecord;
import org.apache.hadoop.fs.Path;
import org.apache.parquet.hadoop.ParquetReader;
import org.apache.parquet.avro.AvroParquetReader;

import java.io.IOException;

public class MinimalParquetRead {

    public static void main(String[] args) throws IOException {
        // 本地文件路径,不依赖 HDFS
        Path filePath = new Path("data/weather.parquet");

        // 使用 AvroParquetReader 直接读取,无需 MapReduce 上下文
        ParquetReader<GenericRecord> reader = AvroParquetReader
                .builder(filePath)
                .build();

        GenericRecord record;
        long count = 0;
        while ((record = reader.read()) != null) {
            // 只读你需要的列,不反序列化整行
            String city = record.get("city").toString();
            double temperature = (Double) record.get("temperature");
            if (count < 5) {
                System.out.printf("%s: %.1f°C%n", city, temperature);
            }
            count++;
        }
        reader.close();
        System.out.printf("Total rows: %d%n", count);
    }
}

运行前准备一个测试 Parquet 文件:

# 用 Python 快速生成一个示例 Parquet 文件
pip install pyarrow

python3 -c "
import pyarrow as pa
import pyarrow.parquet as pq
import numpy as np

cities = ['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Chengdu']
n = 1000
table = pa.table({
    'city': pa.array(np.random.choice(cities, n)),
    'temperature': pa.array(np.random.normal(25, 5, n).astype(np.float64))
})
pq.write_table(table, 'data/weather.parquet')
print('Written', n, 'rows to data/weather.parquet')
"
# 编译运行
mvn compile exec:java -Dexec.mainClass="MinimalParquetRead"

如果你想更激进——完全不依赖 Hadoop 类,可以参考 Hardwood 的思路,直接用 ByteBuffer 读取 Parquet 的 magic bytes 和 row group 元数据:

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.io.IOException;

public class RawParquetProbe {

    public static void main(String[] args) throws IOException {
        // 直接打开文件,检查 Parquet magic header
        FileChannel channel = FileChannel.open(
                java.nio.file.Path.of("data/weather.parquet"),
                StandardOpenOption.READ
        );

        ByteBuffer header = ByteBuffer.allocate(4);
        channel.read(header, 0);
        header.flip();

        String magic = new String(header.array(), "UTF-8");
        System.out.println("Parquet magic: " + magic); // 应输出 "PAR1"

        // 读取文件尾部的 footer length(最后 4 字节)
        long fileSize = channel.size();
        ByteBuffer tail = ByteBuffer.allocate(8);
        channel.read(tail, fileSize - 8);
        tail.flip();

        // 最后 4 字节也是 "PAR1",前 4 字节是 footer 长度
        int footerLength = tail.getInt();
        String tailMagic = new String(tail.array(), 4, 4, "UTF-8");
        System.out.printf("Footer length: %d bytes, tail magic: %s%n",
                footerLength, tailMagic);

        channel.close();
    }
}

这段代码零外部依赖,只用 JDK 自带的 NIO API 就能验证 Parquet 文件结构。这正是 Hardwood 要走的路——从字节层开始,不借 Hadoop 的抽象层。

什么时候该走轻量路径

Morling 的实践给出了几个明确的判断点:

  1. 你的场景是读取而非写入分布式存储——如果只是从本地或对象存储读 Parquet 列数据,Hardwood 式的轻量解析器比 Hadoop 依赖链更合适。
  2. 你需要嵌入到其他系统——Kafka Connect、Flink 作业、嵌入式分析引擎里,依赖树越浅,冲突越少,启动越快。
  3. 你在做极限性能优化——1BRC 的经验表明,当你需要压到每秒数 GB 的吞吐,必须绕过 InputStream 包装链和堆上对象分配,直接操作 ByteBuffersun.misc.Unsafe(有风险但可控)。

反过来,如果你依赖 HDFS 的纠删码、租约恢复、分布式一致性,那 Hadoop 生态的重量是合理的——不要为了"轻"牺牲你实际需要的系统能力。

检查清单

在决定是否走轻量 Java 数据处理路径时,可以快速过一遍:

  • [ ] 当前项目解析 Parquet 时,hadoop-* 依赖是否超过 5 个 jar?
  • [ ] 你的读取场景是否只涉及本地文件或 S3/OSS 对象存储?
  • [ ] 启动时间是否被 Hadoop 初始化拖慢(典型症状:冷启动超过 10 秒)?
  • [ ] 是否有依赖冲突导致 NoSuchMethodErrorClassNotFoundException
  • [ ] 是否需要单线程或低线程数下的极限吞吐?

如果超过 3 项命中,值得认真评估 Hardwood 式的轻量替代路径。1BRC 已经证明了 Java 在数据密集场景的性能潜力,Hardwood 正在证明依赖树可以砍到多短——这两件事合在一起,才是"高效 Java 开发"的完整含义。


相关推荐