快速热身,精益运行:使用Azul Prime和Kedify在Kubernetes上对Java进行垂直扩展
Kubernetes 上的自动扩缩已取得显著发展,但许多生产系统仍依赖基于 CPU 和内存利用率的响应式扩缩。问题在于资源指标往往滞后于实际需求。等到 CPU 上升时,用户可能已经在承受不可接受的延迟。
对于大多数真实服务而言,水平扩缩应基于主动型工作负载信号(如请求率、并发数或队列深度)来驱动,而非仅靠原始 CPU 指标。若想了解更多,请参阅这篇关于自动扩缩延迟与主动指标的文章。
垂直扩缩并非水平扩缩的替代品,而是互补。水平扩缩决定所需副本数量,垂直扩缩则决定每个副本在生命周期不同阶段应分配多少 CPU 和内存。
本文聚焦于一个常见的垂直扩缩挑战:Java 预热。
为什么 JVM 预热阶段与稳态不应共享同一资源配置
JVM 的核心概念之一——即时编译(JIT),能够根据实际处理的数据和执行路径持续优化源代码。与 C/C++ 等语言的静态编译相比,这种方法最终能带来更高的峰值性能,或者说在相同负载下消耗更少的资源。
由于 JIT 编译的存在,Java 虚拟机(及其上的 Java 应用)在整个生命周期内并非只有单一的稳定资源占用。简单来说,应用启动时“较慢”,随后被 JIT 编译器持续优化,直到达到“稳态”——此时应用以完全优化的机器码运行,呈现最佳吞吐量和延迟。从启动到稳态的过程称为预热,其核心权衡在于 JIT 编译器会(一次性)额外消耗 CPU 来执行优化。
图 #1:应用生命周期、JIT 编译器 CPU 消耗与性能之间的关系
每次应用新启动时都会发生预热。在现代 Kubernetes 世界中,这极为常见——根据负载动态创建新实例并停止不必要的实例,已成为高效运行的成本标准。
如果你仅按稳态用量来调整 Kubernetes 容器组的大小,预热就会变长。服务可能在尚未完全优化(代码未完全编译)时就开始接收流量,更糟糕的是,服务还要与 JIT 编译器争夺 CPU。这常常导致延迟不稳定和早期性能不佳。
另一方面,永久分配足够 CPU 来从容应对预热阶段会浪费集群容量。常见情况是:容器仅在头几分钟(预热期间)几乎占满所有可用 CPU,而一旦达到稳态,例如只需 50% 的 CPU 就能轻松处理流量。总之,这会导致过度配置,最终造成资金浪费。
正确的模型简单直观:预热期间分配更多 CPU,等 JVM 完全优化后再减少分配。
图 #2:预热阶段 vs 稳态
Azul Zing(Azul Prime 的核心 JVM)是市场上首个与 Kedify 直接集成的 JVM,使得这种生命周期感知的合理调整变得无缝且易用。
什么是 Azul Prime?
Azul Prime 是一个高性能 JVM 平台,可作为 OpenJDK HotSpot 的即插即用替代品:相同的 API、相同的 TCK 合规性,无需更改应用代码。三项正交的性能技术使其脱颖而出:
- 基于 LLVM 的 Falcon JIT 编译器比 HotSpot 的 C2 生成更激进的优化机器码;
- ReadyNow 通过持久化 JIT 配置文件解决预热问题,使应用启动时即已优化,无需每次从头学习。
- C4(连续并发压缩收集器)是一种真正无暂停的垃圾收集器,可扩展至多 TB 堆内存而无需 Stop-the-World 暂停。
结合可选的 Optimizer Hub 服务(云原生编译器将 JIT 编译卸载到专用服务;ReadyNow Orchestrator 管理整个 JVM 集群的配置文件),Azul Prime 追求两个互补目标:为延迟敏感的 Java 工作负载(金融交易、电子商务、实时分析、Kafka 管道)提供更好、更可预测的性能,并在更广泛的 Java 工作负载中实现有意义的基础设施节省(因为相同负载可在更少的核心上运行)。
PodResourceProfile:无需重启的垂直扩缩
Kubernetes 在 v1.27 引入了原地 Pod 资源调整,自 v1.33 起默认启用。这允许在不重启 Pod 的情况下更新运行中容器的 CPU 和内存请求及限制。
Kedify PodResourceProfile(PRP)构建在此能力之上。
什么是 Kedify?
Kedify 是一个基于 KEDA 构建的企业级自动扩缩平台,旨在使 Kubernetes 扩缩变得可预测、高效且适合生产。
关键能力包括:
- 主动式和预测性自动扩缩:基于真实工作负载信号(HTTP/gRPC、队列、自定义指标),而非滞后的 CPU。
- 多集群自动扩缩与调度:通过单一控制平面管理跨集群工作负载。
- 动态合理调整:结合实时垂直自动扩缩与生命周期感知的资源调整。
- 高级工作负载支持:包括 HTTP 缩容至零和 GPU/LLM 推理扩缩。
- 企业级控制与可见性:包括多租户 KEDA 管理、FinOps 洞察,以及与现有可观测性栈的无缝集成。
Kedify 由 KEDA 的创建者构建,通过更智能的扩缩帮助团队将性能与可靠性提升的同时,将基础设施成本降低 30–40%。
PodResourceProfile 允许你定义一个由工作负载生命周期事件(例如容器就绪时)触发的未来资源调整。
这解锁了一种生命周期感知的垂直扩缩模式:仅在预热期间分配额外 CPU,等 JVM 优化后自动缩减。这是由工作负载生命周期驱动的垂直自动扩缩,而非静态请求或手动调优。
注意:虽然预热调整是一个常见用例,但 PRP 也可以由其他生命周期或需求信号触发,从而在水平扩缩受限或副本数无法增加时实现垂直扩容。如果我们希望基于实际使用而非预定义触发器实现快速垂直扩缩,可以利用 PodResourceAutoscaler,甚至将它们与 PRP 结合使用。
使用 Azul Prime JMX 指标测量 JVM 预热
为了正确调整资源,我们需要一个真实的信号来表明预热已完成。
Azul Prime 通过 JMX 暴露编译活动:
该指标表示编译队列的深度,即还有多少工作需要优化。启动初期该值很高;当 JVM 完成优化后,编译队列深度下降并保持低位。
我们使用此指标作为就绪门控。
代码片段:基于 JMX 的预热探测
以下是就绪探针的核心逻辑:
预热期间,输出如下:
仅当队列深度低于阈值时,Kubernetes 才认为 Pod 就绪。这避免了将流量路由到技术上正在运行但尚未优化的 JVM。
你可以注意到一个额外的微小细节——通过 JMX 调用名为 finishWarmup 的方法。这是 Azul Zing JVM 独特紧密集成的另一部分。
当 JVM 启动时(包括 HotSpot),它会根据可用 CPU 大小调整其内部线程池(例如 JIT 编译器、垃圾收集器等),此调整发生在 JVM 启动时。但这不再适用于垂直缩容的情况。如果我们在预热后减少可用 CPU,但保持原有的编译器线程数,则突然的 JIT 活动峰值(通常由工作负载类型转换引起)可能导致 JVM 消耗不恰当的 CPU,再次影响应用延迟。
通过调用 Zing 的 finishWarmup 方法,Kedify 有效地发出了“让我们缩容”的信号,使 JVM 适当调整内部线程计数。
演示工作负载:强制重度预热
为了清晰展示效果,我们使用了来自 MIT 的 Renaissance 基准测试套件,具体是 finagle-http 基准测试。
该工作负载通过向 Finagle 服务器发送大量小型 HTTP 请求,产生显著的 JIT 活动。它模拟了真实 JVM 服务中存在的预热压力。
步骤 1:以高 CPU 启动
我们以较高的初始 CPU 分配部署工作负载:
这确保 JVM 有足够的算力快速完成编译。
步骤 2:延迟就绪直至预热完成
我们添加一个就绪探针(也可以是启动探针),通过 JMX 查询编译队列深度。当队列仍高于阈值时,就绪检查失败,Kubernetes 不会将流量路由到该 Pod。
一旦编译活动稳定,就绪检查成功,Pod 即可接收流量。
需要注意的是,探针仅控制流量和重启行为。启动探针可以延迟重启,但 PodResourceProfile 才是实际在预热完成后原地调整 CPU 请求和限制的机制。
注:在现实世界中,绝大多数 JIT 编译实际上是在处理负载时触发的。那么如何在就绪探针成功之前完成预热?首先,垂直扩缩(以及调用 finishWarmup)并不与就绪探针绑定——此处仅为演示目的。因此,你可以在就绪探针成功后,在扩展资源下处理一段时间流量,然后再缩容。虽然这种方法已经提供了价值(更快完成预热并为 JIT 编译提供额外资源,最小化对应用延迟的影响),但还可以做得更好。
借助 Azul Zing JVM 的 ReadyNow 技术,你可以更进一步。ReadyNow 持久化先前运行时的编译器配置文件,因此新启动的 JVM 无需从头发现其热点代码路径——它可以在第一个请求到达之前预编译已知需要的方法。结合 ReadyNow Orchestrator,它收集并分发整个 JVM 集群的优化配置文件,你实际上可以在预热窗口期间就达到接近稳态的性能。等到就绪探针成功、流量到来时,JVM 已经真正准备好以全速处理,没有早期延迟代价。
步骤 3:使用 PodResourceProfile 自动缩容 CPU
一旦就绪成功,Kedify 应用一个 PodResourceProfile:
这会将 Pod 从预热配置转换为稳态配置,无需重启。
你可以实时观察变化:
图 #3:资源过渡时间线
指标:由此实现的效果
PodResourceProfile 让你将 JVM 预热视为一个需要临时计算资源的短暂生命周期阶段,然后自动恢复到精简的稳态占用。
使用 Azul Prime,编译活动会引发早期的 CPU 突发。PRP 使这种突发显式化:
- 仅在 JVM 正在编译时提供额外的 CPU
- 在预热完全完成之前,就绪检查一直失败
- 一旦 Pod 可以安全提供流量,资源立即缩减
预热时长取决于工作负载,但 PRP 确保你不会永久为预热 CPU 付费。优化完成后,分配自动回到稳态占用。
结果直截了当:
- 预热更快,因此新副本在滚动更新或扩容期间能更快投入使用
- 稳态效率更高,因为 CPU 不被永久预留
- 流量门控更可靠,因为 Pod 仅在真正优化后才进入服务轮换
典型的过渡如下:
- 预热分配:10 个 CPU 核心
- 稳态分配:4 个 CPU 核心
- 预热后回收容量:约 60%
具体数字取决于工作负载行为,但操作模式是一致的:快速预热、就绪门控流量、精简稳态配置。
垂直与水平扩缩如何协同工作
PodResourceProfile 解决每个容器的配置问题,而水平自动扩缩解决副本数量问题。两者结合效果最佳。
水平扩缩根据主动的需求信号(如请求率或消息队列深度)提前添加新副本。这确保在 CPU 饱和变得可见之前就创建了容量。
每个新的 Java Pod 仍需要预热。PodResourceProfile 对每个新建的 Pod 应用垂直调整,在 JVM 编译和优化期间给予其高 CPU。一旦 Pod 真正就绪,资源自动减少到稳态占用。
在实践中,流程如下:
- 基于工作负载指标(每秒请求数、消息队列深度)进行水平扩容。
- 对每个新 Pod 应用 PRP 垂直 CPU 提升,以应对预热。
- 预热后缩减资源,避免长期过度配置。
- 在多个优化后的副本上高效运行实际工作负载。
亲自尝试
若要尝试此方法:
- 获取 Azul Prime。
- 启用原地 Pod 资源调整(Kubernetes v1.33+ 默认启用)。
- 在 Kubernetes 集群上安装 Kedify。
- 部署具有预热感知就绪或启动探针的工作负载。
- 应用 PodResourceProfile 以在预热后缩减资源。
这样你就拥有了一种声明式的方法,将预热视为一等生命周期阶段,而不是永久的配置决策。
总结
Java 预热是一个具有独特 CPU 需求的独立阶段。将 Pod 资源视为静态迫使团队在过度配置与早期性能下降之间做出取舍。
借助 Azul Zing 暴露 JIT 编译进度、支持对资源变化做出反应,以及 Kedify PodResourceProfile 实现原地垂直扩缩,Kubernetes 可以在预热期间分配额外 CPU,并在 Pod 真正就绪后释放它。结果是启动更快、延迟更稳定、稳态集群效率更高。
如果你在 Kubernetes 上运行 JVM 服务,这值得在具有明显预热行为的工作负载上尝试。如需帮助进行基准测试或在生产环境中应用 PodResourceProfile,请联系 Kedify 团队。
快速预热。仅在就绪时提供服务。之后精简运行。