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)設定為物理核心數量。不妨舉例說明。
我們將執行一個簡單的應用程式,它消耗盡可能多的記憶體(可在該網站上找到):
我們在擁有64GB記憶體的系統上執行,所以不妨檢查預設的最大堆大小:
如上所述,它是實體記憶體的1/4即16GB。如果我們使用docker cgroups限制記憶體,會發生什麼?不妨檢查一下:
JVM程式被殺死了。由於它是一個子程式――容器本身倖存下來,但通常當java是容器(PID 1)內的唯一程式時,容器會崩潰。
不妨深入看看系統日誌:
像這樣的故障除錯起來可能很難――應用程式日誌中沒有任何內容。在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愉快!