記一次刷數(shù)據(jù)遇到的坑-OOM問題

問題背景

筆者在處理一項刷數(shù)據(jù)的工作,過程是將數(shù)據(jù)源A的數(shù)據(jù)經(jīng)過一些調(diào)整和計算后存入到數(shù)據(jù)源B,整個過程大致如下圖所示。


過程概述

整個刷數(shù)據(jù)過程分為以下步驟:

  1. 通過執(zhí)行SQL的join語句查詢指定的數(shù)據(jù)(select A1.col1,A1.col2,A2.col3 from A1 join A2 on A1.col1=A2.col1 where userid=10086 > temp.out
    此時temp.out中的文件格式形如col1 \t col2 \t col3
  2. 通過在client端執(zhí)行RPC調(diào)用(如dubbo)請求server端已有的添加接口將數(shù)據(jù)寫入到表B
    這么做是因為表B數(shù)據(jù)的寫入伴隨著相關(guān)的業(yè)務(wù)邏輯(例如操作記錄、消息通知等),因此需要復(fù)用server端已有的接口,步驟二client端的偽代碼如下:
def transfer():
    uid2Data = {}
    for file in directory.listFiles(): # directory中有622個文件,共2.1G
        uid = resolveUid(file)
        uid2Data[uid] = resolveData(file) # 平均每個file中有30w行數(shù)據(jù)
    for uid,data in uid2Data.items():
        requestServer(uid, data) # 請求server

過程中出現(xiàn)的問題

-server -Xms16384m -Xmx32768m

在執(zhí)行步驟二的時候首先是出現(xiàn)了java.lang.OutOfMemoryError: Java heap space錯誤,錯誤的原因很明顯是JVM堆空間不足,這時統(tǒng)計了下文件系統(tǒng)中數(shù)據(jù)的量級已經(jīng)達到了2.1G,從client端的偽代碼實現(xiàn)也可以看出,是uid2Data這個Map太大了,在裝載這個Map的時候出現(xiàn)了堆內(nèi)存不足。

內(nèi)存不足

由于client端代碼改造成本較大,筆者開始從JVM調(diào)參入手考慮,既然堆內(nèi)存不足,就調(diào)大堆內(nèi)存為-server -Xms16384m -Xmx32768m

  • -server,既然刷數(shù)據(jù)任務(wù)比較漫長,則使用server模式運行重量級虛擬機來對運行時內(nèi)存進行更多優(yōu)化
    JVM有兩種運行模式Server與Client。兩種模式的區(qū)別在于,Client模式啟動速度較快,Server模式啟動較慢;但是啟動進入穩(wěn)定期長期運行之后Server模式的程序運行速度比Client要快很多。這是因為Server模式啟動的JVM采用的是重量級的虛擬機,對程序采用了更多的優(yōu)化;而Client模式啟動的JVM采用的是輕量級的虛擬機。所以Server啟動慢,但穩(wěn)定后速度比Client遠遠要快。
  • -Xms,-Xmx,設(shè)置JVM初始內(nèi)存和最大內(nèi)存

由此,遇到了我們第一個坑,The specified size exceeds the maximum representable size

Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
Invalid initial heap size: -Xms8192m
The specified size exceeds the maximum representable size.
一臉懵逼

OutOfMemoryError,簡稱OOM,是Java開發(fā)中常見的錯誤,OOM是一種Error,區(qū)別于Exception(例如NPE,NullPointerException),Error一般是虛擬機相關(guān)的問題,如系統(tǒng)崩潰、內(nèi)存空間不足、方法調(diào)用棧溢出等,應(yīng)用程序本身無法處理Error類錯誤,這種情況下建議讓程序終止

OutOfMemoryError與FullGC的關(guān)系,F(xiàn)ullGC通常發(fā)生在JVM老年代空間不足,概括理解為JVM不得不停下手上的工作清理屋子的垃圾,JVM區(qū)分垃圾的方式一般是通過GCRoot的強引用,如果一個對象沒有到GCRoot的強引用,則判定為垃圾并進行回收。反之,如果一個對象有到GCRoot的強引用(存活),則不論內(nèi)存多么不足都堅決不回收,這么做的結(jié)果就可能會導(dǎo)致OOM。也就是說,發(fā)生了OOM可以理解為就是內(nèi)存不足,而FullGC關(guān)注的點是運行太慢。

32bit vs. 64bit(jdk與操作系統(tǒng))

經(jīng)過一番排查后,發(fā)現(xiàn)報錯的原因是申請的堆空間超過了最大尋址空間,但是我們的操作系統(tǒng)是windows10 64bit,排查發(fā)現(xiàn)是使用了32位的JDK,導(dǎo)致申請的空間不足。
于是筆者調(diào)低了申請的堆空間大小為4G,因為32位操作系統(tǒng)的尋址范圍是2^32次方,不論實際內(nèi)存空間有多少,操作系統(tǒng)都會給每個進程分配獨立的4GB內(nèi)存空間(虛擬內(nèi)存+頁表),這樣總沒問題了吧,當筆者正因為自己操作系統(tǒng)的知識而沾沾自喜時,同樣的報錯還是發(fā)生了。

得意

經(jīng)過一系列搜索+分析,才發(fā)現(xiàn)操作系統(tǒng)給每個進程分配的空間是用戶空間+內(nèi)核空間,所以其實在windows中JVM最大可用內(nèi)存為1.5G,但是1.5G運行過后還是提示OOM,筆者只好放棄32位JDK,選擇下載64位JDK,調(diào)大JVM堆空間再運行后程序正常執(zhí)行。

反思

調(diào)整代碼邏輯

想必很多讀者已經(jīng)發(fā)現(xiàn)了,“你兜兜轉(zhuǎn)轉(zhuǎn)搞了這么多不就是因為上面那段代碼寫的太爛了嗎?”,確實,所以這也是后面相關(guān)刷數(shù)據(jù)操作需要牢記的原則,一定要合理評估操作的數(shù)據(jù)量并且依據(jù)數(shù)據(jù)量評估實現(xiàn)方案,很多時候會遇到這樣測試時穩(wěn)得一批的代碼,在實際操作大量數(shù)據(jù)的時候血崩的情況。

代碼應(yīng)該調(diào)整為如下

def transfer():
    for file in directory.listFiles(): # directory中有622個文件,共2.1G
        uid = resolveUid(file)
        data = resolveData(file) # 平均每個file中有30w行數(shù)據(jù)
        requestServer(uid, data) # 每個文件內(nèi)容單獨請求一次server

Map-Reduce的思想

當處理的數(shù)據(jù)量大到一定程度時,就會很自然的想到將數(shù)據(jù)分開并發(fā)去請求,因為RPC服務(wù)的server往往是多臺服務(wù)器,可以將622個文件的每次請求都改為并行請求server的一個線程,在執(zhí)行后分別收集每個線程的執(zhí)行結(jié)果并匯總

client-多server

這個思想其實比較類似于Map-Reduce[1]的思想了,并發(fā)請求不同server的過程就是map的過程,收集每個線程的結(jié)果并匯總的過程其實就是reduce的過程。
如果存儲在文件系統(tǒng)中的數(shù)據(jù)很大,大到單機的磁盤不足,就需要放到HDFS下,client腳本也需要去不同HDFS機器上取數(shù)并運算,請求的結(jié)果進行匯總時也需要reduce機器進行合并匯總。

擴展閱讀

  1. MapReduce介紹
  2. java中error和exception的區(qū)別
  3. JVM強引用
  4. JVM的Client模式與Server模式
  5. 《深入理解計算機系統(tǒng)》
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容