播客:追寻高效的Java开发——从1BRC到AI原生开发Hardwood
转录文稿
Olimpiu Pop:大家好。我是 InfoQ 编辑 Olimpiu Pop,坐在我对面的是 Gunnar Morling。作为团队中最了解 Java 与数据处理之间关系的人之一,他对这类话题有着敏锐的洞察力和浓厚的兴趣。闲话少说,Gunnar,请先介绍一下你自己吧?
Gunnar Morling:好的,Olimpiu,非常感谢你的邀请。是的,你刚才说的基本概括了我的兴趣所在。我非常热衷于 Java 和数据领域交叉的部分。我从业已经有一段时间了。比如,我曾参与 Hibernate 项目和 Bean Validation 的工作。过去,我曾是 Bean Validation 2.0 规范的负责人,这绝对是 Java 领域一个与数据相关的故事。后来,我参与了 Debezium 项目,这是一个用于变更数据捕获的工具,其核心是从 PostgreSQL 或 MySQL 这类数据库中提取数据,并将其放入 Kafka,从而实现从数据库到数据仓库或搜索引擎的实时数据流。它也同样适用于微服务间的数据交换等场景。现在,我在 Confluent 担任技术专家。我的工作内容非常多元化。一部分是内部事务,比如进行技术调研,评估我们是否应该投资某些项目,或者是否进行某项收购,我会在这方面做一些研究工作。有时我也会为领导团队解答问题,比如有人可能会问:"嘿,能解释一下 Flink 的 watermark 吗?",然后我就会写一份简要说明。而在对外方面,我主要专注于写博客和参加各种会议。当然,我也会尝试做一些原型开发和开源项目。我仍然参与 Debezium 的工作,同时也在开发一个新项目,这正是我们今天要谈论的。所以,可以说是涵盖了方方面面。
Olimpiu Pop:听起来很有趣。
Gunnar Morling:确实很有趣,绝对。我非常享受做这些事情。虽然我已经在这个行业里待了很长时间,但我每天仍然乐在其中。
反思“十亿行挑战”的意外影响
[02:32] Olimpiu Pop:嗯,Gunnar,你在公司和项目方面经历了很多变化,但核心始终如一——对技术的热情、好奇心以及类似的品质。很高兴看到你还是那个老样子。提到你,有两件事让我印象深刻,而且它们彼此关联。其中之一就是“十亿行挑战”,它迅速走红,将整个 Java 社区凝聚在了一起。而这个挑战的初衷,有点像在证明 Java 确实可以非常快,与当时的主流观点背道而驰。你自己感觉如何?这大概是三年前的事了吧?
Gunnar Morling:是的。你问得很有意思,因为那已经是两年多以前的事了。这个挑战发生在两年前的一月份。但到现在,人们还在问起它,甚至还有人向那个 GitHub 仓库提交拉取请求。我的意思是,挑战早就结束了。其实那个自述文件里已经写了,这个挑战项目已经完结,但人们仍然充满好奇。时不时还会有人谈论它,也许是在播客上或者别的场合。所以说,这个话题依然能引起人们的兴趣。另外,也有人问:还会有新的挑战吗?我还在考虑。在挑战结束后的第一年里,我绝对没想过要再来一次,因为那次挑战压力巨大。整个 2024 年一月我都在运营这个挑战,当时我真的不想再经历一次。但后来我慢慢恢复了,如果有一个能引起大家兴趣的好点子,我很乐意再做一次。关键在于,我相信它当时之所以能火起来,是因为它恰到好处地满足了一个点:非常容易解释。你有一个包含十亿行的文件,需要聚合这些值。每个人一分钟就能理解。同时,它又提供了巨大的优化空间。人们整个月都在忙着优化,如果我在一月底没有叫停,他们还会一直优化下去。所以我一直在寻找另一个兼具这两个特点的问题。如果找到了,我愿意再做一次。我还想把它自动化程度做得更高,因为上次基本上全靠手动操作,我事先完全没有预料到。所以,我会花更多时间在自动化和设置上。如果有人有什么好主意,我非常欢迎。
Olimpiu Pop:太好了。祝你未来的想法一切顺利。
Gunnar Morling:谢谢。
JDK 17 成为新基线,开箱即用性能更强
[04:50] Olimpiu Pop:另一件事是,你觉得 Java 在这段时间里进化得怎么样?我知道有些人走的是传统路线,他们用以前那些老方法,就是机械共鸣领域的人曾经使用的方式。而另一些人则在探索当时刚刚起步的新东西。现在,那些新东西很多已经成了 JDK 的一部分,成为长期支持版本(LTS)的一部分,很可能已经在生产环境中使用了。你觉得过去三年里,Java 生态系统发生了怎样的变化?
Gunnar Morling:嗯,我的意思是,并不是所有这些新特性都已经稳定了。比如向量 API,在挑战中被人大量使用,但我甚至不确定它现在是第 11 个孵化版本还是什么,所以它还在演进中。不过,像外部内存 API 这样的东西已经最终定型了,人们也在使用。是的,Java 从那时起已经取得了长足的进步。我仍然认为,如果你想要达到那种超高级的性能水平,你很可能还是得用上不少人们常用的那些技巧。而且现在也出现了一些当时还不存在的传统 API。例如,我特别感兴趣的是压缩对象头 JEP,它基本上减少了 Java 堆上每个对象的大小。这使得 JVM 能使用更少的内存,并且由于管理的总内存变小了,垃圾回收(GC)的周期也会减少。在我看来,这是 JVM 真正酷的地方之一。你可以保持应用不变,仅仅通过升级到新的 Java 版本,就能获得更好的性能。比如,你同样能从一个更好的并发 GC 算法(如 ZGC)中受益,这些算法自挑战以来也有了显著的改进。这就是为什么我总是建议所有人都不要停留在那些古老的版本上。我知道有些人还在用 Java 8 什么的,一定要升级到最新版本。它会带给你所有这些性能提升。而一旦你升级到一个相对较新的版本,比如 17,后续的升级就非常容易了。从 17 开始,基本上每个版本都是平滑升级,升级到最新版本花不了多少时间。
Olimpiu Pop:好的。看起来 Java 17 就是新的 Java 8。我记得我刚开始用 Java 的时候是 1.4 版本。然后 Java 8 是一个爆发点,那是一个划时代的时刻。而现在,看起来 17 正在成为新的——
Gunnar Morling:某种意义上的基线。
Olimpiu Pop:是的。我不想提“泰坦”这个词,但我不得不说,看起来 Java 17 就是 Oracle 时代的 Java 8。
Gunnar Morling:没错。在 8 和 17 之间,有一些破坏性的变化,比如收紧了对反射的使用,移除了 JAXB 等 API。当然,也引入了模块系统。所以,我认为所有这些事情都带来了一些摩擦。但一旦你升到了 17,一切就变得很顺畅了。再升级到最新版本,工作量也不大。
Olimpiu Pop:我和一些人辩论过,他们说:"好吧,如果你只是换一个 Java 运行时环境,并没有充分利用整个变化。"但是,即使只是换了 JVM,它本身就能带来改进,这是很好的第一步。然后你可以再研究一下新特性,看看如何将它们封装到代码中,让代码变得更好。
Gunnar Morling:这说的非常到位。我会说,当人们在考虑升级,并且希望向他们的管理团队证明升级的价值时,我总会建议他们不要只盯着语言特性。我的意思是,是的,能用更简洁或更安全的方式表达某些东西固然很好。但归根结底,负责决策的人可能更关心实在的事情,比如省钱。所以我认为,从性能角度、可观测性角度来论证升级的必要性会更有效。比如 flight recorder 以及它所启用的各种选项,这才是说服管理层升级的更好途径。然后,作为附加福利,你还可以免费使用所有那些漂亮的语言改进。
Java 中的持久化执行:用纯代码编写可恢复、可重启的工作流
[08:59] Olimpiu Pop:你确实喜欢折腾,我们姑且这么说吧。幸运的是,我们社区总能受益,因为你每次折腾后都会在博客上分享一些东西。你最近折腾的另一件事是用 Java 写了一个持久化执行引擎。而人们对 Java 有几个固有的印象。其中之一就是 Java 不够快,你已经打破了这一点。
Gunnar Morling:希望如此吧。
Olimpiu Pop:而且有些人认为,如果你需要一个真正高效的语言……但你多次证明了 Java 足够高效,足以构建数据工具。跟我们讲讲那个持久化执行引擎吧。你的动机是什么,想要达到什么目的,实际又取得了什么成果?
Gunnar Morling:要说明一下,那个引擎的实际状态存储用的是 SQLite(也就是 C 语言),但它被集成到了 Java 和引擎本身中。不过,为了让大家理解,我先设定一下场景:什么是持久化执行,以及它解决的问题是什么?当我们在企业应用上工作时,总会遇到我们称之为工作流或长时间运行的业务事务的问题。对吧?比如,你需要在某个地方执行某个操作,你可能需要给另一个服务发送消息。也许有一个批处理作业,负责处理采购订单,让它们从一个步骤流转到下一个步骤。所以,在我们的应用中有这些长时间运行的活动,这是非常常见的情况。问题在于,当这些流程跨不同的系统、组件和作业实现时,要推理这些流程会非常困难。很难理解端到端的流程。例如,我在系统里收到一个采购订单,然后到底发生了什么?它被发送到货运服务,然后某个组件在那里处理它。可能出错了。我怎么才能洞察到订单卡在哪里了,以及为什么没有发货给客户?持久化执行的想法就是从根本上用另一种视角来看待这个问题。它的理念是:让我们把流程基本上定义成一个从开始到结束的普通程序。这个程序可以用 Java 写,也可以用任何语言,但我主攻 Java 领域,所以那是我感兴趣的。我们用纯代码从头到尾编写我们的程序。然后,特殊的技巧在于,这个流程中的各个步骤本质上都是持久化状态和使流程可恢复的单位。比如说,以我的电商场景为例,我想要持久化一个传入的采购订单,也许我需要做一些客户检查,比如他们的信用度如何?我需要完成发货,我需要把它发送给客户,我需要分配库存等等。所有这些步骤当然应该按精确的顺序发生,而且每个步骤应该只发生一次。如果出错了,你不想对同一个采购订单分配两次库存。这个想法就是把所有这些事情写成一个普通的 Java 程序,然后有一个引擎围绕着它,负责执行这些步骤并记录它们的进展。例如,我们可以调用另一个系统来分配库存,然后获取结果并将其存储在本地状态存储中。这就是 SQLite 发挥作用的地方。如果这个流程继续进行,但后来失败了,比如可能是在处理发货时出了问题。那么我们可以重启这个流程,而我们的持久化执行引擎会判断出:"好的,五个步骤中的前两个步骤我已经做过了。所以我们不需要再次运行它们。"然后我们的流程只会从尚未运行的第一个步骤恢复执行。所以,这基本上就是持久化执行的理念:向你提供端到端流程的表示,使其在面对故障时可恢复、可重启。
Olimpiu Pop:谢谢。这听起来像分布式事务,但上升到了一个新的层次。
Gunnar Morling:从某种意义上说,是的,它可能在底层用了分布式事务,但真正酷的地方在于,作为应用程序开发者,你基本上不需要考虑那些复杂性。你只需要定义你的流程,然后由这个引擎来负责实现这些保证。这并不是一个新想法。它已经存在相当一段时间了。我的意思是,你也可以考虑传统的工作流引擎,它们基本上也处于同样的领域,但它们对流程的表示通常不那么方便。而持久化执行的理念就是:你写的是纯代码,开发者很喜欢这一点。而不是像 XML 表示或者其他什么东西。我对此很好奇。而且有时候,关于它会有一些复杂性方面的考量,人们会问:"嘿,这到底是怎么工作的?"比如说,我有一个程序,它是如何做到可以从后面的某个方法调用处恢复执行的?所以,我很好奇自己能不能做到这一点?同时,让我们去掉复杂性,看看能否做到不需要太多基础设施。它就是一个普通的 Java 程序。有状态存储和 SQLite,但真正需要的复杂性并不高。这就是我当时的想法,想了解清楚,然后也与其他人分享。你看,它不一定非得那么复杂。这个概念相对容易实现,这就是我在构建这个名为 Persistasaurus 的持久化执行引擎时展示的。
Olimpiu Pop:为了给我们认为 Java 做不到的事情画上一个句号,还有一件事你没有涉足,至少据我所知你没有。那就是 TUI(文本用户界面)。这个话题适合跟 Max Anderson 聊聊,因为他似乎非常热衷于证明 Java 可以做到这一点。
Gunnar Morling:绝对是的。
Hardwood:用 Java 构建快速、零依赖的 Parquet 解析器
[14:52] Olimpiu Pop:好的。不过让我们回到你当前的游乐场,也就是 Parquet 和整个技术栈。InfluxData 的伙计们称之为 FDAP,代表 Apache Flight、Data Fusion、Parquet 和 Arrow。他们用这个技术栈在 Rust 中重写了 InfluxData。我很高兴看到这在 Java 领域也在发生,因为我也在研究这个模型。对于内存模型,能有一个匹配的 Apache Arrow 是很棒的,然后你基本上就可以拿着 Parquet 用了。考虑到这是一个专业项目,我本以为这会是别人发起的,或者实际上,并不是这样,而是你提出了想法,然后你们开始做的?Parquet 背后的故事是什么?实际上,应该是 Hardwood 的故事?
Gunnar Morling:Apache Parquet 是一种广泛使用的文件格式,用于以列式方式存储数据。如果你考虑一个 CSV 文件,它是以基于行的方式存储数据的。你想在 CSV 文件中持久化的每条记录都是一行。这对于某些用例很好,但对其他用例就不那么好。想想这样一种情况:比如,你想要聚合数据中某个字段的所有值。假设你想聚合所有采购订单的总价值。对于 CSV 文件或者类似基于行的文件,你必须遍历所有行,找到采购订单字段,然后累加这些值。这不会很快。正是针对这类用例,就有了这个想法:让我们不以基于行的方式存储数据,而是以基于列的方式。对于每一列,我们存储该列的所有值,并且我们对数据集中的所有列都这样做。这样做有几个优点。我们可以非常有选择性地查询数据。如果你只对聚合采购订单价值列中的所有值感兴趣,我们可以顺序读取数据,只读取每条记录的采购订单价值。由于读取速度非常快,我们甚至不需要对其他列进行 I/O 操作,因为我们不关心它们,所以这非常高效。而且,以这种方式存储数据也变得非常高效,因为,例如,考虑时间戳,也许你的数据集是有序的。那么,与其将每个时间戳存储为一个完全自包含的值,你只需要持久化第一个时间戳。然后对于下一个时间戳,你只存储增量。可能只是晚了两毫秒,所以你只存储一个 2,而不是一个完整的时间戳。所以它非常节省空间,也非常适合压缩。所以,基本上,这就是列式文件格式如此有趣的原因。特别是在数据分析的上下文中,Apache Parquet 可以说是数据湖中的默认文件格式,并且被 Apache Iceberg、Delta Lake 等多个开放表格式大量使用。现在的问题是,Parquet 已经存在相当长一段时间了,Java 中有一个广泛使用的解析器和写入器,但它的依赖项也非常多。如果你使用现有的解析器(同样,那是社区的出色工作),你基本上会引入整个 Hadoop 栈,最终得到一个非常庞大的依赖项集。这就是 Hardwood 这个项目的由来。至于名字,Hardwood 和 Parquet 类似,所以是不同地板铺装方式的一个文字游戏。Hardwood 的想法有两点。第一,看看我们能否构建一个新的 Apache Parquet 解析器,并且没有任何强制性的依赖项。也就是说,完全从头开始编写。其次,我想让它非常快。这正是我们可以回顾“十亿行挑战”的地方,因为它的许多经验教训都适用于构建这个真实的系统。我想让它支持多线程,事实上它已经是多线程的,我想利用我所有的 CPU 核心。所以,这个想法就是用 Java 构建一个 Apache Parquet 解析器,目标是依赖项最少,速度极快。这就是它的大致梗概。
Olimpiu Pop:听起来很棒。所以它的物料清单应该很短,对吧?因为网络弹性法案(CRA),我们现在也得关注这个。
Gunnar Morling:我的意思是,这是非常现实的问题。所有这些供应链攻击的情况,所有那些依赖项,本质上都是负债,你肯定不希望有它们。这甚至还没谈到类路径冲突之类的问题,你可能会遇到不同版本的冲突。所以,这里真正的想法就是尽可能减少依赖项集。例如,这里我实际上可以回到较新的 Java 版本,因为没有日志依赖项,因为从 Java 9 开始,Java 就有了一个最小但足够好的日志抽象层。所以我正在使用它。然后人们可以添加他们想用的任何日志基础设施的绑定。此外,还有一些可选的依赖项。例如,Parquet 可以使用不同的压缩算法,比如 LZ4 和 GZip。我可不想费力去重新实现所有这些压缩算法。所以我把它们作为可选的依赖项集成进来。如果你想使用特定的压缩算法解析某个 Parquet 文件,你只需要引入那个压缩依赖项。对于对象存储支持也是如此。现在,你也可以解析存储在 S3 存储桶中的文件。在这种情况下,你会引入 Hardwood 项目中的一个额外模块。
Hardwood 的架构:并行化和性能优化
[20:25] Olimpiu Pop:你提到你在两个主要目标上投入了很多精力。一个是占用空间小,另一个是速度快。有什么诀窍吗?回顾过去,如果有人也想:“嘿,Gunnar,如果我正在构建一个解析器,想要具备这些属性,我应该注意什么?” 对于架构方面有什么建议吗?
Gunnar Morling:没错。所以我想到的第一件事就是并行化。正如我提到的,我们所有的机器都有这么多 CPU 核心。在我这台 MacBook 上,我甚至不知道具体多少,可能有 16 个核心左右。如果我单线程运行,我就浪费了大约 15 个核心,这是巨大的时间浪费。所以这里的想法是充分利用所有 CPU 核心。现在的问题是,我实际学到的是,并行化解析 Parquet 文件出奇地棘手。比如,你可能会说,对于文件中的每一列,我使用不同的线程,每列一个线程。这听起来不错,我也是这么开始的。但问题在于,你的数据集可能没有很多列。也许你只有三列。那么你只用了 16 个 CPU 核心中的三个。比以前好,但仍然不够好。然后的想法是,我们再往下走一步,Parquet 实际上将文件存储在称为页面的单元中。所以,这个引擎实际上实现了页面级别的并行性。我们可以利用基本上所有的 CPU 资源,我们的工作线程会处理文件中的所有页面。而这带来的挑战是:根据列所使用的编码类型,解码所需的 CPU 时间可能不同。我可能有慢的列和快的列。如果我用固定数量的线程来解码某个特定列的页面,我仍然会浪费资源。所以,这里的想法是某种自适应平衡机制。基本上,慢的列会分配更多的工作线程,快的列分配更少。通过这种方式,我基本上充分利用了尽可能多的 CPU。这就是关于并行化的整个话题。还有预取的概念。如果我处理多个文件(这些文件共同构成一个数据集),当我即将处理完第一个文件时,我会开始预加载下一个文件的内容。这只是为了避免任何冷启动的情况。还有尽可能避免装箱开销的想法。所以,我尽可能地将数据保存在原始数组、数组中,以及双精度数组中。这与你之前提出的问题有关。假设在 Hardwood 代码库中有这个页面表示。现在我需要有一个 int 页面、一个 float 页面和一个 double 页面,因为我不能有一个泛型的表示,并且它背后还能是原始数组,对吧?所以,目前,如果我使用泛型,我会得到一个 ArrayList<Integer>,而不是 int 的数组。所以我要付出装箱的开销。有了 Valhalla 项目,一旦我们得到它,我们就能避免这个问题。那么我就可以有一个泛型的数据结构,但仍然不需要付出对象开销。
Olimpiu Pop:是用虚拟线程还是传统线程?
Gunnar Morling:实际上我用了虚拟线程,我认为对于这个特定的工作负载来说,它并没有带来巨大的区别,但我想,何乐而不为呢?所以我最终使用了虚拟线程。
Olimpiu Pop:谢谢。我什么时候应该考虑使用 Hardwood?
Gunnar Morling:什么时候应该使用它?我相信,最终每个想在 Java 中解析 Parquet 文件的人都应该选择这个选项。现在,我的意思是,我们还处于非常早期的阶段。我是在今年年初开始这个项目的。几周前我发布了第一个版本,Alpha1。我很快就会发布一个 Beta 版本。从功能上讲,我认为在读取方面已经相当完整了。我们支持所有的编码方式以及所有的物理和逻辑类型。我们支持基于行和基于列的两种数据读取方式。我们支持将数据映射到 Avro。正如我提到的,我们支持从 S3 存储桶读取数据,并且有一个完整的机制来只获取我们需要的数据。例如,如果我们只对特定列感兴趣,我们只会从远程存储桶中获取该列的字节。还有谓词下推的概念。你可以说:“我只想要满足某个过滤条件的数据。” 比如,只取超过一百的购买记录。我们可以针对 Parquet 支持的行组和页面统计数据评估这个过滤器。所以,它基本上也能让你减少需要读取的数据块。所以,我们支持所有这些。读取方面,我认为已经相当完整了。我们预计很快会有一个稳定的 1.0 版本,随后不久,我们将支持写入功能,使其成为一个全面完整的 Parquet 库,既能读取也能写入。
Olimpiu Pop:那么,假设我只是在读一行或者别的什么。
Gunnar Morling:没错。
Olimpiu Pop:我会得到什么返回?
Gunnar Morling:基本上有两种模式。一种是我们称之为行读取器 API。这基本上允许你遍历你的数据集。它类似于迭代器模式,你可以访问行中的所有列。Parquet 支持嵌套数据。所以你可以有子结构,或者有列表,然后这种基于行的格式会给你,比如,一组你的博客文章的评论,或者你的数据是什么。所以有这种方式,如果你想以某种基于对象的方式处理这些数据,这很好。另外,就在这周早些时候刚刚合并的,还支持以 Avro 记录的形式返回数据。Parquet 领域的很多人使用 Apache Avro 作为绑定格式,所以我们也支持它。如果你想以基于对象的方式访问复杂结构的数据(比如),这很好。另一种替代方案是列读取器 API,我们这么称呼它。这基本上给你来自特定列的原始数据数组。这非常有趣,因为它使之变得非常快。然后,例如,你可以将整个数组数据送入向量 API,并非常高效地处理它。所以,我们有两种 API;根据使用场景,你会选择其中一种。
Olimpiu Pop:好的,酷。有没有与 Apache Arrow 的集成?看起来它是 Parquet 在内存中的对等物?
Gunnar Morling:目前还没有。我考虑过使用它,但,嗯,又回到了这个避免依赖项的问题,到目前为止我们还没有使用 Arrow。也许将来某个时候我们值得探索一下。
Olimpiu Pop:嗯,我认为这取决于社区以及谁在使用它。然后,如果它真的有意义,我想这个问题会被提出来。
Gunnar Morling:绝对如此。
AI 原生开发:需要人工监督的超级生产力助推器
[27:33] Olimpiu Pop:我认为你在项目公告文章中强调的另一件事是,Hardwood 本质上是一个"AI 原生"项目。感觉如何?你从中得到了哪些经验教训?显然,每个人都需要这么做。
Gunnar Morling:绝对如此。而且实际上,这触及了启动这个项目的动机之一。我的意思是,是的,我普遍感觉到需要一个这样的项目,一个具有最小依赖项且速度极快的 Parquet 解析器。所以它需要存在。我想要构建它。但与此同时,我也想获得使用 AI 构建这类工具的真实世界经验。AI 能在多大程度上帮助我完成这件事?它的构建是 AI 优先的。我大量使用 Claude Code 来构建它。社区中的人们也用它来贡献代码。但我想强调的一点是,我们并不是在“氛围编程”。我们的想法不仅仅是接受 Claude 给出的任何东西。不,真正的想法是我们想要理解代码,引导 AI 代理,并建立某些结构。我们希望拥有一个易于维护的代码库。所以,对于我们如何使用 AI 是相当严格规定的。例如,在我的 CLAUDE.md 文件中,我很明确地告诉它:总是从设计文档开始。在开发项目时,有一些事情我们需要牢记。我们希望有最小的公共 API,并且尽可能不要让代码暴露在公开的包中。我们希望避免重复,也许我们需要重构一些东西。所有这些都写进了我的 CLAUDE.md 文件,AI 会尽可能遵守这些规则。如果不遵守,我会说:“嘿,现在我们这里有了一些冗余,让我们清理一下,提取出一些辅助方法或者别的什么。” 我的意思是,特别是对于 Parquet 来说,它实际上是一个非常适合用 AI 解决的问题,因为它有一个写得非常好的规范。非常清楚我们需要构建什么。文件不同部分的规范等等都有。所以它定义得非常清晰。而且,Parquet 社区提供了一个非常广泛的测试套件。比如,有几百个 Parquet 文件。我们用现有的 Parquet 解析器处理所有这些文件,然后将输出与 Hardwood 给出的结果进行比较;如果有差异,那就是我们需要修复的 bug。AI 非常适合做这种事情。我可以告诉它:“对于测试套件中的那个文件,与上游解析器的结果有差异。为什么会这样,你能去修复它吗?” 到现在,实际上我们实现了完全的一致,所以输出是完全一样的。但 AI 编程并不完美,例如,它不会自动考虑代码的可维护性。所以很多时候,它会说:“好吧,让我在这里再加一个 if-else,让我在那里复制一些东西。” 所以,你仍然需要掌控全局,引导它,并确保它产出的东西是有意义的、高质量的。但它是一个巨大的生产力助推器。如果没有这些工具,我们不可能达到现在的水平。
Olimpiu Pop:好的。我喜欢你在实现更大功能之前使用设计文档的方法。你使用了某种标准吗?因为我知道,正如你提到的,在公共场合有文档记录对于这些新模型来说会容易得多。是你给了它一个模板,还是你只是指出来,说:“好,做 ADR(架构决策记录)吧”?
Gunnar Morling:没有。我们没有模板。也许我们确实应该加一个。当我们开始做的时候,它(Claude)自己提出了一个很好的结构,比如设置上下文,我们想要实现的问题是什么?我们已经有什么了?所以我觉得它挺合理的,但,是的,我完全同意,建立一个模板确实有意义。
Olimpiu Pop:好的。嗯,GitHub 上显然有一个仓库。在 ADR 方面,我喜欢的点是,有很多模板,取决于项目的大小。我真的很喜欢阅读那些内容,它们应该会有所帮助。有趣的是,因为你提到 Parquet 有很多测试,而且文档非常完善。我之前与你的两位联系人——ThoughtWorks 的 Birgitta Böckeler 和 Java 大师 Adam Bien(Air Hacks)——分别进行了两次对话。他们都提出了类似的观点。Adam 提到他大量使用 BCE 模式,这是一个相当古老的模式。他提到的是,Java 非常擅长模型生成,因为所有的流程,所有的社区流程,都有非常完善的文档。你有接口、JSR 和实现。这使得社区能够围绕它进行构建。另一个观点是,Cursor、Anthropic 和 OpenAI 的团队进行了三个实验。他们都构建了一些东西,并且希望推动一些更长期运行的项目。其中一些人构建了一个 C 编译器。另外一些人为企业内部构建了一个简单的工具,用于更深入地研究企业领域。实际上,其根本观点是,当你构建在高度规范化的工具上,并且有大量的测试时,事情会容易得多。比如,对于编译器或浏览器本身来说,因为你已经有构建好的东西了。所以我认为这一点值得注意。
Gunnar Morling:我的意思是,这是一个很好的防止回归的安全网。如果我们做了更改,我们会确定以前能用的东西是否突然不能用了。对我来说还有一个悬而未决的问题,就是我们如何处理性能回归。因为对我来说,性能是头等大事。我花了很多时间使用 Async Profiler、JFR 和其他工具来最小化分配。顺便说一下,我们也会对象数组池化并重用。这些都是我关注的重点,我也担心性能退化。我正在进行一些讨论。有一个名为 Otava 的 Apache 项目,是关于持续性能跟踪和识别性能指标回归的。所以这也是我想要设置的东西,以便在我们做出更改导致性能下降时能够识别出来并尽早预防。
Olimpiu Pop:这也是我正在研究的东西,不过是从一个不同的角度。对我们来说,性能退化的影响会转化为电池续航。因为在我目前的角色中,涉及物联网,但它是高度移动的,这意味着电池。如果出现性能退化,就意味着电池续航减少,意味着更高的成本,所有这些都是由类似的问题引起的。现在我回忆起 Luca Mezzalira 的一个观点。他提到,在他开始构建的时候,他正在实现微前端。他有一个容器,一个在性能、内存等方面都很受限的简单容器。显然,那个时候是关于包的大小。而我正试图看看我是否能创建一个约束,类似于我们设备的数字孪生,让我们可以在虚拟环境中观察它,但还有很长的路要走,因为它是定制的,更复杂。尽管如此,我很好奇你会找到什么样的解决方案,因为这也是我在想的,因为你有所有这些非常表层的东西。但是,在那个时间点,你掌握了如何调整 JVM 并使其工作的技巧。然后,如果你真的深入下去,你讨论的是机械共鸣,你在“十亿行挑战”中也看到了这一点,因为那些真正想深入研究的人,他们会为了特定的机器进行优化,从而获得金牌。
Gunnar Morling:没错。我认为这正是 AI 有趣的地方,因为它可以帮助我们处理一些事情。但就目前而言,我也觉得,你不能让它自己随意运行。我不能说:“去,给我构建一个 Parquet 解析器。”这不会奏效。所以你必须引导它,你必须掌控全局。现在,人们也经常担心这对开发者的影响,以及这对我们意味着什么。我们是不是都会失业?显然,我没有答案,但我现在的感觉是,这是一种双峰分布。如果你是这个领域的新手,可能你做的工作相对容易,那么它很可能是有影响的。但是,如果你经验丰富,在这个领域已经待了一段时间,并且你知道该构建什么,那么它就是一个巨大的生产力助推器,你可以完成以前因为能力不足而无法完成的事情。是的,我觉得它在经验层面上对开发者光谱产生了不同的影响。
Olimpiu Pop:对于我们这个年龄的很多开发者来说,它把乐趣带回来了。因为在高级职位上,你被困在官僚主义中,然后你对年轻一代不理解而感到沮丧。然后,在期望如何做事的方式与新一代做事的方式之间存在差距。但现在,你实际上有能力与其他人一起做这些事情,并保持那种高标准。我唯一的担忧,也是我目前还没有一个真正的回答,甚至没有一条路径的问题是:我们如何培养新人?
Gunnar Morling:绝对是的。那是一个巨大的担忧。
编程的未来:AI、成本与保持开发者技能
[36:52] Olimpiu Pop:因为我看到了两种截然不同的趋势。一些人就是,嗯,我需要一个有至少四五年经验的人。他们构建一些东西。然后另一些人说:“好的,我们现在不需要开发者了,因为我知道如何和 AI 对话,然后我们什么都有了。” 然后,你会看到他们的代码中有很大的缺陷。这些是我们要弥合差距并考虑长远利益的点,它们是编程演进的极好垫脚石。尽管我仍然充满好奇。我只是说出来,我们行业花了很多时间做的是构建更高级的语言,让事情变得容易得多。现在,甚至更好了,你直接生成。所以,现在我们做的是……让我们想想 Java。我们只是在编写遵循最佳实践的高级代码,然后被编译,再被解释等等。在你到达裸机之前,有很多层。我期望在某个时候,会有提前编译的构造,我们从中提取 bits 和 pieces,要么我们绕过整个代码;时间会证明一切。
Gunnar Morling:我的意思是,这一切……我认为目前还是悬而未决的。现在,没有什么是确定性的。如果你没有一个我们可以至少看到发生了什么的中间步骤,那么要演进这些事物,对它们进行调试等等,将会非常困难。所以我现在还看不到一个我们跳过源代码生成步骤的世界。但是,是的,我的意思是,它也在快速演进。所以,12 个月后可能会有很大的不同。
Olimpiu Pop:我甚至不想触及成本方面的问题,因为对我来说,成本是多方面的,不仅仅是材料成本,不仅仅是我们要花的钱。还涉及基础设施成本,涉及我们使用的水,涉及电力以及随之而来的污染和二氧化碳排放。我认为这些都是需要解决的难题,这完全取决于我们作为行业能否朝着正确的方向前进。
Gunnar Morling:是的,正如我提到的,我整天都在使用 Claude Code,有时我会问它:“嘿,你能做这个更改吗?” 然后我感觉我自己就是写不出来。在 CPU 周期方面,它显然会更高效,但一旦你进入了那种基本上总是与编码助手一起工作的模式,事情就变得如此容易。所以,是的,这确实是我很担心的事情。我们会失去自己动手做事的技能吗?前几天我坐在飞机上,没有 Claude Code 可用。然后我想,好吧,我该做什么?我可以开始写一些代码,但我感觉那会非常慢。做这个没有意义。所以我最后看了一些东西。是的,它具备所有这些动态,而且一切都在快速发展。
Olimpiu Pop:嗯,我曾经读过一本书叫《玻璃盒》,它提到了类似的事情,但那是关于开手动档还是自动变速箱。它说的是,在某个特定时刻,你只是需要把自己从舒适区中拉出来,尝试做不同的事情。因为在这些特定情况下,我认为这关乎纪律,关乎如何做这些事情,但那是完全不同的讨论了。
为 Hardwood 社区做贡献
[40:05] Gunnar Morling:没错。在我们结束之前,我想提一件事,因为很重要。我们谈到了构建 Hardwood,但实际上,一个社区正在围绕它形成。所以我想大声感谢 Rion、Andres 以及贡献了项目的其他几位。没有这些人,事情就不会这么有趣,我们也不会达到现在的地步。所以,对这个社区致以崇高的敬意。当然,我希望它能继续成长,我们将拥有一个更加多样化的 Hardwood 社区。
Olimpiu Pop:很好。你指的是 Andres Almiray?
Gunnar Morling:绝对是的。是的。他在构建和发布基础设施方面帮了很大的忙。
Olimpiu Pop:这家伙参与所有重要的项目。上周我们录制节目的时候,我还对他说,他可能是 Java 领域开源社区中最多产的人物之一。
Gunnar Morling:是的,是的。
Olimpiu Pop:感谢你今天的时间。感谢你打造了 Hardwood,祝你好运。我们期待 1.0 版本的发布。
Gunnar Morling:太棒了。是的。非常感谢你的邀请。这很有趣。
Olimpiu Pop:谢谢。
提及的资源:
- 十亿行挑战
- Hardwood 项目网站
- GitHub 上的 Hardwood 项目
- Bean Validation 规范
- Apache Parquet 网站
- Apache Iceberg 网站
- Apache Arrow 网站
- SQLite 网站
- Open JDK 项目 Loom
- Open JDK 项目 Valhalla
- 关于 JFR 的 Oracle 文档
- GitHub 上的 Async Profiler
- Apache Otava(孵化)项目
- 数字战略 – 网络弹性法案
- 用 Java 构建持久化执行引擎
- 从 Java EE 到 Quarkus 和 LLM:Adam Bien 关于无聊且面向未来的系统的剧本
关于作者
Gunnar Morling
Gunnar Morling 是一位专注于 Java 和数据流领域的开源软件工程师,目前在 Confluent 担任技术专家。此前,他帮助构建了基于 Apache Flink 的实时流处理平台,并领导了 Debezium 项目(一个用于变更数据捕获的分布式平台)。他是 Java Champion,并创建了 Hardwood、kcctl、JfrUnit 和 MapStruct 等多个开源项目。Gunnar 还是一位活跃的博主(morling.dev),曾在 QCon、Java One 和 Devoxx 等多个会议上发表演讲。他居住在德国汉堡。 显示更多 显示更少
您可以通过我们的 RSS Feed 持续关注播客,它们也可以通过 SoundCloud、Apple Podcasts、Spotify、Overcast 和 YouTube 获取。在此页面上,您还可以访问我们录制的内容说明。它们都包含可点击的链接,可以直接将您带到音频的相应部分。