原生 Minecraft 服务器:基于 GraalVM Native Image 的极致启动优化
一次将大型Java应用程序转化为原生可执行文件的实践。
这是一篇客座博客文章,作者是乔纳森·格伦达,他是德国波茨坦大学哈索·普拉特纳研究所软件架构组的一名硕士研究生。 原文链接:https://medium.com/graalvm/native-minecraft-servers-with-graalvm-native-image-1a3f6a92eb48#440b
文章大纲
- 动机
- 对 Minecraft 服务器进行 AOT 编译
- 如何构建原生 Minecraft 服务器
- 初步结果
- 展望
动机
Minecraft 是全球销量排名第一的电子游戏。可以说,尤其是其多人游戏方面,帮助它成为了今天这样一种文化现象。与朋友一起生存、探索、玩迷你游戏或建造,乐趣会倍增。自 2009 年以来,Minecraft 的 server.jar 文件已面向所有想要搭建多人游戏服务器的人提供。但是,尽管人们可能在技术上能够做到这一点,但有些人的计算机就是无法满足这些服务器即使只为几个玩家提供服务所需的高昂 CPU 或内存资源要求。虽然与 13 年前相比,内存和 CPU 资源现在便宜得多,但 Minecraft 服务器的性能一直让服务器管理员们期待更好的替代方案。
在哈索·普拉特纳研究所的一个夏季学期中,一个学生项目专门研究了这个问题。其目标是创建一个可工作的、原生可执行的 Minecraft 服务器,利用原生部署相较于 JVM 执行所带来的优势。但是,这样的服务器到底能好多少呢?
对 Minecraft 服务器进行 AOT 编译
Minecraft 服务器是一个大型 Java 应用程序,通常在 JVM 上运行。JVM 提供了一个独立于平台的运行时环境、垃圾回收内存管理和即时 (JIT) 原生机器代码编译。开发人员必须依赖 JIT 编译器提供的动态优化,但这在考虑应用程序的启动时间或资源占用时,也带来了一系列自身的缺点。
GraalVM Native Image 提供了一种不同的方法,它在构建时执行广泛的静态分析,从而消除了在运行时对 JIT 编译器的需求。这类似于 Rust、C++、Go 等语言采用的提前编译方法。为此,它做出一系列假设,使得分析能够涵盖构建原生可执行文件所需的一切。这里的主要假设是封闭世界假设,这意味着在运行时执行的所有内容都必须在构建时可用于静态分析且对其可见。不允许在运行时动态包含额外的代码,因为这会破坏 CWA。执行 CWA 使 GraalVM Native Image 有机会进行激进的优化(例如通过去虚拟化),根据事先执行的分析,从应用程序和 JVM 环境内容中移除任何未使用的类、方法或字段。
(此处原为图片描述:GraalVM Native Image 构建过程(来源))
这个项目的主要挑战在于 Minecraft 服务器内部对 Java 反射的使用。在 Java 中,可以在运行时动态访问类、方法或字段。然而,这在构建时很难被任何静态分析自动检测到。不利的是,Minecraft 大量使用反射,如果不适当支持它,你甚至无法启动服务器。因此,GraalVM Native Image 支持包含配置文件,这些文件可以指定程序的哪些部分是通过反射访问的,因此应该包含并链接到最终产品中,即使静态分析在正常情况下不会这样做。虽然这些文件可以手动创建,但推荐的入门方法(特别是对于大型应用程序)是使用 Native Image 追踪代理。此代理会观察目标程序的常规 JVM 执行过程,追踪反射访问和 Java 的其他动态特性,并以所需的配置文件格式记录下来。这保证了在 JVM 执行期间进行的每个反射调用在生成的原生可执行文件中也同样能够进行。
(原文此处为邮件订阅推广部分,已省略)
但你可能已经想到,这要求你在 JVM 执行期间覆盖所有可能导致反射调用的程序路径。对于 Minecraft 来说,这几乎是不可能的。该应用程序是闭源的,因此我们无法直接查看源代码来确定所有的反射访问。虽然可以对编译后的代码进行反编译,但开发团队事先对代码进行了混淆,随机化了尽可能多的信息。因此,在没有深入访问源代码的情况下,我们无法确定每个可能通过反射访问的类、方法或字段是否都已被捕获。这就是为什么我们选择首先追踪一次常规的 Minecraft 游戏会话来覆盖基本内容。虽然代理很有帮助,仅从那一次会话中就为反射配置注册了超过 340 个不同的反射访问,但我们很快在测试构建结果时发现了 Native Image 中的一些错误。不过,在 GraalVM 团队的帮助下,这些问题很快得到了修复。在手动测试过程中,我们注意到服务器崩溃是由于某些类或方法不可用,而这些类或方法显然不是追踪代理记录的最初游戏会话所必需的。然后,我们手动将这些必需的访问添加到配置文件中,供未来的构建尝试使用。我们重复这个过程,直到我们能够通过击败 Minecraft 的官方最终 Boss——末影龙来完成游戏。使用通过此过程创建的配置文件,每个人都可以在家中创建可工作的服务器。
如何构建原生 Minecraft 服务器
(此处原为图片描述:在 Oracle Cloud 的免费 ARM Ampere A1 计算实例上运行的原生 Minecraft 服务器。)
如果你想自己尝试,我们已经在 GitHub 上建立了这个代码仓库。
该仓库附带一个简短的 README.md 文件,解释了入门需要做的一切。克隆后,只要你的系统上安装了 GraalVM 22.2 或更高版本,就可以在根目录下运行一个简单的构建脚本。该构建脚本将自动下载 Minecraft 的 server.jar,使用提供的 GraalVM Native Image 配置文件,并为你构建一个原生可执行文件。如果你希望获得尽可能小的可执行文件,我们甚至加入了一个压缩步骤来进一步减少部署大小。要启动原生 Minecraft 服务器,只需运行 ./native-minecraft-server 文件。
专业提示:Oracle Cloud 免费套餐不仅永久免费提供 4 个 Ampere A1 核心和 24 GB RAM,这对于托管 Minecraft 服务器来说绰绰有余,而且还免费提供 GraalVM 企业版的访问权限,该版本比社区版性能更好、内存使用率更低。
祝你 Minecraft 之旅愉快!
初步结果
GraalVM Native Image 提供的 AOT 编译承诺在保持运行时性能的同时,减少工件大小、改善启动时间和内存占用。
一个原生 Minecraft 服务器的大小不到 120MB,因此明显小于 Minecraft 的 server.jar 加上运行它所需的 JDK。使用 upx 压缩,原生可执行文件的大小甚至可以进一步减小到不到 40MB,这比单独的 server.jar 还要小。
初步实验还表明,在减少内存占用的情况下,运行时性能也具有竞争力。我们最初使用 Native Image 的快速构建模式来加速开发周期,尽管该模式不适用于生产环境,因为它禁用了重要的性能优化,但我们能够与单个用户正常游戏。
此外,我们发现 Minecraft 服务器的启动时间相当难以衡量。原因是服务器启动时执行的“准备生成区域”阶段,其计算开销似乎明显大于启动 JVM 所需的时间。虽然 Native Image 可以减少 JVM 启动时间,但我们发现,与在 JVM 上运行相比,使用 Native Image 社区版可能导致 Minecraft 服务器的总体启动时间更慢。然而,使用 GraalVM 企业版 Native Image,我们能够构建出比在 JVM 上启动更快的原生 Minecraft 服务器。
Native Image 企业版还提供了额外的 AOT 编译优化和功能,包括多线程 G1 垃圾回收器和配置文件引导优化,可以显著提升原生 Minecraft 服务器的性能。即使使用 GraalVM Native Image 默认的串行 GC,我们也测量到内存占用显著降低,高达 43%。考虑到这些都是初步结果,我们乐观地认为 Minecraft 服务器部署可以从 GraalVM Native Image 中受益。
由于 Minecraft 服务器是一个复杂的真实世界应用程序,GraalVM 团队已表示有兴趣将其用作 GraalVM Native Image 的基准测试目标。我们也希望 Minecraft 社区能在我们的工作基础上,帮助更详细、在更大规模的环境中基准测试原生 Minecraft 服务器的不同配置。如果你有任何基准测试结果或任何问题,请随时提出问题。
展望
除了更多的测试和基准测试,原生 Minecraft 服务器还可以通过不同的方式进一步改进。构建脚本和配置可以更新以支持最新版本的 Minecraft(本文撰写时为 1.19)。由于模组是 Minecraft 社区的重要组成部分,研究可以支持哪些服务器端模组也将会很有趣。目前的构建脚本也只适用于 Linux 和 macOS。在 Windows 上构建原生 Minecraft 服务器应该是可行的,但需要一些脚本工作。还需要额外的工作来增加对服务器 GUI 的支持。目前,它只能在 nogui 模式下运行。如果你想贡献任何这方面的内容,欢迎提交拉取请求。
作为这个项目的一部分,我们着手使用 GraalVM Native Image 实现对 Minecraft 服务器的 AOT 编译。用于此目的的构建脚本和 Native Image 的相应配置已在 GitHub 上提供。
请随时尝试并与我们、GraalVM 社区和 Minecraft 社区分享你的经验!
撃っていいのは撃たれる覚悟のあるヤツだけだ。