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愉快!