侧边栏壁纸
博主头像
汪洋

即使慢,驰而不息,纵会落后,纵会失败,但一定可以达到他所向的目标。 - 鲁迅

  • 累计撰写 204 篇文章
  • 累计创建 79 个标签
  • 累计收到 128 条评论

在 Kubernetes 上优化 Java 性能

汪洋
2024-07-22 / 0 评论 / 0 点赞 / 108 阅读 / 3,282 字

相应地设置 CPU 和 RAM 限制

除了使用 -Xmx、-XX:MaxRAM、-XX:+UseStringDeduplication 等选项优化 JVM 内存占用之外。你应该为 Kubernetes 中的 pod 和容器使用的 CPU 和 RAM 设置请求和限制,建议在请求和限制中使用相同的值。

但是,为应用程序选择适当的内存限制就像是在Scylla和Charybdis之间徘徊:FinOps要求缩减资源使用量以降低云账单,但与此同时,满足与用户或客户签订的服务级别协议(SLA)可能需要更多资源。幸运的是,以下建议将帮助您安全地驾驭这些水域:

  • 使用负载测试了解应用程序在正常条件下的行为,使用压力测试测量最高性能时的资源消耗。相应调整设置。不要将限制设置得太低。即使应用程序在稳定负载下消耗的资源较少,它在预热和峰值负载时也需要更多的 CPU
  • 确定满足 SLO 的最低要求(开发人员为满足 SLA 而应达到的服务级别目标)
  • 利用工具研究相关指标,如内存消耗。例如,kubectl top pod 可提供 pod 内部内存使用情况的数据,而 jcmd GC.heap_info 则可提供堆使用情况的信息
  • 除 GC 日志外,还可使用本地内存跟踪来了解应用程序的实际内存使用量
  • 将 Kubernetes 的开销考虑在内。pod 开销是指它在节点上运行时使用的资源。例如,Fargate pod 会在每个 pod 的内存预留中增加 256 MB,用于必要的 K8s 组件

配置不当会导致

  • 过度使用:当服务耗尽所有可用内存时,应用程序会不断执行垃圾回收,我们会添加更多实例以恢复正常运行
  • 利用不足,即应用程序没有使用所有可用内存,我们不得不启动更多实例,造成资源浪费

区分服务类型

在进行优化时,始终要考虑服务类型。有些服务不太关键,有些则非常关键,因此对性能的要求也不同。不太关键的服务

  • 有适度的 RTO(恢复时间目标)要求
  • 具有可突发的服务质量(QoS)等级,这意味着 pod 根据容器请求提供较低的资源保证,并且不要求特定的内存限制(但 pod 中至少有一个容器必须有内存或 CPU 请求/限制)

高度关键服务

  • 有严格的 RTO 要求
  • 高度灵活,专为应对指数级增长而设计
  • 具有 "保证 QoS "类,即 pod 有严格的资源限制,并保证在超出限制之前不会被杀死。此类 pod 中的所有容器都必须有 CPU 限制/请求和内存限制/请求

因此,应按服务确定性能要求。

请注意,Java 应用程序不能很好地处理垂直扩展,更适合水平扩展。这意味着请求和限制应基于峰值性能数据。此外,扩展策略不应仅基于 CPU 和 RAM 指标。有时,延迟或吞吐量对给定的应用程序更为重要。

有效使用 Kubernetes 探测器

kubelet 使用的 Kubernetes 探测器对于收集容器健康状况的信息至关重要。另一方面,不正确的配置可能会导致性能下降和不必要的扩展。

Kubernetes 探测器有三种类型:

  • 启动探针确定容器化应用程序是否已启动。在启动探针确认成功启动之前,其他探针将被关闭。
  • 有效性探针可确定容器是否正在运行。如果没有,它会向 kubelet 发出重启信号。
  • 就绪探测器决定容器何时可以接受网络请求。

这些探针可以更好地协同工作。例如,启动探针非常适合启动缓慢的容器,否则,除非适当设置 timeoutSeconds,否则有效性探针可能会提前杀死容器。准备就绪探针可能会说容器运行正常,但实际上应用程序正处于死锁状态,而这只有有效性探针才能识别。

包括 Spring Boot 在内的主要 Java 框架都支持 Kubernetes 探测器配置和自动配置。使用 Spring Boot,您需要在 pom.xml 文件中添加 spring-boot-starter-actuator 依赖关系。如果在 application.properties 中将 management.health.probes.enabled 属性设为 true(或从 Spring 2.3.2 开始将 management.endpoint.health.probes.enabled=true 设为 true),Spring Boot 就会自动注册有效性和就绪性探针。

然后,您可以根据工作负载调整探针设置。正确的配置可以避免频繁和不必要的容器重启或其他问题。例如,假设探针等待的时间不够长(例如,当您将响应时间限制设置得过低时),并返回负响应。在这种情况下,Kubernetes 自动调节器可能会认为需要额外的 pod,并执行不需要的垂直扩展,从而浪费资源。

升级 Java 版本

即使您使用定期更新的 JDK 映像,也还不是晒太阳的时候。升级 Java 版本的重要性丝毫不减,因为 JVM 的整体性能随着 JDK 的发布而不断提高。例如,从 JDK 9 开始,Java 的容器感知能力越来越强:

  • JDK 10+ 版本具有更好的 Docker 容器检测算法,允许更好地使用资源配置,并能更灵活地根据可用内存调整堆百分比
  • 11+ 版本收集并使用 cgroups v1 数据
  • 17+ 和 11u 版本支持 cgroups v2

等等。此外,新版本对垃圾回收进行了大量改进,对 KPI 影响很大。但是,尽管一些修复会向旧版本的 Java 版本回传,但每次新版本发布后,旧版本 LTS 版本中的改进却越来越少。因此,升级 JDK 对于优化 Java 性能至关重要,不仅是在 Kubernetes 中。

但是,如果您现在不能迁移到更新的 Java 版本怎么办?毕竟,迁移需要解决兼容性问题,有时还要重新编写大量代码。

选择合适的垃圾回收器

Java 平台提供了多种针对特定工作负载的垃圾回收器,旨在改善相关的 KPI。例如

  • ParallelGC 适用于高通量应用
  • G1GC 旨在减少延迟
  • ZGC 是一种并发垃圾回收器,这意味着所有繁重的工作都会在 Java 线程继续执行时完成
  • ShenandoahGC(不包含在 Oracle Java 中,但随 OpenJDK 发行版(包括 Liberica JDK)一起提供)的重点是,即使堆很大,也要保持较短的暂停时间

我们的目标是为 Kubernetes 集群选择合适的收集器--在大多数情况下,它足以提高性能,无需对 GC 进行细致的调整。

此外,开发人员应避免自动切换 SerialGC。假设您将 -Xmx 参数设置为 2 Gb 或更小,并将应用程序限制在两个处理器以下。在这种情况下,SerialGC 将自动切换(即使在 JVM 设置中明确指定另一个收集器也无法弥补这种情况)。对于单 CPU 机器和在内存极其紧张的环境中运行的应用程序来说,SerialGC 可能是最佳选择,但在其他使用情况下可能会导致性能显著下降。

因此,不要将限制设置得太低,或使用 -XX:+AlwaysActAsServerClassMachine 来防止自动使用 SerialGC。

使用较小的基础操作系统图像

最小化容器的大小对于优化 Kubernetes 集群的资源消耗和控制云成本至关重要。虽然有多种技术能让开发人员保持容器的整洁和精简,但最优先的步骤还是选择一个最小化的基础操作系统映像。这样,您就能立即缩小容器大小,而无需费力地配置 JVM 内存或从操作系统映像中剥离不必要的软件包。

最适合云计算的轻量级 Linux 发行版是 Alpine 和 Alpaquita Linux。两者的基本镜像大小都小于 4MB(使用 APK 工具可以轻松安装附加软件包)。不过,100% 与 Alpine 兼容的 Alpaquita 有几个显著特点,非常适合企业 Java 开发:

  • 两个 libc 实现,即优化的 musl 和 glibc,以提高性能和实现无缝迁移
  • 额外的内核加固和定期更新可实现最佳安全性
  • 促进 Java 开发的工具和适用于各种 Java 工作负载的四种 malloc
  • 为 Spring 开发人员量身打造了 Alpaquita 容器,旨在将 Spring Boot 应用程序的内存消耗量最多减少 30%

缩短启动和预热时间

在使用云运行类似服务时,减少应用程序启动时间至关重要。如果您的云提供商向您收取 CPU 时间的费用,这也是至关重要的。此外,JVM 预热会增加内存消耗,因此您必须为实例分配更多的内存,而这些内存以后将不会被使用。

有几种方法可以减少 Java 应用程序的启动,包括 AppCDS、AOT 编译和一些全新的解决方案,其中一种将集成到即将发布的 JDK 小版本中。

0

评论区