Docker容器内部署Java微服务的内存限制问题
1. 前言
文末有福利
前兩天有同事發(fā)現(xiàn),通過華為云 ServiceStage 的流水線部署基于模板創(chuàng)建的 CSEJavaSDK demo 服務時,會在容器啟動過程中報錯。初步排查是由于 JVM 占用的內存超出了 docker 內存配額的上限,導致容器被 kill 掉。于是我們需要排查一下問題出在哪里,為什么以前沒有這類問題,而現(xiàn)在卻發(fā)生了。
2. 基本定位
要確定 docker 容器內存超限問題的直接原因并不難。直接進入docker容器,執(zhí)行 top 命令,我們發(fā)現(xiàn)宿主機是一臺8核16G的機器,而且 docker 并不會屏蔽這些信息,也就是 JVM 會認為自己工作于一臺 16G 內存的機器上。而查看 demo 服務的 Dockerfile,發(fā)現(xiàn)運行服務時并沒有對 JVM 的內存進行任何限制,于是 JVM 會根據(jù)默認的設置來工作 —— 最大堆內存為物理內存的1/4(這里的描述并不完全準確,因為 JVM 的默認堆內存大小限制比例其實是根據(jù)物理內存有所變化的,具體內容請自行搜索資料),而基于模板創(chuàng)建的 ServiceStage 流水線,在部署應用堆棧的時候會把 docker 容器的內存配額默認設置為 512M,于是容器就會在啟動的時候內存超限了。至于以前沒有碰到過這種問題的原因,只是因為以前沒將這么高規(guī)格的 ECS 服務器用于流水線部署應用堆棧。
在查詢過相關資料后,我們找到了兩種問題解決方案,一個是直接在 jar 包運行命令里加上 -Xmx 參數(shù)來指定最大堆內存,不過這種方式只能將 JVM 堆內存限制為一個固定的值;另一個方法是在執(zhí)行 jar 包時加上 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap 參數(shù),讓 JVM 能夠感知到docker容器所設置的 cgroup限制,相應地調整自身的堆內存大小,不過這個特性是 JDK 8u131 以上的版本才具備的。
最終,我們提醒 ServiceStage 流水線的同學將 CSEJavaSDK demo 的創(chuàng)建模板做了改進,在 Dockerfile 中將打包的基礎鏡像版本由原先的 java:8u111-jre-alpine 升級為了 openjdk:8u181-jdk-alpine,并且在運行服務 jar 包的命令中加上了 -Xmx256m 參數(shù)。問題至此已經解決了。
3. 進一步的探究
雖然問題已經解決,但是在好奇心的驅使下,我還是打算自己找個 demo 實際去觸發(fā)一下問題,另外看看從網(wǎng)上搜到的解決方法到底好不好用 : )
3.1 準備工作
創(chuàng)建云上工程
首先需要在華為云 ServiceStage 創(chuàng)建一個云上工程。
在 ServiceStage -> 應用開發(fā) -> 微服務開發(fā) -> 工程管理 -> 創(chuàng)建云上工程中,選擇“基于模板創(chuàng)建”,語言選擇 Java, 框架選擇 CSE-Java (SpringMVC),部署系統(tǒng)選擇“云容器引擎CCE”,給你的云上工程取一個名字,比如test-memo-consuming,最后選擇存放代碼的倉庫,就可以完成云上工程的創(chuàng)建了。
之后云上工程會根據(jù)你的選項自動地生成腳手架代碼,上傳到你指定的代碼倉庫中,并且為你創(chuàng)建一條流水線,完成代碼編譯、構建、打包、歸檔鏡像包的操作,并且使用打好的 docker 鏡像包在 CCE 集群中部署一個應用堆棧。
創(chuàng)建云上工程和流水線不是本文的重點,我就不詳細講操作了 : )。同一個應用堆棧的實例可以部署多個,在這里為了實驗方便就按照默認值1個來部署。
由于云上工程已經改進了腳手架代碼的模板,不會再出現(xiàn)內存超限的問題,所以我們現(xiàn)在能看到 demo 服務已經正常的跑起來,微服務實例已經注冊到服務中心了。
登錄到 demo 服務所部署的容器,使用curl命令可以調用 demo 服務的 helloworld 接口,可以看到此時服務已經可以正常工作。
增加實驗代碼
為了能夠觸發(fā)微服務實例消耗更多的內存,我在項目代碼中增加了如下接口,當調用/allocateMemory接口時,微服務實例會不停申請內存,直到 JVM 拋出 OOM 錯誤或者容器內存超限被 kill 掉。
private HashMap cacheMap = new HashMap<>();
@GetMapping(value = "/allocateMemory")
public String allocateMemory() {
LOGGER.info("allocateMemory() is called");
try {
for (long i = 0; true; ++i) {
cacheMap.put("key" + i, new long[1024 * 1024]);
}
} catch (Throwable t) {
LOGGER.info("allocateMemory() gets error", t);
}
return "allocated";
}
此時用來打鏡像包的基礎鏡像是openjdk:8u181-jdk-alpine,jar 包啟動命令中加上了-Xmx256m參數(shù)。
執(zhí)行流水線,應用堆棧部署成功后,調用/allocateMemory接口觸發(fā)微服務實例消耗內存,直到 JVM 拋出 OOM 錯誤,可以在 ServiceStage -> 應用上線 -> 應用管理中選擇相應的應用,點擊進入概覽頁面,查看應用使用內存的情況。
應用使用的內存從 800M+ 陡然下降的時間點就是我重新打包部署的時間,而之后由于調用/allocateMemory接口,內存占用量上升到了接近 400M,并且在這個水平穩(wěn)定了下來,顯示-Xmx256m參數(shù)發(fā)揮了預期的作用。
3.2 復現(xiàn)問題
現(xiàn)在將 demo 工程中的 Dockerfile 修改一下,將基礎鏡像改為 java:8u111-jre-alpine,并且刪除啟動命令中的-Xmx256m參數(shù),將其提交為noLimit_oldBase分支,推送到代碼倉庫中。然后編輯流水線,將 source 階段的任務所使用的代碼分支改為noLimit_oldBase分支,保存并重新運行流水線,將新的代碼打包部署到應用堆棧中。
在微服務實例列表中查詢到新的微服務實例的 endpoint IP 后,調用/allocateMemory接口,觀察內存情況,內存從接近 400M 突然掉下去一下,然后又上升到約 450M 的時間點就是修改代碼后的微服務實例部署成功的時間點,之后內存占用量突然下跌就是因為調用/allocateMemory接口導致容器內存超限被 kill 掉了。
如果你事先使用docker logs -f命令查看容器日志的話,那么日志大概是這個樣子的
2018-11-23 15:40:04,920 INFO SCBEngine:152 - receive MicroserviceInstanceRegisterTask event, check instance Id...
2018-11-23 15:40:04,920 INFO SCBEngine:154 - instance registry succeeds for the first time, will send AFTER_REGISTRY event.
2018-11-23 15:40:04,925 WARN VertxTLSBuilder:116 - keyStore [server.p12] file not exist, please check!
2018-11-23 15:40:04,925 WARN VertxTLSBuilder:136 - trustStore [trust.jks] file not exist, please check!
2018-11-23 15:40:04,928 INFO DataFactory:62 - Monitor data sender started. Configured data providers is {com.huawei.paas.cse.tcc.upload.TransactionMonitorDataProvider,com.huawei.paas.monitor.HealthMonitorDataProvider,}
2018-11-23 15:40:04,929 INFO ServiceCenterTask:51 - read MicroserviceInstanceRegisterTask status is FINISHED
2018-11-23 15:40:04,939 INFO TestmemoconsumingApplication:57 - Started TestmemoconsumingApplication in 34.81 seconds (JVM running for 38.752)
2018-11-23 15:40:14,943 INFO AbstractServiceRegistry:258 - find instances[1] from service center success. service=default/CseMonitoring/latest, old revision=null, new revision=28475010.1
2018-11-23 15:40:14,943 INFO AbstractServiceRegistry:266 - service id=8b09a7085f4011e89f130255ac10470c, instance id=8b160d485f4011e89f130255ac10470c, endpoints=[rest://100.125.0.198:30109?sslEnabled=true]
2018-11-23 15:40:34,937 INFO ServiceCenterTaskMonitor:39 - sc task interval changed from -1 to 30
2018-11-23 15:47:03,823 INFO SPIServiceUtils:76 - Found SPI service javax.ws.rs.core.Response$StatusType, count=0.
2018-11-23 15:47:04,657 INFO TestmemoconsumingImpl:39 - allocateMemory() is called
Killed
可以看到allocateMemory方法被調用,然后 JVM 還沒來得及拋出 OOM 錯誤,整個容器就被 kill 掉了。
這里也給大家提了一個醒:不要以為自己的服務容器能啟動起來就萬事大吉了,如果沒有特定的限制,JVM 會在運行時繼續(xù)申請堆內存,也有可能造成內存用量超過 docker 容器的配額!
3.3 讓 JVM 感知 cgroup 限制
前文提到還有另外一種方法解決 JVM 內存超限的問題,這種方法可以讓 JVM 自動感知 docker 容器的 cgroup 限制,從而動態(tài)的調整堆內存大小,感覺挺不錯的。我們也來試一下這種方法,看看效果如何 ; )
回到demo項目代碼的master分支,將 Dockerfile 中啟動命令參數(shù)的-Xmx256m替換為-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,提交為useCGroupMemoryLimitForHeap分支,推送到代碼倉庫里。再次運行流水線進行構建部署。
等 demo 服務部署成功后,再次調用/allocateMemory接口,容器的內存占用情況如上圖所示(最右邊的那一部分連續(xù)曲線),內存上升到一定程度后,JVM 拋出了 OOM 錯誤,沒有繼續(xù)申請堆內存。看來這種方式也是有效果的。不過,仔細觀察容器的內存占用情況,可以發(fā)現(xiàn)容器所使用的內存僅為不到 300M,而我們對于這個容器的內存配額限制為 512M,也就是還有 200M+ 是閑置的,并不會被 JVM 利用。這個利用率,比起上文中直接設置-Xmx256m的內存利用率要低 : ( 。推測是因為 JVM 并不會感知到自己是部署在一個 docker 容器里的,所以它把當前的環(huán)境當成一個物理內存只有 512M 的物理機,按照比例來限制自己的最大堆內存,另一部分就被閑置了。
如此看來,如果想要充分利用自己的服務器資源,還是得多花一點功夫,手動調整好-Xmx參數(shù)。
這里也為大家準備了一節(jié)視頻:
視頻詳情
干貨列表
1,Docker安裝過程的各種坑
2,完成Docker倉庫環(huán)境搭建
3,增加模板基礎鏡像,供JAVA應用項目使用
4,將定制化的鏡像上傳到倉庫及注意事項
5,Dockfile詳解,結合Java應用程序,將JAVA應用服務鏡像發(fā)布到倉庫
6,企業(yè)常用技能Maven打包創(chuàng)建本地鏡像,推送倉庫及部署;
7,創(chuàng)建Java應用服務容器,完成代碼到容器的發(fā)布及應用自啟
8,演示測試
什么是Docker?
docker 是一個開源的應用容器引擎,讓開發(fā)者可以打包他們的應用以及依賴包到一個可移植的容器中,然后發(fā)布到任何流行的Linux服務器上,可以實現(xiàn)虛擬化,容器是完全使用沙箱(沙盒)機制,相互之間不會有任何接口。
那些在玩docker的大佬
京東:http://www.infoq.com/cn/news/2015/06/jd-618-docker
騰訊內部:http://www.infoq.com/cn/articles/tencent-millions-scale- docker-application-practice
阿里巴巴:http://www.infoq.com/cn/news/2015/07/paas-tae-dock
Docker性能
Docker容器啟動速度秒級,基于操作系統(tǒng)內核技術,對現(xiàn)有基礎設施的侵入較少,所有實現(xiàn)在內核中完成,所以性能幾乎與原生一致,依賴簡單,與進程無本質區(qū)別
正確理解Docker內部流程
Docker線上環(huán)境操作流程
Docker實戰(zhàn)
1,Docker安裝過程的各種坑
2,完成Docker倉庫環(huán)境搭建
3,增加模板基礎鏡像,供JAVA應用項目使用
4,將定制化的鏡像上傳到倉庫及注意事項
5,Dockfile詳解,結合Java應用程序,將JAVA應用服務鏡像發(fā)布到倉庫
6,企業(yè)常用技能Maven打包創(chuàng)建本地鏡像,推送倉庫及部署;
7,創(chuàng)建Java應用服務容器,完成代碼到容器的發(fā)布及應用自啟
資料獲取方式
加群即可獲取?群號:923116658
點擊加入群聊【Java架構解析】
總結
以上是生活随笔為你收集整理的Docker容器内部署Java微服务的内存限制问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql常见报错解决办法
- 下一篇: 侧滑抽屉菜单 FlowingDrawer