這篇文章主要介紹了Flink容器化環(huán)境下OOM Killed是什么,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
成都創(chuàng)新互聯(lián)專(zhuān)業(yè)為企業(yè)提供保靖網(wǎng)站建設(shè)、保靖做網(wǎng)站、保靖網(wǎng)站設(shè)計(jì)、保靖網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、保靖企業(yè)網(wǎng)站模板建站服務(wù),十載保靖做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。
對(duì)于大多數(shù) Java 用戶(hù)而言,日常開(kāi)發(fā)中與 JVM Heap 打交道的頻率遠(yuǎn)大于其他 JVM 內(nèi)存分區(qū),因此常把其他內(nèi)存分區(qū)統(tǒng)稱(chēng)為 Off-Heap 內(nèi)存。而對(duì)于 Flink 來(lái)說(shuō),內(nèi)存超標(biāo)問(wèn)題通常來(lái)自 Off-Heap 內(nèi)存,因此對(duì) JVM 內(nèi)存模型有更深入的理解是十分必要的。
根據(jù) JVM 8 Spec[1],JVM 管理的內(nèi)存分區(qū)如下圖:
JVM 8 內(nèi)存模型
除了上述 Spec 規(guī)定的標(biāo)準(zhǔn)分區(qū),在具體實(shí)現(xiàn)上 JVM 常常還會(huì)加入一些額外的分區(qū)供進(jìn)階功能模塊使用。以 HotSopt JVM 為例,根據(jù) Oracle NMT[5] 的標(biāo)準(zhǔn),我們可以將 JVM 內(nèi)存細(xì)分為如下區(qū)域:
Heap: 各線(xiàn)程共享的內(nèi)存區(qū)域,主要存放 new 操作符創(chuàng)建的對(duì)象,內(nèi)存的釋放由 GC 管理,可被用戶(hù)代碼或 JVM 本身使用。
Class: 類(lèi)的元數(shù)據(jù),對(duì)應(yīng) Spec 中的 Method Area (不含 Constant Pool),Java 8 中的 Metaspace。
Thread: 線(xiàn)程級(jí)別的內(nèi)存區(qū),對(duì)應(yīng) Spec 中的 PC Register、Stack 和 Natvive Stack 三者的總和。
Compiler: JIT (Just-In-Time) 編譯器使用的內(nèi)存。
Code Cache: 用于存儲(chǔ) JIT 編譯器生成的代碼的緩存。
GC: 垃圾回收器使用的內(nèi)存。
Symbol: 存儲(chǔ) Symbol (比如字段名、方法簽名、Interned String) 的內(nèi)存,對(duì)應(yīng) Spec 中的 Constant Pool。
Arena Chunk: JVM 申請(qǐng)操作系統(tǒng)內(nèi)存的臨時(shí)緩存區(qū)。
NMT: NMT 自己使用的內(nèi)存。
Internal: 其他不符合上述分類(lèi)的內(nèi)存,包括用戶(hù)代碼申請(qǐng)的 Native/Direct 內(nèi)存。
Unknown: 無(wú)法分類(lèi)的內(nèi)存。
理想情況下,我們可以嚴(yán)格控制各分區(qū)內(nèi)存的上限,來(lái)保證進(jìn)程總體內(nèi)存在容器限額之內(nèi)。但是過(guò)于嚴(yán)格的管理會(huì)帶來(lái)會(huì)有額外使用成本且缺乏靈活度,所以在實(shí)際中為了 JVM 只對(duì)其中幾個(gè)暴露給用戶(hù)使用的分區(qū)提供了硬性的上限,而其他分區(qū)則可以作為整體被視為 JVM 本身的內(nèi)存消耗。
具體可以用于限制分區(qū)內(nèi)存的 JVM 參數(shù)如下表所示(值得注意的是,業(yè)界對(duì)于 JVM Native 內(nèi)存并沒(méi)有準(zhǔn)確的定義,本文的 Native 內(nèi)存指的是 Off-Heap 內(nèi)存中非 Direct 的部分,與 Native Non-Direct 可以互換)。
從表中可以看到,使用 Heap、Metaspace 和 Direct 內(nèi)存都是比較安全的,但非 Direct 的 Native 內(nèi)存情況則比較復(fù)雜,可能是 JVM 本身的一些內(nèi)部使用(比如下文會(huì)提到的 MemberNameTable),也可能是用戶(hù)代碼引入的 JNI 依賴(lài),還有可能是用戶(hù)代碼自身通過(guò) sun.misc.Unsafe 申請(qǐng)的 Native 內(nèi)存。理論上講,用戶(hù)代碼或第三方 lib 申請(qǐng)的 Native 內(nèi)存需要用戶(hù)來(lái)規(guī)劃內(nèi)存用量,而 Internal 的其余部分可以并入 JVM 本身的內(nèi)存消耗。而實(shí)際上 Flink 的內(nèi)存模型也遵循了類(lèi)似的原則。
首先回顧下 Flink 1.10+ 的 TaskManager 內(nèi)存模型。
Flink TaskManager 內(nèi)存模型
顯然,F(xiàn)link 框架本身不僅會(huì)包含 JVM 管理的 Heap 內(nèi)存,也會(huì)申請(qǐng)自己管理 Off-Heap 的 Native 和 Direct 內(nèi)存。在筆者看來(lái),F(xiàn)link 對(duì)于 Off-Heap 內(nèi)存的管理策略可以分為三種:
硬限制(Hard Limit): 硬限制的內(nèi)存分區(qū)是 Self-Contained 的,F(xiàn)link 會(huì)保證其用量不會(huì)超過(guò)設(shè)置的閾值(若內(nèi)存不夠則拋出類(lèi)似 OOM 的異常)
軟限制(Soft Limit): 軟限制意味著內(nèi)存使用長(zhǎng)期會(huì)在閾值以下,但可能短暫地超過(guò)配置的閾值。
預(yù)留(Reserved): 預(yù)留意味著 Flink 不會(huì)限制分區(qū)內(nèi)存的使用,只是在規(guī)劃內(nèi)存時(shí)預(yù)留一部分空間,但不能保證實(shí)際使用會(huì)不會(huì)超額。
結(jié)合 JVM 的內(nèi)存管理來(lái)看,一個(gè) Flink 內(nèi)存分區(qū)的內(nèi)存溢出會(huì)導(dǎo)致何種后果,判斷邏輯如下:
1、若是 Flink 有硬限制的分區(qū),F(xiàn)link 會(huì)報(bào)該分區(qū)內(nèi)存不足。否則進(jìn)入下一步。
2、若該分區(qū)屬于 JVM 管理的分區(qū),在其實(shí)際值增長(zhǎng)導(dǎo)致 JVM 分區(qū)也內(nèi)存耗盡時(shí),JVM 會(huì)報(bào)其所屬的 JVM 分區(qū)的 OOM (比如 java.lang.OutOfMemoryError: Jave heap space)。否則進(jìn)入下一步。
3、該分區(qū)內(nèi)存持續(xù)溢出,最終導(dǎo)致進(jìn)程總體內(nèi)存超出容器內(nèi)存限制。在開(kāi)啟嚴(yán)格資源控制的環(huán)境下,資源管理器(YARN/k8s 等)會(huì) kill 掉該進(jìn)程。
為直觀地展示 Flink 各內(nèi)存分區(qū)與 JVM 內(nèi)存分區(qū)間的關(guān)系,筆者整理了如下的內(nèi)存分區(qū)映射表:
Flink 分區(qū)及 JVM 分區(qū)內(nèi)存限制關(guān)系
根據(jù)之前的邏輯,在所有的 Flink 內(nèi)存分區(qū)中,只有不是 Self-Contained 且所屬 JVM 分區(qū)也沒(méi)有內(nèi)存硬限制參數(shù)的 JVM Overhead 是有可能導(dǎo)致進(jìn)程被 OOM kill 掉的。作為一個(gè)預(yù)留給各種不同用途的內(nèi)存的大雜燴,JVM Overhead 的確容易出問(wèn)題,但同時(shí)它也可以作為一個(gè)兜底的隔離緩沖區(qū),來(lái)緩解來(lái)自其他區(qū)域的內(nèi)存問(wèn)題。
舉個(gè)例子,F(xiàn)link 內(nèi)存模型在計(jì)算 Native Non-Direct 內(nèi)存時(shí)有一個(gè) trick:
Although, native non-direct memory usage can be accounted for as a part of the framework off-heap memory or task off-heap memory, it will result in a higher JVM’s direct memory limit in this case.
雖然 Task/Framework 的 Off-Heap 分區(qū)中可能含有 Native Non-Direct 內(nèi)存,而這部分內(nèi)存嚴(yán)格來(lái)說(shuō)屬于 JVM Overhead,不會(huì)被 JVM -XX:MaxDirectMemorySize 參數(shù)所限制,但 Flink 還是將它算入 MaxDirectMemorySize 中。這部分預(yù)留的 Direct 內(nèi)存配額不會(huì)被實(shí)際使用,所以可以留給沒(méi)有上限 JVM Overhead 占用,達(dá)到為 Native Non-Direct 內(nèi)存預(yù)留空間的效果。
與上文分析一致,實(shí)踐中導(dǎo)致 OOM Killed 的常見(jiàn)原因基本源于 Native 內(nèi)存的泄漏或者過(guò)度使用。因?yàn)樘摂M內(nèi)存的 OOM Killed 通過(guò)資源管理器的配置很容易避免且通常不會(huì)有太大問(wèn)題,所以下文只討論物理內(nèi)存的 OOM Killed。
RocksDB Native 內(nèi)存的不確定性
眾所周知,RocksDB 通過(guò) JNI 直接申請(qǐng) Native 內(nèi)存,并不受 Flink 的管控,所以實(shí)際上 Flink 通過(guò)設(shè)置 RocksDB 的內(nèi)存參數(shù)間接影響其內(nèi)存使用。然而,目前 Flink 是通過(guò)估算得出這些參數(shù),并不是非常精確的值,其中有以下的幾個(gè)原因。
首先是部分內(nèi)存難以準(zhǔn)確計(jì)算的問(wèn)題。RocksDB 的內(nèi)存占用有 4 個(gè)部分[6]:
Block Cache: OS PageCache 之上的一層緩存,緩存未壓縮的數(shù)據(jù) Block。
Indexes and filter blocks: 索引及布隆過(guò)濾器,用于優(yōu)化讀性能。
Memtable: 類(lèi)似寫(xiě)緩存。
Blocks pinned by Iterator: 觸發(fā) RocksDB 遍歷操作(比如遍歷 RocksDBMapState 的所有 key)時(shí),Iterator 在其生命周期內(nèi)會(huì)阻止其引用到的 Block 和 Memtable 被釋放,導(dǎo)致額外的內(nèi)存占用[10]。
前三個(gè)區(qū)域的內(nèi)存都是可配置的,但 Iterator 鎖定的資源則要取決于應(yīng)用業(yè)務(wù)使用模式,且沒(méi)有提供一個(gè)硬限制,因此 Flink 在計(jì)算 RocksDB StateBackend 內(nèi)存時(shí)沒(méi)有將這部分納入考慮。
其次是 RocksDB Block Cache 的一個(gè) bug[8][9],它會(huì)導(dǎo)致 Cache 大小無(wú)法嚴(yán)格控制,有可能短時(shí)間內(nèi)超出設(shè)置的內(nèi)存容量,相當(dāng)于軟限制。
對(duì)于這個(gè)問(wèn)題,通常我們只要調(diào)大 JVM Overhead 的閾值,讓 Flink 預(yù)留更多內(nèi)存即可,因?yàn)?RocksDB 的內(nèi)存超額使用只是暫時(shí)的。
glibc Thread Arena 問(wèn)題
另外一個(gè)常見(jiàn)的問(wèn)題就是 glibc 著名的 64 MB 問(wèn)題,它可能會(huì)導(dǎo)致 JVM 進(jìn)程的內(nèi)存使用大幅增長(zhǎng),最終被 YARN kill 掉。
具體來(lái)說(shuō),JVM 通過(guò) glibc 申請(qǐng)內(nèi)存,而為了提高內(nèi)存分配效率和減少內(nèi)存碎片,glibc 會(huì)維護(hù)稱(chēng)為 Arena 的內(nèi)存池,包括一個(gè)共享的 Main Arena 和線(xiàn)程級(jí)別的 Thread Arena。當(dāng)一個(gè)線(xiàn)程需要申請(qǐng)內(nèi)存但 Main Arena 已經(jīng)被其他線(xiàn)程加鎖時(shí),glibc 會(huì)分配一個(gè)大約 64 MB (64 位機(jī)器)的 Thread Arena 供線(xiàn)程使用。這些 Thread Arena 對(duì)于 JVM 是透明的,但會(huì)被算進(jìn)進(jìn)程的總體虛擬內(nèi)存(VIRT)和物理內(nèi)存(RSS)里。
默認(rèn)情況下,Arena 的最大數(shù)目是 cpu 核數(shù) * 8,對(duì)于一臺(tái)普通的 32 核服務(wù)器來(lái)說(shuō)最多占用 16 GB,不可謂不可觀。為了控制總體消耗內(nèi)存的總量,glibc 提供了環(huán)境變量 MALLOC_ARENA_MAX 來(lái)限制 Arena 的總量,比如 Hadoop 就默認(rèn)將這個(gè)值設(shè)置為 4。然而,這個(gè)參數(shù)只是一個(gè)軟限制,所有 Arena 都被加鎖時(shí),glibc 仍會(huì)新建 Thread Arena 來(lái)分配內(nèi)存[11],造成意外的內(nèi)存使用。
通常來(lái)說(shuō),這個(gè)問(wèn)題會(huì)出現(xiàn)在需要頻繁創(chuàng)建線(xiàn)程的應(yīng)用里,比如 HDFS Client 會(huì)為每個(gè)正在寫(xiě)入的文件新建一個(gè) DataStreamer 線(xiàn)程,所以比較容易遇到 Thread Arena 的問(wèn)題。如果懷疑你的 Flink 應(yīng)用遇到這個(gè)問(wèn)題,比較簡(jiǎn)單的驗(yàn)證方法就是看進(jìn)程的 pmap 是否存在很多大小為 64MB 倍數(shù)的連續(xù) anon 段,比如下圖中藍(lán)色幾個(gè)的 65536 KB 的段就很有可能是 Arena。
pmap 64 MB arena
這個(gè)問(wèn)題的修復(fù)辦法比較簡(jiǎn)單,將 MALLOC_ARENA_MAX 設(shè)置為 1 即可,也就是禁用 Thread Arena 只使用 Main Arena。當(dāng)然,這樣的代價(jià)就是線(xiàn)程分配內(nèi)存效率會(huì)降低。不過(guò)值得一提的是,使用 Flink 的進(jìn)程環(huán)境變量參數(shù)(比如 containerized.taskmanager.env.MALLOC_ARENA_MAX=1)來(lái)覆蓋默認(rèn)的 MALLOC_ARENA_MAX 參數(shù)可能是不可行的,原因是在非白名單變量(yarn.nodemanager.env-whitelist)沖突的情況下, NodeManager 會(huì)以合并 URL 的方式來(lái)合并原有的值和追加的值,最終造成 MALLOC_ARENA_MAX="4:1" 這樣的結(jié)果。
最后,還有一個(gè)更徹底的可選解決方案,就是將 glibc 替換為 Google 家的 tcmalloc 或 Facebook 家的 jemalloc [12]。除了不會(huì)有 Thread Arena 問(wèn)題,內(nèi)存分配性能更好,碎片更少。在實(shí)際上,F(xiàn)link 1.12 的官方鏡像也將默認(rèn)的內(nèi)存分配器從 glibc 改為 jemelloc [17]。
JDK8 Native 內(nèi)存泄漏
Oracle Jdk8u152 之前的版本存在一個(gè) Native 內(nèi)存泄漏的 bug[13],會(huì)造成 JVM 的 Internal 內(nèi)存分區(qū)一直增長(zhǎng)。
具體而言,JVM 會(huì)緩存字符串符號(hào)(Symbol)到方法(Method)、成員變量(Field)的映射對(duì)來(lái)加快查找,每對(duì)映射稱(chēng)為 MemberName,整個(gè)映射關(guān)系稱(chēng)為 MemeberNameTable,由 java.lang.invoke.MethodHandles 這個(gè)類(lèi)負(fù)責(zé)。在 Jdk8u152 之前,MemberNameTable 是使用 Native 內(nèi)存的,因此一些過(guò)時(shí)的 MemberName 不會(huì)被 GC 自動(dòng)清理,造成內(nèi)存泄漏。
要確認(rèn)這個(gè)問(wèn)題,需要通過(guò) NMT 來(lái)查看 JVM 內(nèi)存情況,比如筆者就遇到過(guò)線(xiàn)上一個(gè) TaskManager 的超過(guò) 400 MB 的 MemeberNameTable。
JDK8 MemberNameTable Native 內(nèi)存泄漏
在 JDK-8013267[14] 以后,MemeberNameTable 從 Native 內(nèi)存被移到 Java Heap 當(dāng)中,修復(fù)了這個(gè)問(wèn)題。然而,JVM 的 Native 內(nèi)存泄漏問(wèn)題不止一個(gè),比如 C2 編譯器的內(nèi)存泄漏問(wèn)題[15],所以對(duì)于跟筆者一樣沒(méi)有專(zhuān)門(mén) JVM 團(tuán)隊(duì)的用戶(hù)來(lái)說(shuō),升級(jí)到最新版本的 JDK 是修復(fù)問(wèn)題的最好辦法。
YARN mmap 內(nèi)存算法
眾所周知,YARN 會(huì)根據(jù) /proc/${pid} 下的進(jìn)程信息來(lái)計(jì)算整個(gè) container 進(jìn)程樹(shù)的總體內(nèi)存,但這里面有一個(gè)比較特殊的點(diǎn)是 mmap 的共享內(nèi)存。mmap 內(nèi)存會(huì)全部被算進(jìn)進(jìn)程的 VIRT,這點(diǎn)應(yīng)該沒(méi)有疑問(wèn),但關(guān)于 RSS 的計(jì)算則有不同標(biāo)準(zhǔn)。 依據(jù) YARN 和 Linux smaps 的計(jì)算規(guī)則,內(nèi)存頁(yè)(Pages)按兩種標(biāo)準(zhǔn)劃分:
Private Pages: 只有當(dāng)前進(jìn)程映射(mapped)的 Pages
Shared Pages: 與其他進(jìn)程共享的 Pages
Clean Pages: 自從被映射后沒(méi)有被修改過(guò)的 Pages
Dirty Pages: 自從被映射后已經(jīng)被修改過(guò)的 Pages 在默認(rèn)的實(shí)現(xiàn)里,YARN 根據(jù) /proc/${pid}/status 來(lái)計(jì)算總內(nèi)存,所有的 Shared Pages 都會(huì)被算入進(jìn)程的 RSS,即便這些 Pages 同時(shí)被多個(gè)進(jìn)程映射[16],這會(huì)導(dǎo)致和實(shí)際操作系統(tǒng)物理內(nèi)存的偏差,有可能導(dǎo)致 Flink 進(jìn)程被誤殺(當(dāng)然,前提是用戶(hù)代碼使用 mmap 且沒(méi)有預(yù)留足夠空間)。
為此,YARN 提供 yarn.nodemanager.container-monitor.procfs-tree.smaps-based-rss.enabled 配置選項(xiàng),將其設(shè)置為 true 后,YARN 將根據(jù)更準(zhǔn)確的 /proc/${pid}/smap 來(lái)計(jì)算內(nèi)存占用,其中很關(guān)鍵的一個(gè)概念是 PSS。簡(jiǎn)單來(lái)說(shuō),PSS 的不同點(diǎn)在于計(jì)算內(nèi)存時(shí)會(huì)將 Shared Pages 均分給所有使用這個(gè) Pages 的進(jìn)程,比如一個(gè)進(jìn)程持有 1000 個(gè) Private Pages 和 1000 個(gè)會(huì)分享給另外一個(gè)進(jìn)程的 Shared Pages,那么該進(jìn)程的總 Page 數(shù)就是 1500。 回到 YARN 的內(nèi)存計(jì)算上,進(jìn)程 RSS 等于其映射的所有 Pages RSS 的總和。在默認(rèn)情況下,YARN 計(jì)算一個(gè) Page RSS 公式為: ``` Page RSS = Private_Clean + Private_Dirty + Shared_Clean + Shared_Dirty ``` 因?yàn)橐粋€(gè) Page 要么是 Private,要么是 Shared,且要么是 Clean 要么是 Dirty,所以其實(shí)上述公示右邊有至少三項(xiàng)為 0 。而在開(kāi)啟 smaps 選項(xiàng)后,公式變?yōu)? ``` Page RSS = Min(Shared_Dirty, PSS) + Private_Clean + Private_Dirty ``` 簡(jiǎn)單來(lái)說(shuō),新公式的結(jié)果就是去除了 Shared_Clean 部分被重復(fù)計(jì)算的影響。 雖然開(kāi)啟基于 smaps 計(jì)算的選項(xiàng)會(huì)讓計(jì)算更加準(zhǔn)確,但會(huì)引入遍歷 Pages 計(jì)算內(nèi)存總和的開(kāi)銷(xiāo),不如 直接取 /proc/${pid}/status 的統(tǒng)計(jì)數(shù)據(jù)快,因此如果遇到 mmap 的問(wèn)題,還是推薦通過(guò)提高 Flink 的 JVM Overhead 分區(qū)容量來(lái)解決。
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“Flink容器化環(huán)境下OOM Killed是什么”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持創(chuàng)新互聯(lián),關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!