返回
RSS Databricks Blog 发布 2026-08-12 03:00 22

Databricks开源Metals v2:面向百万行代码库的Java/Scala语言服务器

Databricks分享了其通过扩展Metals(广泛使用的Scala语言服务器)来支持Java并扩展至大型单仓库规模的经验,并与上游团队合作开源了Metals v2。该版本现已在Cursor、VS Code和Neovim中可用,旨在让拥有大型Java和Scala代码库的团队能够将编码代理与轻量级编辑器配对。
推荐理由:该内容聚焦于Databricks内部的开发工具和基础设施,而非数据垂直领域的某一具体方向,因此不推荐给数据从业者关注。
Databricks

译文 AI 逐段翻译

在Databricks,现在大多数代码由代理编写。当工程师仍需亲自动手时,他们倾向于使用轻量级编辑器,这些编辑器启动迅速,且几乎无需配置即可浏览代码。然而,对于Scala和Java,IntelliJ多年来一直树立着标准。在我们单体仓库的规模下,它实际上是唯一能跟上的编辑器。本文分享了我们如何通过扩展Metals(广泛使用的Scala语言服务器)以提供一流的Java支持并适应我们单体仓库的规模,构建了替代方案。与上游Metals团队合作,我们现在已开源Metals v2,以便任何拥有大型Java和Scala代码库的人都能将其编码代理与轻量级编辑器配对使用。

Metals v2现已在Cursor、VS Code和Neovim中可用,安装说明请参阅Metals网站

构建IDE飞轮

2025年5月,我们开始将Databricks的日常编辑器工作流标准化为Cursor。Cursor和VS Code在Databricks早已广泛用于前端和其他非JVM工作,并且两者都对我们的基于云的开发环境提供了强大的SSH远程支持。然而,我们的大多数服务是用Scala和Java编写的,在我们单体仓库的规模下导航这些代码是症结所在;这正是我们着手解决的问题。

统一编辑器的重要性超越了个人偏好。统一的IDE平台形成了飞轮效应:团队共享一个代码探索和开发的基准,而平台团队则能集中投资。Cursor成为我们单体仓库中JVM工作的主要编辑器,我们进行了整合,以至于今年未续订大部分IntelliJ许可证。

image2.png

三个信号显示了向Cursor转移的进展程度:总体IDE使用情况、Scala和Java文件打开事件(其中Metals处于关键路径),以及Databricks外部的采用情况。

IDE使用情况

最广泛的信号是总体IDE使用情况。Cursor在Databricks的采用自增长,但在2025年9月陷入停滞,并持续了数月。随着Metals v2的推出,增长恢复,到2026年7月,92%的周活跃IDE用户打开Cursor,而IntelliJ为12%。在仅使用单一IDE的工程师中,现在有2400人使用Cursor,而IntelliJ为120人。

image3.png

Scala和Java文件打开事件

比总体IDE使用情况更接近的信号是每种IDE中Scala和Java文件打开事件的比率,这些语言中Metals v2处于关键路径。自2025年10月Metals v2的首个主要改进在Cursor中落地以来,Cursor在Scala和Java文件打开事件中的份额从40%上升至78%。这一攀升速度慢于总体IDE采用率,因为Scala和Java是IntelliJ最根深蒂固的领域,但Cursor的份额继续保持逐月上升的趋势。

image1.png

早期外部采用

Metals v2最初是Databricks的分支,但目标始终是将这项工作带回开源社区。我们与Cursor团队以及VirtusLab的核心Metals维护者合作,正在准备一个稳定的Metals v2版本,以取代当前的稳定版v1。

我们正在为Metals v2做贡献,以使Cursor在数百万行的Java代码库中表现良好,重点改进Bazel支持、调试和测试。——Kevin Niparko,Cursor
AI正在改变开发者使用IDE的方式,Metals v2朝着正确的方向发展:快速启动、可靠的代码库定位,以及为大型代码库设计的架构。Databricks在非凡的规模上验证了这种方法,VirtusLab很高兴帮助将这项工作推广到更广泛的Scala和JVM社区。——Krzysztof Romanowski,VirtusLab开发生产力负责人

与其他大型JVM代码库的交流强化了我们所见到的与Databricks相同的模式:对具有强大SSH远程支持的Cursor、VS Code和Neovim的需求,平台向统一工具的压力,以及在单体仓库规模下现有JVM语言服务器没有可靠的路径。

不到一个月前,我们在Stripe开始部署Metals V2,但我们不断对其在我们Java代码库中的出色表现、工程师们的兴奋以及维护者们的协作和可学习性感到印象深刻。——Mahib Hosain,Stripe开发者平台

这就是我们想要的Metals v2模式:为大型JVM代码库提供共享基础设施,在开放环境中维护,并由需要其在规模上工作的公司塑造。持续开发由VirtusLab领导,如有问题、反馈或贡献,可通过GitHub问题跟踪器[email protected]联系他们。

深入探讨:Metals v2如何扩展

以上是Metals v2的情况。其余部分面向希望更深入了解跨越26M行单体仓库的低延迟代码智能背后的工程的读者。

由于代理编写大部分代码,快速代码库定位比全面的语言服务器协议(LSP)补全和重构覆盖更重要。这就缩小了问题范围,但并未让解决方案变得明显:我们仍需为如此大的Scala和Java Bazel单体仓库提供易于设置、低延迟的导航,而现成的LSP并非为支持这种规模而设计。我们必须从头开始构建仓库规模的代码智能,并将启动时的实用性视为一个关键指标,以便衡量和改进。

定义初始智能时间(TTII)

初始智能时间(TTII)衡量打开仓库后编辑器变得有用的速度。我们从语言服务器激活时开始计时,并在最重要的功能可用时停止,假设用户不进行干预。这些关键功能包括模糊搜索工作区符号、跳转到定义以及在整个仓库中查找符号使用情况。

Metals v2 的三层架构

Metals v2 是 Scala 和 Java 的语言服务器。我们从 Metals v1(官方 Scala 语言服务器)开始,但达到我们的 TTII 目标不仅仅需要对它进行边缘调优。我们对其进行了分叉,并重构了三个核心层:仓库索引、Scala 和 Java 编译器管道以及构建集成边界。

  1. 无构建仓库索引: Metals v2 通过使用自己的 mbt 索引(在下面的部分中描述)直接索引工作区源文件,将构建服务器协议(BSP)从启动关键路径中移除。与 Metals v1 的主要区别在于,Metals 现在拥有初始项目模型,而不是等待构建服务器提供它。
  2. 支持编译器的 Scala 和 Java 交互式管道: Metals v2 将符号加载从构建提供的类路径转移到 Metals 提供的源路径,使诊断和导航能够更准确地反映磁盘上的实际代码,而不是上次成功编译的过时快照。这需要重新思考 v1 代码库中的大量核心假设。
  3. 元数据优先的构建集成: Metals v2 仍然使用 BSP,但契约更窄。Metals v1 在编辑器热路径上使用构建服务器进行诊断,而 Metals v2 将常规诊断移出构建服务器,主要使用 BSP 查询构建元数据:依赖项、生成的源代码、测试发现和调试启动器。这一转变降低了为 Metals v2 实现 BSP 服务器的门槛,我们的内部 Bazel BSP 服务器验证了该模型可扩展到大型 Bazel monorepo。

下面的部分将逐一介绍每一层。

mbt 索引:构建同步前的仓库级智能

mbt 代表 Metals Build Tool,mbt 索引是实现 TTII 或“立即可用”契约的主要推动因素。它是工作区源文件的内容寻址索引:Metals 使用命令git ls-files --stage 来发现文件,并使用 Git blob OID 来决定哪些索引条目可以重用。在构建同步之前获得仓库级信息,Metals 可以回答“第一英里”问题:

  • 跨文件引用的诊断
  • 模糊符号搜索
  • 跨仓库的跳转到定义,但不包括外部依赖项或生成的代码
  • 通过创造性使用每文档布隆过滤器,实现广泛的查找引用和查找实现

mbt 索引实际上是一个从源文件到文件局部摘要的哈希映射。一个条目记录文件的包声明、带源位置的定义以及文件中引用的标识符的紧凑布隆过滤器。定义支持工作区符号搜索和跳转到定义,而布隆过滤器让 Metals 在进行更精确的检查之前快速排除不可能包含引用的文件。由于每个条目仅从一个文件派生,增量更新保持简单:当文件更改时,Metals 重新计算该文件的条目并替换它。

在我们的 monorepo 中,持久化的 mbt 索引未压缩大小为 936MB,包含超过 142k 个 Scala、Java 和 Protobuf 文件中的 290 万个符号的信息。一个干净的基准构建在 32 核上充分利用 CPU 需要 22 秒,而从磁盘解析预构建索引需要 5 秒。在生产环境中,我们将 TTII 衡量为启动服务器、加载过时的 mbt 索引、根据最新的git ls-files --stage 状态更新它,以及重启 Scala 和 Java 呈现编译器的时间:p50 8.7秒,p90 36.7秒。在 290 万个工作区符号中进行模糊符号搜索是p50 10毫秒,p90 95毫秒。还有进一步减少 TTII 的空间,但以这些数字来看,它不是我们接下来需要解决的瓶颈。

Scala 管道:在单个编译器实例上推动 2400 万行代码

Scala 管道围绕呈现编译器构建,这是 Scala 类型检查器的一种模式,它在多次编译运行之间缓存和重用符号表信息。这种重用,加上编译器的惰性符号解析,让单个实例可以保持完整的 2400 万行 Scala 代码库在作用域内,同时以p50 0.9秒,p90 8.9秒的速度发布诊断。

那个单一的 Scala 编译器实例以两种模式之一运行,取决于 Metals 从构建服务器获得的关于正在编辑文件的信息量。在构建同步之前,一个回退编译器采取宽松的观点,将仓库中的每个源文件视为合格的依赖候选,使导航立即可用,即使跨尚未在 Bazel 中编译的代码。构建同步后,一个精确编译器将自己限制在构建服务器报告的类路径和源路径边界内,这正是使其诊断和依赖信息构建准确的原因。精确和回退模式都依赖相同的两种技术来保持如此大的源路径可管理:

  • 未打开源文件的概要模式。 未在编辑器中打开的源文件在类型检查前会移除方法体。这保留了编译器需要的类型签名,同时避免了方法体中的工作,而方法体是大部分类型检查时间花费的地方。
  • 内存中的源布局索引。 Scala 源路径不能可靠地编码包结构,因此 Metals 运行解析器构建一个轻量级索引,将类映射到源位置,然后通过该索引加载符号,而不是扫描文件系统。回退编译器在 50 毫秒内直接从 mbt 索引构建此索引,重用 Metals 已有的仓库级数据,而不是从源代码重新解析。

精确模式增加了第三种技术:它将传递依赖的源码保留在源路径上,这样在编辑来自不同 Bazel 目标的文件时,编辑器能立即反映更改,而无需等待 Bazel 生成新的类路径,这是阻碍 Metals v1 的一个限制。

在单个展示编译器实例中容纳 2400 万行代码库,远远超出了 Scala 编译器的原始设计目标。Metals v2 通过两种基于共享源路径扩展技术的编译器模式实现了这一点。跳转到定义(编辑器最常用的功能)在整个仓库中运行,p50 7毫秒,p90 575毫秒。Scala 是我们大多数工程师使用的语言,因此这条流水线必须工作正常,Cursor 才能在我们的单体仓库中真正替代 IntelliJ。

Java 流水线:使用 Turbine 每秒处理一百万行 Java 代码

Metals v2 直接基于 javac API 实现了 Java 语言服务器协议表面。我们评估过复用现有的 Java 语言服务器(基于 JDT 和 NetBeans 的实现),但两者都像 Metals v1 一样以构建为中心,这正是 v2 想要去除的耦合。相反,基于 javac 让 Java 与 Scala 共享 Metals v2 的源路径和构建同步模型,Scala 仍是我们单体仓库的主要部分,而且我们需要的 Java 表面足够小,可以自行维护。

这些 javac API 提供了足够的编译器访问权限,可以实现诊断、导航、语义高亮以及其他关键的 LSP 方法,并且它们对部分损坏的代码表现良好,这对于交互式编辑器工作流至关重要。与 Scala 一样,我们有意将重构和代码补全等活跃编辑功能保持在较窄的范围内。

这种架构的主要可扩展性问题出现在触发 javac “enter”阶段出现病态性能的文件中,这些问题文件通过传递导入在符号大纲层引入了数百万行代码。Metals v2 通过 javaSymbolLoader:"turbine-classpath" 模式(默认启用)来解决,该模式使用修改版的 Turbine 头文件编译器来生成适用于 IDE 环境的仓库类路径,包括优雅处理命名解析错误。Turbine 在单线程上每秒处理接近一百万行 Java 代码,这意味着 Metals 可以定期重新编译整个 Java 代码库。这使几乎所有跨文件符号都保留在类路径上而不是源路径上,因此 javac “analyze”阶段可以接近其交互使用的实际极限运行:根据我们的基准测试,每秒处理近 10 万行代码。

构建集成:在编辑器延迟下查询 285k 个 Bazel 目标

“在构建同步之前有用”并不意味着“忽略构建”。Metals 仍然需要构建图保真度来处理一系列功能,包括导航到第三方依赖项或生成的源码、尊重自定义遮蔽规则、发现测试套件以及自动配置调试启动器。在 Databricks,这意味着要让 285k 个 Bazel JVM 目标的元数据在编辑器延迟下可查询。

我们这一层的生产实现是一个用 Go 编写的内部 BSP 服务器,专为我们的 Bazel 规则定制。此 BSP 服务器不包含在此开源版本中,但其设计选择仍然值得借鉴到其他 Bazel BSP 实现中。

首先,除非用户明确要求,否则 Metals v2 绝不会通过 BSP 服务器调用 Bazel。在大型单体仓库中,后台 IDE 同步可能会占用 Bazel 锁,并与开发人员发起的构建竞争,因此构建同步是明确的用户操作,而不是启动行为或后台维护任务。生成的元数据存储在 JSON 快照中,通过常量池化重复标签、路径和仓库前缀来扩展。当用户同步时,BSP 服务器会增量地向此快照添加更多目标,并通过 BSP 提供更新的元数据。

其次,没有共享的同步配置格式。用户按需同步单个文件或目录,以改善导航或诊断保真度。根据我们的经验,预定义的同步集随时间增长,在团队之间复制,并且变得比开发人员实际需要的聚焦同步更慢。

整合三层

总之,无需构建的索引、编译器支持的流水线以及元数据优先的构建集成,在数百万行的 Bazel 代码库中提供了低延迟的代码智能。通过以 Apache 2.0 许可证开源,我们希望为更广泛的生态系统提供一个以前不存在的选择:将编码代理和轻量级编辑器与丰富的 Scala 和 Java 导航相结合,规模之大使得现有的 JVM 语言服务器无法提供可信的路径。

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存