在容器中使用Java的資源分配準則

短短几年,容器就改變了軟件行業的開發模式。也許,很多開發者已經開始在容器中運行Java應用。但是,對於容器化的Java應用程序,當遇到CPU和內存佔用等問題時,還是有很多問題需要注意。本文假設讀者對Java和容器技術有基本瞭解,如果需要更多背景知識,可以閱讀文末的參考文獻。

堆空間

如果說在容器中運行Java應用有一條核心定律,那麼就是:對於在容器中運行的Java進程,不要手工設置JVM堆內存。相反的,設置容器的限制。

爲什麼?

首先,設置容器的限制可以實現容器/cgroup提供的基本功能,既隔離容器內進程的資源使用。當我們通過JVM參數手工設置堆內存的時候,就意味着徹底無視這個功能。這樣能夠方便的調整容器資源分配,爲自動化擴縮容容器(例如K8s垂直pod自動擴縮容)打開了大門,而無需手工調整JVM參數。

如果容器運行在編排引擎環境中(例如Kubernetes),那麼容器的限制對於節點健康度和調度都非常重要。調度器需要使用這些限制來找到適合容器運行的節點,同時確保節點之間負載均衡。如果通過JVM參數設置內存使用,這個信息無法通知到調度器,因此調度器無法知道如何爲容器分配負載。

如果不設置容器限制,同時運行在容器中的Java進程也沒有顯式設置JVM內存參數,那麼JVM將會自動設置最大堆內存爲運行節點總內存的25%。例如,如果容器運行在一個內存爲64GB的節點上,JVM進程堆內存最大可設置成16GB。如果這個節點上運行了10個容器(對於自動擴縮容經常發生),那麼可能會突然需要160G內存。

我們能做什麼?

設置容器內存(和CPU)限制,依賴資源請求(軟限制)是不夠的。資源請求對調度器非常有用,但是設置硬限制讓Docker(或者其他容器運行時環境)爲容器分配指定資源,同時確保不會超出。這也讓Java(在Java 8u191之後,默認提供“容器感知”功能)基於容器設置的資源限制自動分配內存,而不是通過運行節點分配。

關於[Min|Max|Initial]RAMPercentage參數

最近的Java版本中,引入瞭如下JVM參數(同時向後移植到了Java 8u191):

  • -XX:MinRAMPercentage
  • -XX:MaxRAMPercentage
  • -XX:InitialRAMPercentage

本文不會詳細介紹這些參數如何工作,但是關鍵點是這些參數可以在不需要直接設置堆內存大小的情況下用於調優JVM堆大小。也就是說,容器仍然可以依賴對其設置的資源限制。

那麼,這些參數的值該怎麼設置呢?答案是:看情況,尤其是依賴於容器上設置的資源限制。

默認設置下,JVM堆內存會設置成容器內存的25%。我們可以通過這些參數來修改初始、最小、最大堆內存。例如,設置-XX:MaxRAMPercentage=50將會允許JVM將容器內存的50%作爲堆內存使用,而不是默認的25%。這樣設置是否安全主要取決於容器運行的內存以及容器內的進程情況。

例如,假設容器只運行一個Java進程,分配了4GB內存,而我們設置了-XX:MaxRAMPercentage=50,此時JVM堆內存上限是2GB。這與默認情況下只能使用1GB內存不同。在這種情況下,50%基本上是非常安全的,也許也是最佳的,因爲還有許多可用內存實際利用率都不高。相反,假設相同的容器只分配了512MB內存,現在設置了-XX:MaxRAMPercentage=50之後,堆內存會佔用256MB內存,而對於容器剩下的所有可用內存就只有256MB了。這些內存需要被容器中運行的其他進程共享,同時還有JVM的Metaspace/PermGen等其他內存使用。因此在這種場景下,50%可能不太安全。

這裏提供如下建議:

  • 除非想爲Java進程壓榨額外內存,否則不要修改這些參數。在大部分情況下默認值25%對於內存管理來說是比較安全的。這個設置對內存來說可能並不是最有效的,但是內存是相對廉價的,同時相比於JVM進程在未知情況下被OOM-kill,還是謹慎一些比較好。

  • 如果非要調試這些參數,還是保守點爲妙。50%通常是個安全值,可以避免(大部分)問題。當然,這還是主要取決於容器內存大小。我不推薦設置成75%,除非容器至少有512MB內存(最好是1GB),同時需要對應用程序的實際內存使用非常瞭解。

  • 如果容器內除了Java進程之外還有其他進程,那麼在調整這些值的時候需要額外的注意。容器內存由其中所有進程共享,因此在這種情況下,瞭解整個容器內存使用會更加複雜。

  • 設置成超過90%可能是在自找麻煩。

對於Metaspace/PermGen/其他內存呢?

這已經超出了本文的範圍,不過這些也可以調整,通常情況下最好不要。大多數情況下,JVM默認行爲已經很好了。如果你發現自己正試圖解決一個晦澀的內存問題,那麼可能需要研究一下JVM內存這個深奧的領域。其他情況,我儘可能避免直接去修改。

對於CPU

對於CPU沒有什麼可做的。從Java 8u191開始,JVM默認情況下已經實現“感知容器”,能夠正確解析CPU共享(CPU Share)設置。這裏有一些細節需要理解,因此我直接附上一篇不錯的文章,詳細介紹相關知識,就不在本文中概述。

總結

現代的Java已經爲容器環境做好了準備,但是爲了應用程序能夠有更好的性能,其中有一些不是那麼明顯的細節需要我們瞭解。我希望本文提供的信息,加上優秀的參考文獻,可以幫助讀者達到這個目的。

參考文獻

附錄:

在64GB/16GB JVM例子中,這裏並不是說JVM進程會爲堆內存自動消費16GB內存,只是說在內存溢出之前,堆內存可以增長到那麼大。另外,由於設置的最大堆內存還有很多,對於垃圾回收器來說沒有壓力,堆內存很容易在觸發垃圾回收之前,消耗多餘容器實際可以提供的內存。這必然會引起應用程序問題(例如OOM錯誤),甚至更嚴重的錯誤(例如被OOM kill,崩潰)。

原文鏈接:

https://www.ccampo.me/java/docker/containers/kubernetes/2019/10/31/java-in-a-container.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章