詳解 Flink 容器化環境下的 OOM Killed

seven123發表於2021-01-27

在生產環境中,Flink 通常會部署在 YARN 或 k8s 等資源管理系統之上,程式會以容器化(YARN 容器或 docker 等容器)的方式執行,其資源會受到資源管理系統的嚴格限制。另一方面,Flink 執行在 JVM 之上,而 JVM 與容器化環境並不是特別適配,尤其 JVM 複雜且可控性較弱的記憶體模型,容易導致程式因使用資源超標而被 kill 掉,造成 Flink 應用的不穩定甚至不可用。

針對這個問題,Flink 在 1.10 版本對記憶體管理模組進行了重構,設計了全新的記憶體引數。在大多數場景下 Flink 的記憶體模型和預設已經足夠好用,可以幫使用者遮蔽程式背後的複雜記憶體結構,然而一旦出現記憶體問題,問題的排查和修復都需要比較多的領域知識,通常令普通使用者望而卻步。

為此,本文將解析 JVM 和 Flink 的記憶體模型,並總結在工作中遇到和在社群交流中瞭解到的造成 Flink 記憶體使用超出容器限制的常見原因。由於 Flink 記憶體使用與使用者程式碼、部署環境、各種依賴版本等因素都有緊密關係,本文主要討論 on YARN 部署、Oracle JDK/OpenJDK 8、Flink 1.10+ 的情況。此外,特別感謝 @宋辛童(Flink 1.10+ 新記憶體架構的主要作者)和 @唐雲(RocksDB StateBackend 專家)在社群的答疑,令筆者受益匪淺。

JVM 記憶體分割槽

對於大多數 Java 使用者而言,日常開發中與 JVM Heap 打交道的頻率遠大於其他 JVM 記憶體分割槽,因此常把其他記憶體分割槽統稱為 Off-Heap 記憶體。而對於 Flink 來說,記憶體超標問題通常來自 Off-Heap 記憶體,因此對 JVM 記憶體模型有更深入的理解是十分必要的。

除了上述 Spec 規定的標準分割槽,在具體實現上 JVM 常常還會加入一些額外的分割槽供進階功能模組使用。以 HotSopt JVM 為例,根據 Oracle NMT[5] 的標準,我們可以將 JVM 記憶體細分為如下區域:

Heap: 各執行緒共享的記憶體區域,主要存放 new 運算子建立的物件,記憶體的釋放由 GC 管理,可被使用者程式碼或 JVM 本身使用。

Class: 類的後設資料,對應 Spec 中的 Method Area (不含 Constant Pool),Java 8 中的 Metaspace。

Thread: 執行緒級別的記憶體區,對應 Spec 中的 PC Register、Stack 和 Natvive Stack 三者的總和。

Compiler: JIT (Just-In-Time) 編譯器使用的記憶體。

Code Cache: 用於儲存 JIT 編譯器生成的程式碼的快取。

GC: 垃圾回收器使用的記憶體。

Symbol: 儲存 Symbol (比如欄位名、方法簽名、Interned String) 的記憶體,對應 Spec 中的 Constant Pool。

Arena Chunk: JVM 申請作業系統記憶體的臨時快取區。

NMT: NMT 自己使用的記憶體。

Internal: 其他不符合上述分類的記憶體,包括使用者程式碼申請的 Native/Direct 記憶體。

Unknown: 無法分類的記憶體。

理想情況下,我們可以嚴格控制各分割槽記憶體的上限,來保證程式總體記憶體在容器限額之內。但是過於嚴格的管理會帶來會有額外使用成本且缺乏靈活度,所以在實際中為了 JVM 只對其中幾個暴露給使用者使用的分割槽提供了硬性的上限,而其他分割槽則可以作為整體被視為 JVM 本身的記憶體消耗。

具體可以用於限制分割槽記憶體的 JVM 引數如下表所示(值得注意的是,業界對於 JVM Native 記憶體並沒有準確的定義,本文的 Native 記憶體指的是 Off-Heap 記憶體中非 Direct 的部分,與 Native Non-Direct 可以互換)。

從表中可以看到,使用 Heap、Metaspace 和 Direct 記憶體都是比較安全的,但非 Direct 的 Native 記憶體情況則比較複雜,可能是 JVM 本身的一些內部使用(比如下文會提到的 MemberNameTable),也可能是使用者程式碼引入的 JNI 依賴,還有可能是使用者程式碼 自身透過 sun.misc.Unsafe 申請的 Native 記憶體。理論上講,使用者程式碼或第三方 lib 申請的 Native 記憶體需要使用者來規劃記憶體用量,而 Internal 的其餘部分可以併入 JVM 本身的記憶體消耗。而實際上 Flink 的記憶體模型也遵循了類似的原則。

Flink TaskManager 記憶體模型

首先回顧下 Flink 1.10+ 的 TaskManager 記憶體模型。

顯然,Flink 框架本身不僅會包含 JVM 管理的 Heap 記憶體,也會申請自己管理 Off-Heap 的 Native 和 Direct 記憶體。在筆者看來,Flink 對於 Off-Heap 記憶體的管理策略可以分為三種:

硬限制(Hard Limit): 硬限制的記憶體分割槽是 Self-Contained 的,Flink 會保證其用量不會超過設定的閾值(若記憶體不夠則丟擲類似 OOM 的異常)

軟限制(Soft Limit): 軟限制意味著記憶體使用長期會在閾值以下,但可能短暫地超過配置的閾值。

預留(Reserved): 預留意味著 Flink 不會限制分割槽記憶體的使用,只是在規劃記憶體時預留一部分空間,但不能保證實際使用會不會超額。

結合 JVM 的記憶體管理來看,一個 Flink 記憶體分割槽的記憶體溢位會導致何種後果,判斷邏輯如下:

1、若是 Flink 有硬限制的分割槽,Flink 會報該分割槽記憶體不足。否則進入下一步。
2、若該分割槽屬於 JVM 管理的分割槽,在其實際值增長導致 JVM 分割槽也記憶體耗盡時,JVM 會報其所屬的 JVM 分割槽的 OOM (比如 java.lang.OutOfMemoryError: Jave heap space)。否則進入下一步。
3、該分割槽記憶體持續溢位,最終導致程式總體記憶體超出容器記憶體限制。在開啟嚴格資源控制的環境下,資源管理器(YARN/k8s 等)會 kill 掉該程式。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69989270/viewspace-2753923/,如需轉載,請註明出處,否則將追究法律責任。

相關文章