前言
好久沒寫文章了, 今天之所以突然心血來潮, 是因為昨天出現了這樣一個情況:
我們公司的某個手機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.
readinessProbe:
httpGet:
path: /actuator/info
port: 8088
scheme: HTTP
initialDelaySeconds: 60
timeoutSeconds: 3
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
總結
新的技術帶來新的變革, 我們需要以發展的眼光看待"最佳實踐, 最佳配置".
2016年, 針對虛機部署的Java的最優引數, 在今天來看, 並不一定仍是最優解.
三人行, 必有我師; 知識共享, 天下為公. 本文由東風微鳴技術部落格 EWhisper.cn 編寫.