Java程序運行在Docker等容器環境有哪些新問題


基本回答

一.  對於Java來說,Docker畢竟是一個較新的環境,其內存、CPU等資源限制是通過ControlGroup實現的。早期的JDK版本並不能識別這些限制,進而會導致一些基礎問題。

1.如果未配置合適的JVM堆和元數據區、直接內存等參數,Java就有可能試圖使用超過容器限制的內存,最終被容器OOM kill,或者自身發生OOM。

2.錯誤判斷了可獲取的CPU資源,例如,Docker限制了CPU的核數,JVM就可能設置不合適的GC並行線程數等。

 

二. 從應用打包、發布等角度出發JDK自身就比較大,生成的鏡像就更為臃腫,當我們的鏡像非常多的時候,鏡像的存儲等開銷就比較明顯了。

三. 如果考慮到微服務、Serverless等新的架構和場景,Java自身的大小、內存占用、啟動速度,都存在一定局限性,因為Java早期的優化大多是針對長時間運行的大型服務器端應用。

 

如果真的沒做過,可以從操作系統、容器原理、JVM內部機制、軟件開發實踐等角度展示系統性分心新問題、新場景的能力,畢竟變化才是世界永恆的主題,能夠在新變化中找出共性與關鍵,是優秀工程師的必備能力。

 

擴展

Java在容器環境的局限性來源,Docker到底有什么特別?

雖然看起來Docker之類容器和虛擬機非常相似,例如,有自己的shell,能獨立安裝軟件包,運行時與其他容器互不干擾。但是深入分析會發現,Docker並不是一種完全的虛擬化技術,而更是一種輕量級的隔離技術。

1.從技術角度講,基於namespace,Docker為每個容器提供了單獨的命名空間,對網絡,PID,用戶,IPC通信,文件系統掛載點等實現了隔離。對於CPU\內存,磁盤IO等資源,則是通過ControlGroup等進行管理。

2.Docker僅在類似Linux內核之上實現了有限的 隔離和虛擬化,並不是像傳統虛擬化軟件那樣,獨立運行與一個新的操作系統。如果是虛擬化的操作系統,不管是Java還是其他程序,只要調用的是同一個系統API,都可以透明的獲取所需的信息,基本不需要額外的兼容性改變。

容器雖然省略了虛擬操作系統的開銷,實現了輕量級的目標,但也帶來了額外復雜性,它限制對於應用是不透明的,需要用戶理解Docker的新行為。所以有人曾說過,“幸運的是Docker沒有完全隱藏底層信息,但是不幸的也是Docker沒有隱藏底層信息”。

對於Java平台來說,這些未隱藏的底層信息帶來很多意外的困難,只要體現在以下幾個方面:

1). 容器環境對計算資源的管理方式是全新的,ControlGroup作為相對比較新的技術,歷史版本的Java顯然並不能自然的理解相應的資源機制。

2). namespace對於容器內的應用細節增加了一些微妙的差異,比如jcmd、jstack等工具會依賴於"/proc"下面提供的部分信息,但是Docker的設計改變了這部分信息的原有結構,我需要對原有工具進行修改以適應這種變化。

 

3.從JVM運行機制的角度,為什么這些溝通障礙會導致OOM問題?

這個問題實際反映了JVM如何根據系統資源(內存,CPU等)情況,在啟動時設置默認參數。

這就是所謂的Ergonomics機制,例如:

1)JVM會根據檢測到的內存大小,設置最初啟動時的堆大小為系統內存的1/64;並將堆最大值,設置為系統內存的1/4

2)而JVM檢測到系統的CPU核數,則直接影響到了Parallel GC的並行線程數目和JIT complier線程數目,甚至是我們應用中ForkJoinPool等機制的並行等級。

這些默認參數,是根據通用場景選擇的初始值。但是由於容器環境的差異,Java的判斷很可能是基於錯誤信息而做出的,這就類似我以為我住的是整棟別墅,實際上卻只有一個房間是給我住的。

 更加嚴重的是,JVM的一些原有診斷或備用機制也受到影響,為保證服務的可用性,一種常見的選擇是依賴 -XX:OnOutOfMemoryError 功能,通過調用處理腳本的形式來做一些補救措施,比如自動重啟服務等,但是這種機制是基於fork實現的,當Java進程已經過度提交內存時,fork新的進程往往已經不可能正常運行了。

該如何解決這些問題呢?

1))首先,如果能升級到最新的JDK版本,就一切ok了

針對這種情況,JDK9中引入了一些實驗性的參數,以方便Docker和Java溝通,通過針對內存限制,可以使用下面的參數設置:

-XX:+UnlockExperimentaVMOptions

-XX:+UserCGroupMemoryLimitForHeap

這兩個參數是順序敏感的,並且只支持Linux環境。而對於CPU核數限定,Java已經可以正確理解-cpuset-cpus等設置,,無需單獨設置參數。

2))如果可以切換到JDK10或者更新的版本,問題就更加簡單了。Java對容器Docker的支持已經比較完善,默認就會自適應各種資源限制和實現差異,前面提到的實驗性參數已經被標記為廢棄。於此同時,新增了參數用以明確指定CPU核心的數目。

-xx:ActiveProcessorCount=N

如果實踐中發現有問題,也可以使用-XX:UseContainerSupport,關閉Java容器的支持特性,這可作為一種防御型機制,避免新特性破壞原有功能。

 幸運的是,JKD9中的實驗性改進已經被移植到Oracle JDK 8u131中了,可以直接下載鏡像,並配置UseCGroupMemoryLimitForHeap

3))如果暫時只能用JDK老版本怎么辦?

 第一:明確設置堆、元數據區等內存區域大小,保證Java進程的總大小可控,

限制容器內存: $ docker run -it --rm --name your container -p 8080:8080 -m 800M repo/your -java-container:openjdk

這樣就可以額外配置下面的環境變量,直接指定JVM堆大小 -e JAVA_OPTIONS='xMX300m'

第二:明確GC和JIT並行線程數目,以避免二者占用過多資源。

-XX:ParallelGCTreads

-XX:CICompilerCount

第三:Java在Docker環境中會意外Swap。

當內存消耗達到一定門限,操作系統會試圖將不活躍的進程喚出

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM