为什么 java 容器推荐使用 ExitOnOutOfMemoryError 而非 HeapDumpOnOutOfMemoryError ?
本文最后更新于:2024年7月24日 晚上
前言
好久没写文章了, 今天之所以突然心血来潮, 是因为昨天出现了这样一个情况:
我们公司的某个手机 APP 后端的用户 (customer) 微服务出现内存泄露, 导致 OutOfMemoryError, 但是因为经过我们精心优化的 openjdk 容器参数, 这次故障对用户 完全无感知. 💪💪💪
那么我们是如何做到的呢?
HeapDumpOnOutOfMemoryError VS ExitOnOutOfMemoryError
我们都知道, 在传统的虚拟机上部署的 Java 实例. 为了更好地分析问题, 一般都是要加上: -XX:+HeapDumpOnOutOfMemoryError
这个参数的. 加这个参数后, 如果遇到内存溢出, 就会自动生成 HeapDump, 后面我们可以拿到这个 HeapDump 来更精确地分析问题.
但是, “大人, 时代变了!”
容器技术的发展, 给传统运维模式带来了巨大的挑战, 这个挑战是革命性的:
- 传统的应用都是 " 永久存在的 " 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
探测失败, K8S 就会自动将这个节点从 SVC 中摘除. 那么合理的Readiness Probe
在这里指的就是应用不可用时,Readiness Probe
探测必然是失败的. 所以一般不能是探测某个端口是否在监听, 而是应该是探测对应的 api 是否正常. 如下方. - 答: 通过 Prometheus JVM Exporter + Prometheus + AlertManger, 配置合理的 AlertRule. 如: " 过去 X 时间, GC total time>5s" 告警, 告警后人工介入提前处理.
- 答: 配置合理的
- 用 "ExitOnOutOfMemoryError" 代替 "HeapDumpOnOutOfMemoryError", 那我怎么定位该问题的根因并解决? 2 个参数一起用不是更香么?
- 答: 目的是为了 " 快速退出, 快速终结 ". 毕竟做 HeapDump 也是需要时间的, 这段时间内可能就会造成体验的下降. 所以, 只有 "ExitOnOutOfMemoryError", 退出地越快越好.
- 答: 至于分析问题, 可以通过其他手段分析, 如嵌入 "Tracing agent" 做 Tracing 的监控, 通过分析故障时的 traces 定位根因.
- Prometheus Alertrule gctime 告警后, 人工通过
jcmd
等命令手动做 heapdump.
1 |
|
总结
新的技术带来新的变革, 我们需要以发展的眼光看待 " 最佳实践, 最佳配置 ".
2016 年, 针对虚机部署的 Java 的最优参数, 在今天来看, 并不一定仍是最优解.