JVM预热这个顽疾,我在k8s上终于改进了

2023-11-22 12:10

本文主要是介绍JVM预热这个顽疾,我在k8s上终于改进了,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

JVM 预热是一个非常头疼而又难解决的问题。本文讨论了在运行在 Kubernetes 集群中的 Java 服务如何解决 JVM 预热问题的一些方法和经验。

JVM 预热是一个非常头疼而又难解决的问题。基于 JVM 的应用程序在达到最高性能之前,需要一些时间来“预热”。当应用程序启动时,通常会从较低的性能开始。这归因于像即时(JIT)编译这些事儿,它会通过收集使用配置文件信息来优化常用代码。最终这样的负面影响是,与平均水平相比,预热期间接收的 request 将具有非常高的响应时间。在容器化、高吞吐量、频繁部署和自动伸缩的环境中,这个问题可能会加剧。

在这篇文章中,我们将讨论在运行在 Kubernetes 集群中的 Java 服务如何解决 JVM 预热问题的经验

起因

几年前,我们逐步从整体中分离出服务,开始在 Kubernetes 上进行迁移到基于微服务的体系结构。大多数新服务都是在 Java 中开发的。当我们在印度市场上运行一个这样的服务时,我们第一次遇到了这个问题。我们通过负载测试进行了通常的容量规划过程,并确定 N 个 Pod 足以处理超过预期的峰值流量。

尽管该服务在轻松处理高峰流量,但我们在部署过程中发现了问题。我们的每个 Pod 在高峰时间处理的 RPM 都超过 10k,而我们使用的是 Kubernetes 滚动更新机制。在部署过程中,服务的响应时间会激增几分钟,然后再稳定到通常的稳定状态。在我们的仪表板中,会看到类似的图表:

与此同时,我们开始收到来自部署时间段内的大量投诉,几乎都关于高响应时间和超时错误。

第一步:花钱解决问题

我们很快意识到这个问题与 JVM 预热阶段有关,但当时有其他的重要事情,因此我们没有太多时间进行调查,直接尝试了最简单的解决方案——增加 Pod 数量,以减少每个 Pod 的吞吐量。我们将 Pod 数量增加了近三倍,以便每个 Pod 在峰值处理约 4k RPM 的吞吐量。我们还调整了部署策略,以确保一次最多滚动更新 25%(使用 maxSurge 和 maxUnavailable 参数)。这样就解决了问题,尽管我们的运行容量是稳定状态所需容量的 3 倍,但我们能够在我们的服务中或任何相关服务中没有问题地进行部署。

随着后面几个月里更多的迁移服务,我们开始在其他服务中常常看到这个问题。因此我们决定花一些时间来调查这个问题并找到更好的解决方案。

第二步:预热脚本

在仔细阅读了各种文章后,我们决定尝试一下预热脚本。我们的想法是运行一个预热脚本,向服务发送几分钟的综合请求,来完成 JVM 预热,然后再允许实际流量通过

为了创建预热脚本,我们从生产流量中抓取了实际的 URL。然后,我们创建了一个 Python 脚本,使用这些 URL 发送并行请求。我们相应地配置了 readiness 探针的 initialDelaySeconds,以确保预热脚本在 Pod 为 ready 并开始接受流量之前完成。

令人吃惊的是,尽管结果有一些改进,但并不显著。我们仍然经常观察到高响应时间和错误。此外,预热脚本还带来了新的问题。之前,Pod 可以在 40-50 秒内准备就绪,但用了脚本,它们大约需要 3 分钟,这在部署期间成为了一个问题,更别说在自动伸缩期间。我们在预热机制上做了一些调整,比如允许预热脚本和实际流量有一个短暂的重叠期,但也没有看到显著的改进。最后,我们认为预热脚本的收益太小了,决定放弃。

第三步:启发式发现

由于预热脚本想法失败了,我们决定尝试一些启发式技术-

  • GC(G1、CMS 和 并行)和各种 GC 参数
  • 堆内存
  • CPU 分配

经过几轮实验,我们终于取得了突破。测试的服务配置了 Kubernetes 资源 limits:

我们将 CPU request 和 limit 增加到 2000m,并部署服务以查看影响,可以看到响应时间和错误有了巨大的改进,比预热脚本好得多。

第一个 Deployment(大约下午 1 点)使用 2 个 CPU 配置,第二个 Deployment (大约下午 1:25)使用原来 1 个 CPU 配置

为了进一步测试,我们将配置升级到 3000m CPU,令我们惊讶的是,问题完全消失了。正如下面看到的,响应时间没有峰值。

具有 3 个 CPU 配置的 Deployment

很快,我们就发现问题出在 CPU 节流上。在预热阶段,JVM 需要比平均稳定状态下更多的 CPU 时间,但 Kubernetes 资源处理机制(CGroup)根据配置的 limits,从而限制了 CPU。

有一个简单的方法可以验证这一点。Kubernetes 公开了一个每个 Pod 的指标,
container_cpu_cfs_throttled_seconds_total 表示这个 Pod 从开始到现在限制了多少秒 CPU。如果我们用 1000m 配置观察这个指标,应该会在开始时看到很多节流,然后在几分钟后稳定下来。我们使用该配置进行了部署,这是 Prometheus 中所有 Pod 的
container_cpu_cfs_throttled_seconds_total 图:

正如预期,在容器启动的前 5 到 7 分钟有很多节流,大部分在 500 秒到 1000 秒之间,然后稳定下来,这证实了我们的假设。

当我们使用 3000m CPU 配置进行部署时,观察到下图:

CPU 节流几乎可以忽略不计(几乎所有 Pod 都不到 4 秒)。

第四步:改进

尽管我们发现了这个问题的根本,但就成本而言,该解决方案并不太理想。因为有这个问题的大多数服务都已经有类似的资源配置,并且在 Pod 数量上超额配置,以避免部署失败,但是没有一个团队有将 CPU 的 request、limits 增加三倍并相应减少 Pod 数量的想法。这种解决方案实际上可能比运行更多的 Pod 更糟糕,因为 Kubernetes 会根据 request 调度 Pod,找到具有 3 个空闲 CPU 容量的节点比找到具有 1 个空闲 CPU 的节点要困难得多。它可能导致集群自动伸缩器频繁触发,从而向集群添加更多节点。

我们又回到了原点 但是这次有了一些新的重要信息。现在问题是这样的:

在最初的预热阶段(持续几分钟),JVM 需要比配置的 limits(1000m)更多的 CPU(大约 3000m)。预热后,即使 CPU limits 为 1000m,JVM 也可以充分发挥其潜力。Kubernetes 会使用 request 而不是 limits 来调度 Pod。我们清楚地了解问题后,答案就出现了——Kubernetes Burstable QoS。

Kubernetes 根据配置的资源 request 和 limits 将 QoS 类分配给 Pod。

到目前为止,我们一直在通过指定具有相等值的 request 和 limits(最初是 1000m,然后是 3000m)来使用 Guaranteed QoS。尽管 Guaranteed QoS 有它的好处,但我们不需要在整个 Pod 生命周期中独占 3 个 CPU,我们只需要在最初的几分钟内使用它。Burstable QoS 允许我们指定小于 limits 的 request,例如:

由于 Kubernetes 使用 request 中指定的值来调度 Pod,它会找到具有 1000m CPU 容量的节点来调度这个 Pod。但是由于 3000m 的 limits 要高得多,如果应用程序在任何时候都需要超过 1000m 的 CPU,并且该节点上有空闲的 CPU 容量,那么就不会在 CPU 上限制应用程序。如果可用,它最多可以使用 3000m。

这非常符合我们的问题。在预热阶段,当 JVM 需要更多的 CPU 时,它可以获取需要的 CPU。JVM 被优化后,可以在 request 范围内全速运行。这允许我们使用集群中的冗余的资源(足够可用时)来解决预热问题,而不需要任何额外的成本。

最后,进行假设测试。我们更改了资源配置并部署了应用程序,成功了!我们做了更多的测试以验证结果一致。此外,我们监控了
container_cpu_cfs_throttled_seconds_total 指标,以下是其中一个 Deployment 的图表:

正如我们所看到的,这张图与 3000m CPU 的 Guaranteed QoS 设置非常相似。节流几乎可以忽略不计,它证实了具有 Burstable QoS 的解决方案是有效的。

为了使 Burstable QoS 解决方案正常工作,节点上需要有可用的冗余资源。这可以通过两种方式验证:

就 CPU 而言,节点资源未完全耗尽;

工作负载未使用 request 的 100% CPU。

结论

尽管花了一些时间,最终找到了一个成本效益高的解决方案。Kubernetes 资源限制是一个重要的概念。我们在所有基于 Java 的服务中实现了该解决方案,部署和自动扩展都运行良好,没有任何问题。

要点:

在为应用程序设置资源限制时要仔细考虑。花些时间了解应用程序的工作负载并相应地设置 request 和 limits。了解设置资源限制和各种 QoS 类的含义。

通过
monitoring/alertingcontainer_cpu_cfs_throttled_seconds_total 来关注 CPU 节流。如果观察到过多的节流,可以调整资源限制。

使用 Burstable QoS 时,确保在 request 中指定了稳定状态所需的容量。

 

这篇关于JVM预热这个顽疾,我在k8s上终于改进了的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/410055

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows