Java 8 終於支援 Docker!

遺失的拂曉發表於2019-07-21

Java 8曾經與Docker無法很好地相容性,現在問題已消失。

請注意:我在本文中使用採用GNU GPL v2許可證的OpenJDK官方docker映像。在Oracle Java SE中,這裡描述的docker支援功能在更新191中引入。Oracle在2019年4月更改了Java 8更新的許可證,自Java SE 8 Update 211以來商業使用不再免費。

你是否遇到過在docker中執行的基於JVM的應用程式出現“隨機”故障?或者也許是一些奇怪的當機?兩者都可能是Java 8(仍廣泛使用的)中糟糕的docker支援引起的。

Docker使用控制組(cgroups)來限制資源。在容器中執行應用程式時限制記憶體和CPU絕對是個好主意――它可以阻止應用程式佔用整個可用記憶體及/或CPU,這會導致在同一個系統上執行的其他容器毫無反應。限制資源可提高應用程式的可靠性和穩定性。它還允許為硬體容量作好規劃。在Kubernetes或DC/OS之類的編排系統上執行容器時尤為重要。

問題

JVM可以“看到”系統上的整個記憶體和可用的所有CPU核心,並確保與資源一致。它預設情況下將最大堆大小(heap size)設定為系統記憶體的1/4,並將某些執行緒池大小(比如針對GC)設定為物理核心數量。不妨舉例說明。

我們將執行一個簡單的應用程式,它消耗盡可能多的記憶體(可在該網站上找到):

img

我們在擁有64GB記憶體的系統上執行,所以不妨檢查預設的最大堆大小:

如上所述,它是實體記憶體的1/4即16GB。如果我們使用docker cgroups限制記憶體,會發生什麼?不妨檢查一下:

img

JVM程式被殺死了。由於它是一個子程式――容器本身倖存下來,但通常當java是容器(PID 1)內的唯一程式時,容器會崩潰。

不妨深入看看系統日誌:

img

img

像這樣的故障除錯起來可能很難――應用程式日誌中沒有任何內容。在AWS ECS之類的託管系統上尤其困難重重。

CPU怎麼樣?不妨再次檢查,執行一個顯示可用處理器數量的小程式:

不妨在一個cpu編號設定為1的docker容器中執行它:

不好,這個系統上的確有12個CPU。因此,即使可用處理器的數量限制為1,JVM也會嘗試使用12――比如說,GC執行緒數量由該公式設定:

在擁有N個硬體執行緒(N大於8)的機器上,並行收集器使用N的固定分數作為垃圾收集器執行緒的數量。如果N的值很大,該分數約5/8。如果N的值低於8,使用的數字是N。

在我們的情況下:

解決方案

OK,我們現在意識到了這個問題。有解決方案嗎?幸運的是,有!

新的Java版本(10及以上版本)已經內建了docker支援功能。但有時升級不是辦法,比如說如果應用程式與新JVM不相容就不行。

好訊息:Docker支援還被向後移植到Java 8。不妨檢查標記為8u212的最新openjdk映像。我們將記憶體限制為1G,並使用1個CPU:docker run -ti --cpus 1 -m 1G openjdk:8u212-jdk。

記憶體:

它是256M,正好是已分配記憶體的1/4。

CPU:

正如我們想要的那樣。

此外,還有幾個新的設定:

它們允許微調堆大小――這些設定的含義在StackOverflow的這個優秀答案中已得到了解釋。請注意:他們設定的是百分比,而不是固定值。正因為如此,改變Docker記憶體設定不會破壞任何東西。

如果由於某種原因不想要看到新的JVM行為,可以使用-XX:-UseContainerSupport來關閉。

總結

為基於JVM的應用程式設定正確的堆大小極其重要。如果使用最新的Java 8版本,你可以依賴安全(但非常保守)的預設設定。不需要在docker入口點中使用任何變通辦法,也不需要再將Xmx設定為固定值。

使用JVM愉快!

相關文章