为什么 java 容器推荐使用 ExitOnOutOfMemoryError 而非 HeapDumpOnOutOfMemoryError ?

本文最后更新于:2024年7月25日 下午

前言

好久没写文章了,今天之所以突然心血来潮,是因为昨天出现了这样一个情况:

我们公司的某个手机 APP 后端的用户 (customer) 微服务出现内存泄露,导致 OutOfMemoryError, 但是因为经过我们精心优化的 openjdk 容器参数,这次故障对用户完全无感知. 💪💪💪

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

HeapDumpOnOutOfMemoryError VS ExitOnOutOfMemoryError

我们都知道,在传统的虚拟机上部署的 Java 实例。为了更好地分析问题,一般都是要加上: -XX:+HeapDumpOnOutOfMemoryError 这个参数的。加这个参数后,如果遇到内存溢出,就会自动生成 HeapDump, 后面我们可以拿到这个 HeapDump 来更精确地分析问题.

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

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

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

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

  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 个参数一起用不是更香么?

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

  1. JVM 从 fullgc -> OutOfMemoryError 这段时间内,用户的体验还是会下降的,怎么会是 "故障无感知" 呢?
    1. 答:配置合理的 Readiness Probe, 只要 Readiness Probe 探测失败,K8S 就会自动将这个节点从 SVC 中摘除。那么合理的 Readiness Probe 在这里指的就是应用不可用时,Readiness Probe 探测必然是失败的。所以一般不能是探测某个端口是否在监听,而是应该是探测对应的 api 是否正常。如下方.
    2. 答:通过 Prometheus JVM Exporter + Prometheus + AlertManger, 配置合理的 AlertRule. 如: "过去 X 时间,GC total time>5s" 告警,告警后人工介入提前处理.
  2. 用 "ExitOnOutOfMemoryError" 代替 "HeapDumpOnOutOfMemoryError", 那我怎么定位该问题的根因并解决?2 个参数一起用不是更香么?
    1. 答:目的是为了 "快速退出,快速终结". 毕竟做 HeapDump 也是需要时间的,这段时间内可能就会造成体验的下降。所以,只有 "ExitOnOutOfMemoryError", 退出地越快越好.
    2. 答:至于分析问题,可以通过其他手段分析,如嵌入 "Tracing agent" 做 Tracing 的监控,通过分析故障时的 traces 定位根因.
    3. Prometheus Alertrule gctime 告警后,人工通过 jcmd 等命令手动做 heapdump.
1
2
3
4
5
6
7
8
9
10
readinessProbe:
httpGet:
path: /actuator/info
port: 8088
scheme: HTTP
initialDelaySeconds: 60
timeoutSeconds: 3
periodSeconds: 10
successThreshold: 1
failureThreshold: 3

总结

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

2016 年,针对虚机部署的 Java 的最优参数,在今天来看,并不一定仍是最优解.


为什么 java 容器推荐使用 ExitOnOutOfMemoryError 而非 HeapDumpOnOutOfMemoryError ?
https://ewhisper.cn/posts/5006/
作者
东风微鸣
发布于
2020年8月27日
许可协议