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 )執行的詳細資訊。

動手試一下:

$ docker run -it --rm --name mycontainer150 -p 8080:8080 -m 150M rafabene/java-container:openjdk

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

$ curl http://docker-machine ip docker1024:8080/api/memory

此端點將回復“分配超過 80 %( 219.8 MiB )的最大允許 JVM 記憶體大小( 241.7 MiB )” 在這裡我們可以提至少兩個問題: 為什麼 JVM 最大允許記憶體 241.7 MiB ? 如果這個容器將記憶體限制為 150MB ,那為什麼它允許 Java 分配近 220MB ? 首先,我們需要回顧一下 JVM 人機工程學頁面上關於“最大堆大小”的內容:是實體記憶體的 1/4 。由於 JVM 不知道它在一個容器內執行,所以允許最大堆大小將接近 260MB 。鑑於我們在容器初始化期間添加了-XX :+ PrintFlagsFinal 標誌,我們可以檢查這個值:

$ docker logs mycontainer150|grep -i MaxHeapSize uintx MaxHeapSize := 262144000 {product}

其次,我們需要了解,當我們在 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 :

$ docker run -it --name mycontainer -p 8080:8080 -m 800M rafabene/java-container:openjdk

請注意這次, “ 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 的額外引數。 檢查以下行:

CMD java -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA_OPTIONS -jar java-container.jar

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

$ docker run -d --name mycontainer8g -p 8080:8080 -m 800M -e JAVA_OPTIONS='-Xmx300m' rafabene/java-container:openjdk-env $ docker logs mycontainer8g|grep -i MaxHeapSize uintx MaxHeapSize := 314572800 {product}

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

$ kubectl run mycontainer --image=rafabene/java-container:openjdk-env --limits='memory=800Mi' --env="JAVA_OPTIONS='-Xmx300m'" $ kubectl get pods NAME READY STATUS RESTARTS AGE mycontainer-2141389741-b1u0o 1/1 Running 0 6s $ kubectl logs mycontainer-2141389741-b1u0o|grep MaxHeapSize uintx MaxHeapSize := 314572800 {product}

再進一步

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

FROM fabric8/java-jboss-openjdk8-jdk:1.2.3 ENV JAVA_APP_JAR java-container.jar ENV AB_OFF true EXPOSE 8080 ADD target/$JAVA_APP_JAR /deployments/

下面一起看看 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 )

相關推薦

Docker Java總結

背景:眾所周知,當我們執行沒有任何調優引數(如“java-jar mypplication-fat.jar”)的 Java 應用程式時,JVM 會自動調整幾個引數,以便在執行環境中具有最佳效能。 但是許多開發者發現,如果讓 JVM ergonomics (即JVM人體工程學

Docker Java 總結

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

MySQL 到底能不能放到 Docker

前言 前幾月經常看到有 MySQL 到底能不能放到 Docker 裡跑的各種討論。這樣做是錯的!這樣做是對的!說錯的理由也說了一大堆,說對的思想也很明確。大家都有道理。但是我本人覺得這樣的討論落地意義不大。因為對與錯還是要實踐來得出的。 所以同程旅遊也很早開始了 MySQL 的 Do

Jetson TX2 開箱配置+刷機+demo(常見總結

杰特森系列是英偉達公司推出的面向無人智慧化領域的嵌入式平臺,這塊嵌入式板子的出現使得我們可以在邊緣裝置上處理複雜資料,實現人工智慧。 sudo apt-get install libpng 更換步驟 以根身份開啟/etc/apt/sources.list,至於用什麼開啟隨意

vue + element-ui 製作tab切換(切換vue元件總結

本篇文章使用vue結合element-ui開發tab切換vue的不同元件,每一個tab切換的都是一個新的元件。 1、vue如何使用element-ui 上一篇文章已經分享瞭如何在vue中使用element-ui建立tab元件切換內容(需要了解的朋友點選連結檢視) 2、建立相應檔案。   a、建立父元件

整合 KAFKA+Flink 例項(第一部分記錄)

2017年後,一大波網路喧囂,說流式處理如何牛叉,如何高大上,抱歉,工作滿負荷,沒空玩那個; 今年疫情隔離在家,無聊,開始學習 KAFKA+Flink ,目前的打算是用爬蟲抓取網頁資料,傳遞到Kafka中,再用Flink計算。 個人性格原因,我不願意過分沉迷於紙質或者電子教程材料,也不是特別喜歡網上

Java開始Spring boot 的後端開發之路

也有 得來 中間 調用 代碼 行記錄 廣泛 隨手記 domain 換了工作,從遊戲行業的大坑中走了出來,走向互聯網的大世界。新的公司是電商方向,電商行業萬變不離其宗,java,spring是最廣泛的技術。當然也有過一些特立獨行的,也做得很大,不過現在都基本切換到了這個方向。

圖表的線上預覽和java 生成pdf下載itext問題總結

1需求 圖表的線上預覽和下載 2.實現 1 採用html進行展示,再用js程式碼進行快照,進行pdf儲存。實現簡單,無後臺,但是圖片質量差,不可編輯  感謝https://blog.csdn.net/program_guys/article/details/79035244

JavaC#,JavaScript命名的一些總結

一、Java與C#的命名 變數命名: 變數命名習慣上採用匈牙利命名法或駱駝命名法。如定義使用者的一些欄位。 匈牙利命名法: int iAge= 28; string sName = "張三"; boolean bManage = true; char cPlan = 'X'

阿里五年Java程式設計師的總結獻給還在迷茫中的你!

我越來越擔心我作為一個Java程式設計師的未來。 恍然間,發現自己在這個行業裡已經摸爬滾打了五年了,原以為自己就憑已有的專案經驗和工作經歷怎麼著也應該算得上是一個業內比較資歷的人士了,但是今年在換工作的過程中卻遭到了重大的挫折。詳細過程我就不再敘述,在此,只想給大家說一說被拒絕的原因,看看大家有

QVariant(相當於是Java面的Object是萬能的容器但要註冊)

這個型別相當於是Java裡面的Object,它把絕大多數Qt提供的資料型別都封裝起來,起到一個數據型別“擦除”的作用。比如我們的 table單元格可以是string,也可以是int,也可以是一個顏色值,那麼這麼多型別怎麼返回呢?於是,Qt提供了這個QVariant型別,你可以把這很多型別都存放進去,到需要

java的強引用、軟引用、弱引用、幻象引用引用佇列總結

java的強引用、軟引用、弱引用、幻象引用,引用佇列總結 java除了原始資料型別的變數,其他所有都是引用型別。 引用分為強引用、軟引用、弱引用、幻象引用,這幾種引用影響著物件的回收 強引用 強引用:形如Object object = new Object();這樣就是典型的強引用,

阿里6年Java程式設計師的總結獻給還在迷茫中的你!

我越來越擔心我作為一個Java程式設計師的未來。 恍然間,發現自己在這個行業裡已經摸爬滾打了五年了,原以為自己就憑已有的專案經驗和工作經歷怎麼著也應該算得上是一個業內比較資歷的人士了,但是今年在換工作的過程中卻遭到了重大的挫折。詳細過程我就不再敘述,在此,只想給大家說一說被拒絕的原因,看看大家有

Hole1: Pycharm已經安裝好jieba包但是在命令列pycharm卻一直報錯

1 首先,檢查你的pycharm的環境是否和全域性環境一致,我們可以通過在終端輸入 which python 顯示出路徑 /Users/writ/anaconda3/bin/python 然後進入pycharm的preference介面;修改路徑至上面的路徑,修改完後你會發現ji

com.google.gson.JsonSyntaxException: java.lang.NumberFormatException使用GsonFormat記錄。

今天依舊從PostMan測試介面,拿JSON格式資料。使用GsonFormat進行格式轉換。 然後進行網路請求,但是執行程式,出現錯誤。 錯誤如下所示: com.google.gson.JsonSynt

Java面試題每日一總結(2)

1.字串String和StringBuilder 、StringBuffer的區別?StringBuilder和StringBuffer的區別? 分析:java提供了String和StringBuilder 、StringBuffer三種表示和操作字串的類。字串就是有多個字

阿里八年Java架構學習經驗總結第六點尤為重要!

你有沒有靜下心來思考過:同樣是做了x年Java開發,為什麼你的技術比別人差很多?為什麼別人每月28K你卻只有10K? 其實技術水平的高低和個人智商關係不大(畢竟能做Java程式設計開發大家都不會差),主要和勤奮程度、提升方法有關。 勤奮程度不必多說,全靠自我監督和自制力。在這裡我們詳細談談提

釋放雲的無限潛能——Docker

分享主題:《Docker的正確開啟方式》常江 靈雀雲架構師 分享簡介: 1:統一管理底層IAAS資源 2:一套映象、多層環境 3: 遠端登入 4:服務發現機制 5:CICD流程整合 6:docker容災 嘉賓介紹 常江:靈雀雲解決方案架構師,一直在docker,雲端計算一線領域奮戰,aws 認證解決方案架構

java:Applet佈局問題,如何新增按鈕標籤問題總結

今天學習了applet佈局問題:按鈕、標籤是怎樣加入到applet程式(容器)當中去的呢?需要用到佈局容器管理器(LayoutManager):它用於協助容器確定加入的元件(按鈕等)應該放置的位置 awt提供了4中常用的佈局容器管理器>>FlowLayout>>BorderLayou

Java最經典知識點總結看完你都記住的了嗎?

1實現多執行緒的方式有幾種? 其實這個問題並不難,只是在這裡做一個總結。一共有三種。 實現Runnable介面,並實現該介面的run()方法 繼承Thread類,重寫run()方法 實現Callable介面,實現call()方法。 大家可能對前兩種已經很清楚了,重點