為什麼 java 容器推薦使用 ExitOnOutOfMemoryError 而非 HeapDumpOnOutOfMemoryError ?

東風微鳴發表於2023-01-08

前言

好久沒寫文章了, 今天之所以突然心血來潮, 是因為昨天出現了這樣一個情況:

我們公司的某個手機APP後端的使用者(customer)微服務出現記憶體洩露, 導致OutOfMemoryError, 但是因為經過我們精心最佳化的openjdk容器引數, 這次故障對使用者完全無感知. ???

那麼我們是如何做到的呢?

HeapDumpOnOutOfMemoryError VS ExitOnOutOfMemoryError

我們都知道, 在傳統的虛擬機器上部署的Java例項. 為了更好地分析問題, 一般都是要加上: -XX:+HeapDumpOnOutOfMemoryError這個引數的. 加這個引數後, 如果遇到記憶體溢位, 就會自動生成HeapDump, 後面我們可以拿到這個HeapDump來更精確地分析問題.

但是, "大人, 時代變了!"

容器技術的發展, 給傳統運維模式帶來了巨大的挑戰, 這個挑戰是革命性的:

  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個引數一起用不是更香麼?

這些其實可以透過其他手段來解決:

  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.
readinessProbe:
  httpGet:
    path: /actuator/info
    port: 8088
    scheme: HTTP
  initialDelaySeconds: 60
  timeoutSeconds: 3
  periodSeconds: 10
  successThreshold: 1
  failureThreshold: 3

總結

新的技術帶來新的變革, 我們需要以發展的眼光看待"最佳實踐, 最佳配置".

2016年, 針對虛機部署的Java的最優引數, 在今天來看, 並不一定仍是最優解.

三人行, 必有我師; 知識共享, 天下為公. 本文由東風微鳴技術部落格 EWhisper.cn 編寫.

相關文章