专栏名称: 芋道源码
纯 Java 源码分享公众号,目前有「Dubbo」「SpringCloud」「Java 并发」「RocketMQ」「Sharding-JDBC」「MyCAT」「Elastic-Job」「SkyWalking」「Spring」等等
目录
相关文章推荐
芋道源码  ·  Out of ... ·  昨天  
Java编程精选  ·  泛型中的 T、E、K、V,还记得嘛? ·  4 天前  
Java编程精选  ·  CEO裁员后不理解:原来100个人干50个人 ... ·  5 天前  
Java编程精选  ·  Spring Boot ... ·  1 周前  
芋道源码  ·  阿里也出手了!Spring Cloud ... ·  6 天前  
51好读  ›  专栏  ›  芋道源码

Out of Memory?别怕!这个Java容器技巧让你的应用永不宕机!

芋道源码  · 公众号  · Java  · 2024-10-22 09:30

正文

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:www.cnblogs.com/east4
ming/p/17034195.html


为什么 java 容器推荐使用ExitOnOutOfMemoryError而非HeapDumpOnOutOfMemoryError? 今天我们一起来聊聊这个问题。

前言

最近,我们公司的某个应用后端的用户微服务频繁的出现内存泄露,导致OutOfMemoryError,导致经常会发生服务不可用。这对于 toC 场景来说,简直就是灾难性的。

于是,我们决定对 openjdk 的容器参数进行精心优化,通过一些优化,其后面再次发生故障时,对用户完全无感知 💪💪💪

那么我们是如何做到的呢?

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

内存溢出异常时Dump VS 内存溢出时退出

HeapDumpOnOutOfMemoryError VS ExitOnOutOfMemoryError

在传统的虚拟机环境中部署Java应用时,为了便于问题诊断和分析,通常会在JVM启动参数中加入-XX:+HeapDumpOnOutOfMemoryError。这个参数的作用是在Java虚拟机发生内存溢出(OutOfMemoryError)时,自动触发堆转储(HeapDump)的生成。通过这种方式,开发者和运维人员可以在事后获取到一个详细的堆转储文件,其中包含了内存溢出时的内存使用情况和对象信息。

这个堆转储文件对于分析和定位内存泄漏、内存溢出等问题至关重要。通过分析HeapDump,可以查看到各个对象的内存占用情况,识别出内存使用异常的代码路径,以及追踪到具体的代码行。此外,还可以通过HeapDump来分析垃圾回收(GC)的行为,了解不同垃圾回收代的内存使用情况,以及识别出可能导致GC性能问题的潜在原因。

然而,需要注意的是,生成HeapDump是一个资源密集型的操作,可能会占用大量的磁盘空间,并且可能会延长应用的恢复时间。因此,在配置HeapDump时,还需要考虑磁盘空间的容量和性能影响。此外,为了保护敏感信息,还需要确保HeapDump文件中不包含敏感数据,或者在分析完成后及时删除这些文件。

也就是说,在传统的虚拟机环境中,通过配置-XX:+HeapDumpOnOutOfMemoryError参数,可以在Java应用发生内存溢出时自动生成HeapDump,为后续的问题诊断和分析提供重要的数据支持。但同时,也需要权衡HeapDump对资源的占用和性能影响,并采取相应的安全措施来保护敏感信息。通过综合考虑这些因素,可以更有效地利用HeapDump来提升Java应用的稳定性和性能。

但是,“大人,时代变了!”

容器技术的发展,给传统运维模式带来了巨大的挑战,这个挑战是革命性的:

  1. 传统的应用都是“永久存在的” vs 容器pod是“短暂临时的存在”
  2. 传统应用扩缩容相对困难 vs 容器扩缩容丝般顺滑
  3. 传统应用运维模式关注点是:“定位问题” vs 容器运维模式是:“快速恢复”
  4. 传统应用一个实例报HeapDumpError就会少一个 vs 容器HeapDump shutdown后可以自动启动,已达到指定副本数
  5. ...

简单总结一下,在使用容器平台后 ,我们的工作倾向于:

  1. 遇到故障快速失败
  2. 遇到故障快速恢复
  3. 尽量做到用户对故障“无感知”

所以,针对Java应用容器,我们也要优化以满足这种需求,以 OutOfMemoryError 故障为例:

  1. 遇到故障快速失败,即尽可能“快速退出,快速终结”
  2. 有问题java应用容器实例退出后,新的实例迅速启动填补;
  3. “快速退出,快速终结”,同时配合LB,退出和冷启动的过程中用户请求不会分发进来。

-XX:+ExitOnOutOfMemoryError 就正好满足这种需求:

传递此参数时,抛出OutOfMemoryError时JVM将立即退出。如果您想终止应用程序,则可以传递此参数。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

细节

让我们重新回顾故障:“我们公司的某个手机APP后端的用户(customer)微服务出现内存泄露,导致OutOfMemoryError”

该 customer 应用概述如下:

  1. 无状态
  2. 通过 Deployment 部署,有 6 个副本
  3. 通过 SVC 提供服务

完整的过程如下:

  1. 6 个副本,其中 1 个出现 OutOfMomoryError
  2. 因为副本的 jvm 参数配置有:-XX:+ExitOnOutOfMemoryError,该实例的 JVM(PID 为 1)立即退出。
  3. 因为 pid 1 进程退出,此时 pod 立刻出于 Terminating 状态,并且变为:Terminated
  4. 同时,customer 的 SVC 负载均衡会将该副本从SVC 负载均衡中移除,用户请求不会被分发到该节点。
  5. K8S检测到副本数和 Deployment replicas 不一致,启动1个新的副本。
  6. 待新的部分 Readiness Probe 探测通过,customer 的 SVC 负载均衡将这个新的副本加入到负载均衡中,接收用户请求。

在此过程中,用户基本上是对后台故障“无感知”的。

当然,要做到这些,其实JVM参数以及启动脚本中,还有很多细节和门道。如:启动脚本应该是:exec java ....$*

新的疑问

上面,我们解释了《为什么 Java 容器推荐使用 ExitOnOutOfMemoryError 而非 HeapDumpOnOutOfMemoryError》,但是细心的小伙伴也会发现,新的配置也会带来新的问题,比如:

  1. JVM 从 fullgc -> OutOfMemoryError 这段时间内,用户的体验还是会下降的,怎么会是“故障无感知”呢?
  2. ExitOnOutOfMemoryError代替HeapDumpOnOutOfMemoryError,那我怎么定位该问题的根因并解决?2 个参数一起用不是更香么?

这些其实可以通过其他手段来解决:

  • JVM 从 fullgc -> OutOfMemoryError 这段时间内,用户的体验还是会下降的,怎么会是“故障无感知”呢?

答:

在容器化应用的运维实践中,合理配置Readiness Probe对于确保服务的高可用性至关重要。Readiness Probe的主要作用是检测应用是否已经准备好接收流量。当Readiness Probe探测失败时,Kubernetes会自动将该容器实例从服务的负载均衡池中移除,防止未准备好的实例影响整体服务的稳定性和响应时间。

为了实现这一目标,Readiness Probe的配置需要精心设计,以确保在应用不可用时,Probe能够准确反映应用状态。通常,仅仅检查端口是否开放是不够的,因为即使端口开放,应用也可能因为内部错误而无法正常处理请求。因此,更合理的方法是探测应用的特定API端点,这些端点能够更准确地反映应用的实际可用性。例如,可以配置Readiness Probe去调用一个特定的健康检查API,该API会检查应用的内部状态和依赖服务的连通性,从而提供更全面的可用性信息。

除了Readiness Probe,还可以利用Prometheus JVM Exporter、Prometheus监控系统和AlertManager的组合来进一步增强应用的监控和预警能力。通过配置合理的AlertRule,比如设置“在过去X分钟内,GC(垃圾回收)总时间超过5秒”的规则,可以在潜在的性能问题演变成严重故障之前发出告警。这样的告警机制可以帮助运维团队及时发现并介入处理问题,从而避免服务中断或性能下降。

总的来说,通过结合使用Readiness Probe和先进的监控工具,可以有效地提高容器化应用的稳定性和可靠性,确保在应用出现问题时能够快速响应和恢复。这种综合的监控和响应策略,是现代云原生应用运维的最佳实践之一。

  • ExitOnOutOfMemoryError代替HeapDumpOnOutOfMemoryError,那我怎么定位该问题的根因并解决?2 个参数一起用不是更香么?

答:

在容器化的应用环境中,我们通常追求的是快速失败和快速恢复,以此来确保服务的高可用性。在Java应用中,当遇到内存溢出(OutOfMemoryError)时,-XX:+ExitOnOutOfMemoryError 参数能够确保JVM立即退出,避免在生成堆转储(HeapDump)时消耗的额外时间,这段时间可能会导致服务体验进一步下降。因此,我们更倾向于使用 ExitOnOutOfMemoryError 来实现快速退出和终结,以最小化对用户体验的影响。

为了在应用出现内存溢出时快速恢复服务,我们可以依赖于Kubernetes的探针机制,特别是就绪探针(Readiness Probe)。通过配置合理的就绪探针,当应用不可用时,探针探测失败,Kubernetes会自动将该节点从服务负载均衡中摘除,确保用户请求不会被分发到不可用的实例。这样的探针配置应该能够准确反映应用的可用性,而不仅仅是检查端口是否在监听,而是应该探测应用的特定API端点是否正常响应。

在问题诊断方面,虽然HeapDumpOnOutOfMemoryError可以生成堆转储文件以供后续分析,但在容器环境中,我们更推荐使用其他监控手段。例如,通过嵌入分布式追踪代理(Tracing agent),我们可以收集和分析故障发生时的追踪信息,从而定位问题的根本原因。此外,结合Prometheus JVM Exporter、Prometheus监控系统和AlertManager,我们可以设置合理的告警规则,如GC总时间超过阈值时触发告警。在告警触发后,运维人员可以手动介入,使用jcmd等命令工具执行堆转储操作,以便进一步分析和解决问题。

各种平衡之后,再来看这个问题。就是在容器化的应用环境中,我们更倾向于使用ExitOnOutOfMemoryError来实现快速的失败和恢复,同时通过监控和探针机制来确保服务的高可用性,并通过其他手段进行问题诊断,而不是依赖于在OOM时生成堆转储。这种方法更符合容器化应用的运维模式,有助于实现快速响应和问题恢复。

readinessProbe:
  httpGet:
    path: /actuator/info
    port: 8088
    scheme: HTTP
  initialDelaySeconds: 60
  timeoutSeconds: 3
  periodSeconds: 10
  successThreshold: 1
  failureThreshold: 3

在容器时代,推荐使用-XX:+ExitOnOutOfMemoryError而非-XX:+HeapDumpOnOutOfMemoryError的主要原因与容器技术的特点和运维模式的转变有关。以下是几个关键点:

  1. 快速失败与快速恢复 :容器技术的一个核心优势是其快速的启动和停止能力。在容器环境中,当应用出现内存泄漏或溢出时,快速失败并迅速启动新实例可以更快地恢复服务,而不是花费时间生成堆转储文件进行问题分析。
  2. 容器的短暂性 :与传统的虚拟机部署相比,容器被设计为短暂和临时的存在。它们通过快速的扩缩容来适应负载变化,而不是长期运行单个实例。
  3. 用户无感知 :在容器环境中,通过配置如Kubernetes的Readiness Probe和Liveness Probe,可以确保在实例出现问题时,不会将用户请求分发到该实例,并且在实例恢复后迅速重新加入服务,实现用户对故障的无感知。
  4. 避免在OOM期间的资源消耗 :生成堆转储(HeapDump)是一个资源密集型操作,可能会导致服务进一步恶化或延长故障恢复时间。使用ExitOnOutOfMemoryError可以立即终止进程,避免在OOM情况下继续消耗资源。
  5. 故障分析的其他手段 :即使没有堆转储文件,也可以通过其他监控和日志分析工具来定位和分析问题。例如,使用Prometheus JVM Exporter、Prometheus和AlertManager进行监控,可以在GC时间超过阈值时发出告警,从而提前介入处理问题。

总结

新的技术带来新的变革,我们需要以发展的眼光看待“最佳实践,最佳配置”。

过去(2016 年以前),针对虚机部署的 Java 的最优参数,在今天来看,并不一定仍是最优解。

-XX:+ExitOnOutOfMemoryError在容器环境中更符合快速恢复和高可用性的需求,而-XX:+HeapDumpOnOutOfMemoryError虽然有助于问题分析,但在容器的快速运维模式下可能不是最佳选择。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)