Java 8曾經與Docker無法很好地相容性,現在問題已消失。 請註意:我在本文中使用採用GNU GPL v2許可證的OpenJDK官方docker映像。在Oracle Java SE中,這裡描述的docker支持功能在更新191中引入。Oracle在2019年4月更改了Java 8更新的許可證, ...
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愉快!