1. 程式人生 > >在 Docker 裡跑 Java,趟坑總結

在 Docker 裡跑 Java,趟坑總結

背景:眾所周知,當我們執行沒有任何調優引數(如“java-jar mypplication-fat.jar”)的 Java 應用程式時,JVM 會自動調整幾個引數,以便在執行環境中具有最佳效能。

但是許多開發者發現,如果讓 JVM ergonomics (即JVM人體工程學,用於自動選擇和行為調整)對垃圾收集器、堆大小和執行編譯器使用預設設定值,執行在 Linux 容器(docker,rkt,runC,lxcfs 等)中的 Java 程序會與我們的預期表現嚴重不符。

本篇文章採用簡單的方法來向開發人員展示在 Linux 容器中打包 Java 應用程式時應該知道什麼。

懶人超精簡閱讀版:

a.JVM 做不了記憶體限制,一旦超出資源限制,容器就會出錯

b.即使你多給些記憶體資源,也沒什麼卵用,只會錯上加錯

c.解決方案:用 Dockfile 中的環境變數來定義 JVM 的額外引數

d.更進一步:使用由 Fabric8 社群提供的基礎 Docker 映象來定義 Java 應用程式,將始終根據容器調整堆大小

詳細全文:

我們往往把容器當虛擬機器,讓它定義一些虛擬 CPU 和虛擬記憶體。其實容器更像是一種隔離機制:它可以讓一個程序中的資源(CPU,記憶體,檔案系統,網路等)與另一個程序中的資源完全隔離。Linux 核心中的 cgroups 功能用於實現這種隔離。

然而,一些從執行環境收集資訊的應用程式已經在 cgroups 存在之前就被執行了。“top”,“free”,“ps”,甚至 JVM 等工具都沒有針對在容器內執行高度受限的 Linux 程序進行優化。

1.存在的問題

為了演示,我用“docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”在1GB RAM 的虛擬機器中建立了 docker daemon。接下來,在一個虛擬記憶體為100MB 的容器裡面跑三個不同的Linux distribution,執行 “free -h”命令,結果是:它們都顯示了995MB 的總記憶體。

即使在 Kubernetes / OpenShift 叢集中,結果也類似。

我在一個15GB 記憶體的叢集中跑一個 Kubernetes Pod ,並將 Pod 的記憶體限制為512M (通過“kubectl run mycentos –image=centos -it –limits=’memory=512Mi'”命令實現),最後顯示的總記憶體卻是14GB

如果想知道為什麼會發生這種情況,建議您閱讀部落格“Memoryinside Linux containers – Or why don’t free and top work in a Linux container?”(https://fabiokung.com/2014/03/13/memory-inside-linux-containers/)

docker switches(-m,-memory和-memory-swap)和kubernetes switch(–limits)在程序超過限制的情況下,會指示 Linux 核心殺死該程序;但 JVM 是完全不知道限制,所以在程序超過限制的時候,糟糕的事情就發生了!

為了模擬在超過指定的記憶體限制後被殺死的程序,我們可以通過“docker run -it –name mywildfly -m=50m jboss/wildfly” 命令在50MB 記憶體限制的容器中跑WildFly應用 server,用 “dockerstats” 命令來檢查容器限制。

但是在幾秒鐘之後,Wildfly 的容器執行將被中斷並顯示:*** JBossAS process (55) received KILL signal ***

“docker inspect mywildfly -f ‘{{json.State}}'” 命令顯示由於 OOM(記憶體不足),該容器已被殺死。注意容器 “state” 中的OOMKilled = true。

2.JAVA的應用程式是如何被影響的?

在docker daemon裡用 Dockerfile 中定義的引數-XX:+ PrintFlagsFinal和-XX:+ PrintGCDetails起一個 java 應用。

其中 machine:1GB RAM 容器記憶體:限制為150M (對於這個Spring Boot應用,似乎夠用)

這些引數允許我們讀取初始JVM人機工程學引數,並瞭解有關垃圾收集(GC)執行的詳細資訊。

動手試一下:

我已經在“/ api / memory /”上準備了一個端點,它使用 String 物件載入 JVM 記憶體來模擬消耗大量記憶體的操作。我們來呼叫一次:

此端點將回復“分配超過80%(219.8 MiB)的最大允許 JVM 記憶體大小(241.7 MiB)”

在這裡我們可以提至少兩個問題:

  • 為什麼JVM最大允許記憶體241.7 MiB?

  • 如果這個容器將記憶體限制為150MB,那為什麼它允許Java分配近220MB?

首先,我們需要回顧一下 JVM 人機工程學頁面上關於“最大堆大小”的內容:是實體記憶體的1/4。由於 JVM 不知道它在一個容器內執行,所以允許最大堆大小將接近260MB。鑑於我們在容器初始化期間添加了-XX:+ PrintFlagsFinal標誌,我們可以檢查這個值:

其次,我們需要了解,當我們在 docker 命令列中使用引數“-m 150M”時,docker daemon將在RAM中限制150M ,在 Swap 中限制為150M。因此,該過程可以分配300M。這就解釋了為什麼我們的程序沒有被殺死。

docker 命令列中的記憶體限制(-memory)和swap(-memory-swap)之間的更多組合可以在這裡(https://docs.docker.com/engine/reference/run/#example-run-htop-inside-a-container)找到。

3.提供更多記憶體是否靠譜?

不瞭解問題的開發者往往認為環境不能為執行 JVM 提供足夠的記憶體。所以通常的解決辦法是提供更多記憶體,這實際上會使事情變得更糟。

我們假設將 daemon 從1GB 更改為8GB (使用“docker-machinecreate -d virtualbox –virtualbox-memory ‘8192’ docker8192”建立),並將容器記憶體從150M 更改為800M :

請注意這次, “curl http://`docker-machine ipdocker8192`:8080/api/memory” 命令甚至沒有執行完,因為在8GB 環境中計算的 JVM 的MaxHeapSize 為2092957696位元組(〜2GB)。檢查 “docker logs mycontainer|grep -i MaxHeapSize”

該應用將嘗試分配超過1.6GB 的記憶體,這超出了此容器的限制(RAM 中的800MB + Swap中的800MB),並且該程序將被殺掉。

很顯然,用增加記憶體且讓 JVM 自定義引數的方式在容器裡跑Java,不是什麼好主意。 在容器內部執行 Java 應用程式時,我們應該根據應用程式需求和容器限制設定最大堆大小(-Xmx引數)。

4.解決方案

Dockerfile 的一個細微變化允許使用者指定一個環境變數來定義 JVM 的額外引數。 檢查以下行:

現在我們可以使用 JAVA_OPTIONS 環境變數來通知 JVM 堆的大小。對於這個應用程式,300M 就夠了。稍後可以檢查日誌並獲取314572800位元組(300MBi)的值

對於docker,您可以使用“-e”switch指定環境變數。

在Kubernetes中,您可以使用switch “-env = [key = value]”設定環境變數:

再進一步

如果可以根據容器限制自動計算堆的值,該怎麼做?

使用由Fabric8社群提供的基礎Docker映象,就可以搞定。這個映象 fabric8 / java-jboss-openjdk8-jdk 使用一個指令碼來計算容器限制,並使用50%的可用記憶體作為上限。 請注意,這個50%的記憶體比可以被複寫。 您還可以使用此映象來啟用/禁用除錯,診斷等。

下面一起看看 Dockerfile 是如何作用於這個 Spring Boot 應用程式:

搞定!現在,無論容器記憶體限制是多少,我們的 Java 應用程式將始終根據容器調整堆大小,而不是根據 daemon 調整堆大小。

5.結論

直到現在,Java JVM依然沒有提供什麼支援,讓大家可以理解它在容器內是如何執行的,而且它有一些資源是記憶體和CPU限制的。 因此,您不能讓JVM人體工程學本身決定最大堆大小。

解決此問題的一種方法是使用能夠理解它在受限容器內執行的Fabric8 Base映象

在JVM中有一個實驗支援,已經包含在JDK9中以支援容器(即Docker)環境中的cgroup記憶體限制。可以參考:http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/5f1d1df0ea49

原文評論:更好的方法是以 exec 表單定義您的 CMD 指令,這將確保 java 是PID 1程序 - 這對於允許 Java 在容器停止時正常關閉至關重要。

Exec表單不支援環境變數替換,但您可以通過設定JAVA_TOOL_OPTIONS環境變數來傳遞其他命令列標誌(請參閱http://bit.ly/2mTIDUt)

以上,時速雲翻譯,轉載請註明出處!

轉自:https://my.oschina.net/shisuyun/blog/871514