⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 www.cnblogs.com/east4ming/p/17034195.html 「东风微鸣」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

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

前言

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

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

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

内存溢出异常时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将立即退出。如果您想终止应用程序,则可以传递此参数。

细节

让我们重新回顾故障:“我们公司的某个手机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虽然有助于问题分析,但在容器的快速运维模式下可能不是最佳选择。

文章目录
  1. 1. 前言
  2. 2. 内存溢出异常时Dump VS 内存溢出时退出
  3. 3. 细节
  4. 4. 新的疑问
  5. 5. 总结