Ohhnews

分类导航

$ cd ..
foojay原文

生产策略归属:公开构建Eliya(第一部分)

#eliya#openjdk#生产策略#jvm#默认配置

目录

策略、机制与一个提醒那个标志生产配置文件凌晨3点的故事现状与下一步自行验证

本文探讨的是生产意图在托管运行时中的归属问题,以及我们为此构建的 OpenJDK 发行版——Eliya,一个固执己见、注重合规性的 OpenJDK 发行版。这是系列文章的第一篇。后续部分将涉及工程技术:可重现构建、glibc 基础版本、发布签名、以及我们交付的唯一一个源码补丁。本篇则是它们所服务的核心论点。

[LOADING...]

每个可配置系统都拥有两个空间。配置空间:通过调节系统已暴露的旋钮所能达到的所有行为。还有实现空间:只有通过修改内部代码才能实现的行为。

一个包装脚本可以做很多事情:设置标志、启动 -javaagent、挂载卷。这已经超越了“选择一个标志”;它围绕 JVM 组合了外部系统行为。但请注意它所触及的底线,而这正是关键所在。它无法在堆转储字节流正在写入时重写该流。它无法告诉你某个标志的来源——这个值是由内部派生而来,还是通过命令行设置的。即便是原生的 JVMTI 代理——最强大的外部情况,运行在 JVM 自己的地址空间内——也只能通过运行时发布的扩展点工作。真正重要的界限不在于你能从外部触及多少个旋钮,而在于你是否能改变 JVM 自身内部代码的行为——而一个包装脚本,无论多么精巧,始终处于外部。

大多数运维需求都远在这个界限之外,这没关系。大多数需求永远不需要触及 JVM 的内部。当某个需求真正需要进入内部时,有趣的问题就开始了。

策略、机制与一个提醒

操作系统研究在五十年前就命名了这种分离(HYDRA 论文)。策略命名意图;机制实现它。JVM 单独暴露了各种机制,例如 -XX:+HeapDumpOnOutOfMemoryError 是一个机制。JVM 长期以来为某些意图暴露了一等标志——比如编译(-server、分层编译)和内存。但它从未暴露一个策略,说“当这个进程死亡时,它应留下足够的证据来诊断原因”。

上游 JDK 在设计上基本保持中立,因此它不会提供某些特定群体或行业希望作为其基准的那种固执己见的默认值。然而,一个 JDK 发行版可以采纳这种意见,并通过内置的策略来为这些群体提供服务。

为什么这样的主观发行版应该存在?默认值本身就是策略,无论是否有意为之。Johnson 和 Goldstein 测量了德国和奥地利(两个文化相似的国家)的器官捐献同意率,德国约为 12%,奥地利超过 99%(《默认值能拯救生命吗?》,《科学》,2003 年),全部差异归结于哪个复选框被预先勾选。

人们运行的是交付的版本。JDK 发行版的默认值实际上就是整个部署群的配置。这证明了提供一个具有生产级默认值的发行版是合理的。

其次,这仍然不足以证明修补 VM 是合理的(我们确实这么做了),因为一个包装脚本也可以提供默认值。这里有一个提醒。很容易过度延伸,所以让我们精确地界定这个限制。将策略与机制分离并不意味着策略必须位于机制内部。许多系统正确地外部化了它们的策略。Kubernetes 将授权推给准入控制器,应用程序将访问决策委托给 IAM。所以我不能声称“策略属于运行时”。真正的论断更狭窄,也更难反驳:某些策略依赖于只有运行时才能看到或执行的事情——一个标志的来源、活跃对象图、转储写入器即将发出的字节。这样的策略需要运行时亲密性,而外部化它们并非风格偏好,而是根本不可能。开箱即用的 JVM 在出问题时留下的证据通常比运维人员预期的要少。没有 GC 日志。没有堆转储。崩溃日志写入某个随容器消亡的临时路径。而正确记录这些痕迹(对敏感内容进行脱敏、对必须防篡改的内容进行签名、证明每个设置的来源)恰恰需要上一段所说的运行时亲密性。这是包装脚本无法提供的东西,也是我们存在补丁的唯一原因。

这个论点可以用更正式的方式表述:

归属论断。设 C 为配置 JVM 的外部控制器集合,B 为这些控制器所能产生的行为集合。每个包装脚本、Helm chart 或 webhook 仅仅是在 C 内操作以选择 B 内的一个点。但某些策略需要 B 之外的行为(记为 B'),因为它们完全依赖于只有运行时才能看到或执行的事情。B' 行为只能通过改变运行时本身来获得。因此,针对 B' 的策略,狭义地讲,就是属于 VM 内部的策略。

举一个需求确实落在 B 之外的例子——PCI DSS 要求 3.5.1,规定主账号(PAN)在存储的任何地方都必须不可读。然而,支付服务的堆转储会将活生生的卡号以明文形式写入磁盘。批评者会说,你可以从 VM 外部处理这个问题:禁用堆转储,或者加密卷。但看看各自付出的代价。禁用堆转储,你就丢弃了本应以此方式运行的取证证据;卷加密保护了磁盘,但转储在从内存传输到信任边界内的写入器时仍然是明文。在 VM 内部、在流被写入时对转储进行脱敏,就消除了这一困境,而不是用一种风险换取另一种风险。这是 HotSpot 中一个转储写入器的问题。你无法通过组合现有的 JVM 标志纯粹地实现这种行为。

那个标志

整个论点归结为一个标志,它激活了一个策略组:

java -XX:EliyaProfile=Production -jar app.jar

这个标志包含在 Eliya 中——Asymm Systems 推出的 OpenJDK 25 LTS 发行版,本月早些时候首次发布 GA,专为受监管行业(电信、银行和金融服务、医疗、政府)中注重合规性的生产环境而构建。EliyaProfile 是论题所要求的策略点:一个 ccstr 枚举。Production——通用的一组生产就绪默认值——是当前提供的值(第一阶段);其他值已预留,部分在路线图上,其余根据需要驱动。

简单说一下名字。Eliya 是斯里兰卡的高地茶区 Nuwara Eliya 的简称,距离我写这篇文章的地方只有几个小时车程。僧伽罗语单词意为。Java 以印尼一个种植咖啡的岛屿命名。我们的则来自种植茶叶的高地。

生产配置文件

生产配置文件的易用性默认值:

  1. OOM 时堆转储,写入 ${ELIYA_DIAGNOSTIC_PATH}/${service}/${replica}/heap-dumps/ 下的结构化路径
  2. OOM 时退出,干净关闭,以便编排系统可以重启你,而不是留下一个僵尸 JVM
  3. 原生内存跟踪summary 模式
  4. 崩溃日志路径,同一目录树下的可预测 hs_err_pidNNNN.log 位置
  5. 容器支持强化:配置文件中保证 UseContainerSupport=true。上游 JDK 25 已默认开启,所以目前这不起作用。它存在是为了保证在未来上游改变主意时仍然有效。
  6. 解锁诊断 VM 选项,JFR 采样和性能分析器附件需要此选项才能正常工作

请注意,所有这六项都是现有的 HotSpot 标志,结构化路径可以通过包装脚本解析并传递给 -XX:HeapDumpPath。第一阶段提供的行为没有一个是一个脚本无法复现的。严格来说,Production 是从配置空间中选择。那么为什么还要修补 VM,而不是提供一个包装脚本呢?

因为第一阶段不是能力本身——而是边界。EliyaProfile 是在运行时内部建立的一个命名策略点,而真正只有运行时才能拥有的能力将在后续阶段附着于此。

Production 是一个 Saltzer-Schroeder 意义上的故障安全默认值:VM 以其易用性来源设置其易用性参数,因此显式在命令行传递值的运维人员享有优先权。遵循正常的 JVM 优先级(命令行优先于易用性)。Production 有意让位于运维人员。

这里有两个独立的维度被混淆了。值得分开澄清:

  1. 强制执行风格:配置文件是否通过拒绝冲突覆盖来主动维护一个不变式,还是仅设置一个默认值然后退让?当覆盖与它持有的约束冲突时,配置文件可以拒绝启动,但是否应该这样做完全取决于第二个维度。
  2. 权限模型:谁拥有这个不变式,谁有权放弃它?这是真正的区别。Production 的约束是操作性的——它们的存在是为了服务于运维人员自身的目标:可诊断性。有意覆盖其中一项的运维人员是在做出他们有权做出的决定;他们拥有这个目标,因此后果由他们承担,配置文件会退让。合规性值的约束是外部的——它们的存在是为了满足监管机构,而不是运维人员。一个 SRE 说“我接受信用卡号存在于世界可读的转储中”无权做出这种权衡;监管有最终决定权。因此,无论运维人员的意图如何,配置文件在启动时都会失败关闭。

用旧的访问控制概念来说——同一个标志,两种权限模型。自然地,强制强制执行要求编排层在部署清单中锁定配置文件标志,并确保普通运维人员无法通过完全移除标志来绕过合规性。

其他一切与上游 OpenJDK 25 保持一致。java.security 与上游位元完全相同——TLS 1.0 / 1.1 已禁用,弱密码已阻止,当前最低密钥大小要求已到位。GC 选择交给 JDK 25 的易用性判断。在配置文件之外,Eliya 有意保持接近上游,而 EliyaProfile=None 则保留上游行为。

凌晨3点的故事

以上理论都不是人们实际经历问题的方式。以下是每个人都认同的版本。

凌晨3点。寻呼机响了——运行结算引擎的 JVM 刚刚 OOM,容器重启了。你的第一反应是拉取 GC 日志。没有——没人启用 -Xlog:gc*。好吧,那堆转储呢。也没有:没人设置 -XX:+HeapDumpOnOutOfMemoryError,也没人设置 -XX:HeapDumpPath,所以即使有转储,它也会出现在 JVM 恰好运行的任何临时容器路径中。崩溃日志?同样的情况。

每个人都知道这些标志。大多数几乎没有运行时开销。然而,生产系统在没有它们的情况下运行,因为每个团队都自己构建“应该开启哪些标志?”的答案——通常是在第一次事故之后。配置错误的文献表明,这并非粗心大意。也就是说,给人类提供足够多的旋钮,配置错误就会成为主导故障模式(Yin 等,SOSP 2011;Xu 等,“嘿,你给我的旋钮太多了!”,FSE 2015)。文献指出的解决方案正是论题所指出的:将答案内置于系统中,作为一个意图级别的控制,而不是让每个团队通过十几个机制级别的控制重新推导它。

如果你不是需要这个的受众——你在构建内部工具,或者你的团队已经完成了标志工作——那么中性的上游构建是正确的答案,而且有几个好的选择。

现状与下一步

第一阶段,本月已交付:一个可选标志;Linux x86_64 和 aarch64;.tar.gz / .deb / .rpm / 多架构 GHCR Docker;已签名且可重现;每季度在每次上游 CPU 发布后两周内进行上游 CPU 刷新,贯穿 JDK 25 LTS 窗口(2029 年 9 月);GPLv2 附带 Classpath Exception;每个发布附带相应的源代码。有意不构建 JDK 21 版本。JDK 29 LTS 在 GA 时将有 24 个月的重叠期,之后 Eliya 25 才会停止维护。

第二阶段(目标 2026 年下半年)在相同的策略点上构建,提供持续 JFR、捆绑的仅本地诊断工具,以及一个经过 FIPS 验证的提供程序变体。它还将统一 GC 日志(-Xlog:gc*)纳入配置文件——从第一阶段推迟,仅因为 HotSpot 内部的 LogConfiguration 初始化过早,在没有更深层源码补丁的情况下无法安全地覆盖易用性。

我们将继续在 Foojay 上公开构建其余部分,一次一小块——范围限定在发行版本身。已排队的部分:

  • 可重现构建——我们需要消除的每个导致非确定性的来源,以获得字节级重现构建。
  • glibc 基础版本——为什么现代构建主机会在企业 Linux 上静默地破坏你的二进制文件,以及我们对此做出的每个架构的开发工具包决策。
  • 签名与密钥卫生——简单的 80%(签名本身)和困难的 20%(密钥周围的一切)。
  • 结构化诊断路径——第一阶段中唯一真正的源码补丁,以及为什么 ${service}/${replica} 解析属于 VM 而不是包装脚本。

自行验证

让我们运行 Eliya 看看:

1. 锁定字节

# 下载构件 + 签名校验和:
curl -fsSLO https://github.com/asymmsystems/eliya-jdk/releases/download/eliya-jdk-25.0.3/eliya-jdk-25.0.3-linux-x64.tar.gz
curl -fsSLO https://github.com/asymmsystems/eliya-jdk/releases/download/eliya-jdk-25.0.3/SHA256SUMS.txt
curl -fsSLO https://github.com/asymmsystems/eliya-jdk/releases/download/eliya-jdk-25.0.3/SHA256SUMS.txt.asc

# 获取签名密钥,然后在信任之前至少与一个独立渠道交叉核对指纹
gpg --keyserver keys.openpgp.org --recv-keys 076DE547397A5D27EECEE0B307A90689B71A158F
gpg --fingerprint eliya@asymm.systems
# 预期:076D E547 397A 5D27 EECE  E0B3 07A9 0689 B71A 158F

# 验证校验和文件上的签名,
# 然后验证 tarball 上的校验和:
gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt --ignore-missing
# 预期:tarball 校验和上显示“来自‘Eliya Releases (Asymm Systems) <eliya@asymm.systems>’的良好签名” + “OK”。

完整的多渠道验证仪式文档可在验证下载页面找到。

对于 Docker 用户,等效方法是按摘要固定:

# 获取多架构清单的摘要
docker buildx imagetools inspect ghcr.io/asymmsystems/eliya-jdk:25.0.3
# 将 <digest> 替换为上面的值
docker pull ghcr.io/asymmsystems/eliya-jdk@sha256:<digest>

2. 读取构建标识

# 解压并确认供应商字符串:
tar xzf eliya-jdk-25.0.3-linux-x64.tar.gz
./eliya-jdk-25.0.3/bin/java -version

3. 确认配置文件已激活

./eliya-jdk-25.0.3/bin/java -XX:EliyaProfile=Production -XX:+PrintFlagsFinal -version 2>&1 \
    | grep -E '(HeapDumpOnOutOfMemoryError|ExitOnOutOfMemoryError|NativeMemoryTracking|ErrorFile|UnlockDiagnosticVMOptions) +='

# 每个行末尾都显示“{ergonomic}”
# 即 HotSpot 自己的来源标记。
# 重新运行 -XX:EliyaProfile=None 并查看区别。

在验证字节之后,下一步是在 CI 中固定 Eliya。在 Eliya 版本控制指南中了解四种固定模式。


参考文献:

  • Levin, Cohen, Corwin, Pollack & Wulf, "Policy/Mechanism Separation in HYDRA", SOSP 1975
  • Johnson & Goldstein, "Do Defaults Save Lives?", Science 302, 2003
  • Yin et al., "An Empirical Study on Configuration Errors" SOSP 2011
  • Xu et al., "Hey, You Have Given Me Too Many Knobs!", ESEC/FSE 2015
  • Saltzer & Schroeder, "The Protection of Information in Computer Systems" Proc. IEEE 63(9), 1975
  • PCI DSS v4.0 Req 3.5.1

本文 Where production policy belongs: building Eliya in public (part 1) 首发于 foojay