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

科技   2024-11-26 15:33   广东  

来源:www.cnblogs.com/east4ming/p/17034195.html

为什么 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虽然有助于问题分析,但在容器的快速运维模式下可能不是最佳选择。

推荐全新学习项目
全新基于springboot+vue+vant的前后端分离的微商城项目,包括手机端微商城项目和后台管理系统,整个电商购物流程已经能流畅支持,涵盖商品浏览、搜索、商品评论、商品规格选择、加入购物车、立即购买、下单、订单支付、后台发货、退货等。功能强大,主流技术栈,非常值得学习。

项目包含2个版本:

  • 基于springboot的单体版本

  • 基于spring cloud alibaba的微服务版本


线上演示:https://www.markerhub.com/vueshop

从文档到视频、接口调试、学习看板等方面,让项目学习更加容易,内容更加沉淀。全套视频教程约44小时共260期,讲解非常详细细腻。下面详细为大家介绍:

架构与业务

使用主流的技术架构,真正手把手教你从0到1如何搭建项目手脚架、项目架构分析、建表逻辑、业务分析、实现等。

单体版本:springboot 2.7、mybatis plus、rabbitmq、elasticsearch、redis

微服务版本:spring cloud alibaba 2021.0.5.0,nacos、seata、openFeign、sentinel

前端:vue 3.2、element plus、vant ui

更多详情请查看:

手把手教学,从0开发前后端微商城项目,主流Java技术一网打尽!

java思维导图
梳理知识,学习与思考,让java不再难懂。
 最新文章