摘要:最近,我们公司的某个应用后端的用户微服务频繁的出现内存泄露,导致OutOfMemoryError,导致经常会发生服务不可用。这对于 toC 场景来说,简直就是灾难性的。
最近,我们公司的某个应用后端的用户微服务频繁的出现内存泄露,导致OutOfMemoryError,导致经常会发生服务不可用。这对于 toC 场景来说,简直就是灾难性的。
于是,我们决定对 openjdk 的容器参数进行精心优化,通过一些优化,其后面再次发生故障时,对用户完全无感知
那么我们是如何做到的呢?
HeapDumpOnOutOfMemoryError VS ExitOnOutOfMemoryError。
在传统的虚拟机环境中部署java应用时,为了便于问题诊断和分析,通常会在JVM启动参数中加入-XX:+HeapDumpOnOutOfMemoryError。这个参数的作用是在Java虚拟机发生内存溢出(OutOfMemoryError)时,自动触发堆转储(HeapDump)的生成。通过这种方式,开发者和运维人员可以在事后获取到一个详细的堆转储文件,其中包含了内存溢出时的内存使用情况和对象信息。
这个堆转储文件对于分析和定位内存泄漏、内存溢出等问题至关重要。通过分析HeapDump,可以查看到各个对象的内存占用情况,识别出内存使用异常的代码路径,以及追踪到具体的代码行。此外,还可以通过HeapDump来分析垃圾回收(GC)的行为,了解不同垃圾回收代的内存使用情况,以及识别出可能导致GC性能问题的潜在原因。
然而,需要注意的是,生成HeapDump是一个资源密集型的操作,可能会占用大量的磁盘空间,并且可能会延长应用的恢复时间。因此,在配置HeapDump时,还需要考虑磁盘空间的容量和性能影响。此外,为了保护敏感信息,还需要确保HeapDump文件中不包含敏感数据,或者在分析完成后及时删除这些文件。
也就是说,在传统的虚拟机环境中,通过配置-XX:+HeapDumpOnOutOfMemoryError参数,可以在Java应用发生内存溢出时自动生成HeapDump,为后续的问题诊断和分析提供重要的数据支持。但同时,也需要权衡HeapDump对资源的占用和性能影响,并采取相应的安全措施来保护敏感信息。通过综合考虑这些因素,可以更有效地利用HeapDump来提升Java应用的稳定性和性能。
但是,“大人,时代变了!”
容器技术的发展,给传统运维模式带来了巨大的挑战,这个挑战是革命性的:
传统的应用都是“永久存在的” vs 容器pod是“短暂临时的存在”传统应用扩缩容相对困难 vs 容器扩缩容丝般顺滑传统应用运维模式关注点是:“定位问题” vs 容器运维模式是:“快速恢复”传统应用一个实例报HeapDumpError就会少一个 vs 容器HeapDump shutdown后可以自动启动,已达到指定副本数...简单总结一下,在使用容器平台后,我们的工作倾向于:
遇到故障快速失败遇到故障快速恢复尽量做到用户对故障“无感知”所以,针对Java应用容器,我们也要优化以满足这种需求,以 OutOfMemoryError 故障为例:
遇到故障快速失败,即尽可能“快速退出,快速终结”有问题java应用容器实例退出后,新的实例迅速启动填补;“快速退出,快速终结”,同时配合LB,退出和冷启动的过程中用户请求不会分发进来。-XX:+ExitOnOutOfMemoryError 就正好满足这种需求:
传递此参数时,抛出OutOfMemoryError时JVM将立即退出。如果您想终止应用程序,则可以传递此参数。
让我们重新回顾故障:“我们公司的某个手机APP后端的用户(customer)微服务出现内存泄露,导致OutOfMemoryError”
该 customer 应用概述如下:
无状态通过 Deployment 部署,有 6 个副本通过 SVC 提供服务完整的过程如下:
6 个副本,其中 1 个出现 OutOfMomoryError因为副本的 jvm 参数配置有:-XX:+ExitOnOutOfMemoryError,该实例的 JVM(PID 为 1)立即退出。因为 pid 1 进程退出,此时 pod 立刻出于 Terminating 状态,并且变为:Terminated同时,customer 的 SVC 负载均衡会将该副本从SVC 负载均衡中移除,用户请求不会被分发到该节点。K8S检测到副本数和 Deployment replicas 不一致,启动1个新的副本。待新的部分 Readiness Probe 探测通过,customer 的 SVC 负载均衡将这个新的副本加入到负载均衡中,接收用户请求。在此过程中,用户基本上是对后台故障“无感知”的。
当然,要做到这些,其实JVM参数以及启动脚本中,还有很多细节和门道。如:启动脚本应该是:exec java ....$*
上面,我们解释了《为什么 Java 容器推荐使用 ExitOnOutOfMemoryError 而非 HeapDumpOnOutOfMemoryError》,但是细心的小伙伴也会发现,新的配置也会带来新的问题,比如:
JVM 从 fullgc -> OutOfMemoryError 这段时间内,用户的体验还是会下降的,怎么会是“故障无感知”呢?用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的主要原因与容器技术的特点和运维模式的转变有关。以下是几个关键点:
快速失败与快速恢复:容器技术的一个核心优势是其快速的启动和停止能力。在容器环境中,当应用出现内存泄漏或溢出时,快速失败并迅速启动新实例可以更快地恢复服务,而不是花费时间生成堆转储文件进行问题分析。容器的短暂性:与传统的虚拟机部署相比,容器被设计为短暂和临时的存在。它们通过快速的扩缩容来适应负载变化,而不是长期运行单个实例。用户无感知:在容器环境中,通过配置如Kubernetes的Readiness Probe和Liveness Probe,可以确保在实例出现问题时,不会将用户请求分发到该实例,并且在实例恢复后迅速重新加入服务,实现用户对故障的无感知。避免在OOM期间的资源消耗:生成堆转储(HeapDump)是一个资源密集型操作,可能会导致服务进一步恶化或延长故障恢复时间。使用ExitOnOutOfMemoryError可以立即终止进程,避免在OOM情况下继续消耗资源。故障分析的其他手段:即使没有堆转储文件,也可以通过其他监控和日志分析工具来定位和分析问题。例如,使用Prometheus JVM Exporter、Prometheus和AlertManager进行监控,可以在GC时间超过阈值时发出告警,从而提前介入处理问题。新的技术带来新的变革,我们需要以发展的眼光看待“最佳实践,最佳配置”。
过去(2016 年以前),针对虚机部署的 Java 的最优参数,在今天来看,并不一定仍是最优解。
-XX:+ExitOnOutOfMemoryError在容器环境中更符合快速恢复和高可用性的需求,而-XX:+HeapDumpOnOutOfMemoryError虽然有助于问题分析,但在容器的快速运维模式下可能不是最佳选择。
来源:散文随风想一点号