1. 前言
文末有福利
前兩天有同事發(fā)現(xiàn),通過華為云 ServiceStage 的流水線部署基于模板創(chuàng)建的 CSEJavaSDK demo 服務(wù)時,會在容器啟動過程中報錯。初步排查是由于 JVM 占用的內(nèi)存超出了 docker 內(nèi)存配額的上限,導(dǎo)致容器被 kill 掉。于是我們需要排查一下問題出在哪里,為什么以前沒有這類問題,而現(xiàn)在卻發(fā)生了。
2. 基本定位
要確定 docker 容器內(nèi)存超限問題的直接原因并不難。直接進(jìn)入docker容器,執(zhí)行 top 命令,我們發(fā)現(xiàn)宿主機(jī)是一臺8核16G的機(jī)器,而且 docker 并不會屏蔽這些信息,也就是 JVM 會認(rèn)為自己工作于一臺 16G 內(nèi)存的機(jī)器上。而查看 demo 服務(wù)的 Dockerfile,發(fā)現(xiàn)運行服務(wù)時并沒有對 JVM 的內(nèi)存進(jìn)行任何限制,于是 JVM 會根據(jù)默認(rèn)的設(shè)置來工作 —— 最大堆內(nèi)存為物理內(nèi)存的1/4(這里的描述并不完全準(zhǔn)確,因為 JVM 的默認(rèn)堆內(nèi)存大小限制比例其實是根據(jù)物理內(nèi)存有所變化的,具體內(nèi)容請自行搜索資料),而基于模板創(chuàng)建的 ServiceStage 流水線,在部署應(yīng)用堆棧的時候會把 docker 容器的內(nèi)存配額默認(rèn)設(shè)置為 512M,于是容器就會在啟動的時候內(nèi)存超限了。至于以前沒有碰到過這種問題的原因,只是因為以前沒將這么高規(guī)格的 ECS 服務(wù)器用于流水線部署應(yīng)用堆棧。
在查詢過相關(guān)資料后,我們找到了兩種問題解決方案,一個是直接在 jar 包運行命令里加上 -Xmx 參數(shù)來指定最大堆內(nèi)存,不過這種方式只能將 JVM 堆內(nèi)存限制為一個固定的值;另一個方法是在執(zhí)行 jar 包時加上 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap 參數(shù),讓 JVM 能夠感知到docker容器所設(shè)置的 cgroup限制,相應(yīng)地調(diào)整自身的堆內(nèi)存大小,不過這個特性是 JDK 8u131 以上的版本才具備的。
最終,我們提醒 ServiceStage 流水線的同學(xué)將 CSEJavaSDK demo 的創(chuàng)建模板做了改進(jìn),在 Dockerfile 中將打包的基礎(chǔ)鏡像版本由原先的 java:8u111-jre-alpine 升級為了 openjdk:8u181-jdk-alpine,并且在運行服務(wù) jar 包的命令中加上了 -Xmx256m 參數(shù)。問題至此已經(jīng)解決了。
3. 進(jìn)一步的探究
雖然問題已經(jīng)解決,但是在好奇心的驅(qū)使下,我還是打算自己找個 demo 實際去觸發(fā)一下問題,另外看看從網(wǎng)上搜到的解決方法到底好不好用 : )
3.1 準(zhǔn)備工作
創(chuàng)建云上工程
首先需要在華為云 ServiceStage 創(chuàng)建一個云上工程。
在 ServiceStage -> 應(yīng)用開發(fā) -> 微服務(wù)開發(fā) -> 工程管理 -> 創(chuàng)建云上工程中,選擇“基于模板創(chuàng)建”,語言選擇 Java, 框架選擇 CSE-Java (SpringMVC),部署系統(tǒng)選擇“云容器引擎CCE”,給你的云上工程取一個名字,比如test-memo-consuming,最后選擇存放代碼的倉庫,就可以完成云上工程的創(chuàng)建了。
之后云上工程會根據(jù)你的選項自動地生成腳手架代碼,上傳到你指定的代碼倉庫中,并且為你創(chuàng)建一條流水線,完成代碼編譯、構(gòu)建、打包、歸檔鏡像包的操作,并且使用打好的 docker 鏡像包在 CCE 集群中部署一個應(yīng)用堆棧。
創(chuàng)建云上工程和流水線不是本文的重點,我就不詳細(xì)講操作了 : )。同一個應(yīng)用堆棧的實例可以部署多個,在這里為了實驗方便就按照默認(rèn)值1個來部署。
由于云上工程已經(jīng)改進(jìn)了腳手架代碼的模板,不會再出現(xiàn)內(nèi)存超限的問題,所以我們現(xiàn)在能看到 demo 服務(wù)已經(jīng)正常的跑起來,微服務(wù)實例已經(jīng)注冊到服務(wù)中心了。
登錄到 demo 服務(wù)所部署的容器,使用curl命令可以調(diào)用 demo 服務(wù)的 helloworld 接口,可以看到此時服務(wù)已經(jīng)可以正常工作。
增加實驗代碼
為了能夠觸發(fā)微服務(wù)實例消耗更多的內(nèi)存,我在項目代碼中增加了如下接口,當(dāng)調(diào)用/allocateMemory接口時,微服務(wù)實例會不停申請內(nèi)存,直到 JVM 拋出 OOM 錯誤或者容器內(nèi)存超限被 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";
}
此時用來打鏡像包的基礎(chǔ)鏡像是openjdk:8u181-jdk-alpine,jar 包啟動命令中加上了-Xmx256m參數(shù)。
執(zhí)行流水線,應(yīng)用堆棧部署成功后,調(diào)用/allocateMemory接口觸發(fā)微服務(wù)實例消耗內(nèi)存,直到 JVM 拋出 OOM 錯誤,可以在 ServiceStage -> 應(yīng)用上線 -> 應(yīng)用管理中選擇相應(yīng)的應(yīng)用,點擊進(jìn)入概覽頁面,查看應(yīng)用使用內(nèi)存的情況。
應(yīng)用使用的內(nèi)存從 800M+ 陡然下降的時間點就是我重新打包部署的時間,而之后由于調(diào)用/allocateMemory接口,內(nèi)存占用量上升到了接近 400M,并且在這個水平穩(wěn)定了下來,顯示-Xmx256m參數(shù)發(fā)揮了預(yù)期的作用。
3.2 復(fù)現(xiàn)問題
現(xiàn)在將 demo 工程中的 Dockerfile 修改一下,將基礎(chǔ)鏡像改為 java:8u111-jre-alpine,并且刪除啟動命令中的-Xmx256m參數(shù),將其提交為noLimit_oldBase分支,推送到代碼倉庫中。然后編輯流水線,將 source 階段的任務(wù)所使用的代碼分支改為noLimit_oldBase分支,保存并重新運行流水線,將新的代碼打包部署到應(yīng)用堆棧中。
在微服務(wù)實例列表中查詢到新的微服務(wù)實例的 endpoint IP 后,調(diào)用/allocateMemory接口,觀察內(nèi)存情況,內(nèi)存從接近 400M 突然掉下去一下,然后又上升到約 450M 的時間點就是修改代碼后的微服務(wù)實例部署成功的時間點,之后內(nèi)存占用量突然下跌就是因為調(diào)用/allocateMemory接口導(dǎo)致容器內(nèi)存超限被 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方法被調(diào)用,然后 JVM 還沒來得及拋出 OOM 錯誤,整個容器就被 kill 掉了。
這里也給大家提了一個醒:不要以為自己的服務(wù)容器能啟動起來就萬事大吉了,如果沒有特定的限制,JVM 會在運行時繼續(xù)申請堆內(nèi)存,也有可能造成內(nèi)存用量超過 docker 容器的配額!
3.3 讓 JVM 感知 cgroup 限制
前文提到還有另外一種方法解決 JVM 內(nèi)存超限的問題,這種方法可以讓 JVM 自動感知 docker 容器的 cgroup 限制,從而動態(tài)的調(diào)整堆內(nèi)存大小,感覺挺不錯的。我們也來試一下這種方法,看看效果如何 ; )
回到demo項目代碼的master分支,將 Dockerfile 中啟動命令參數(shù)的-Xmx256m替換為-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,提交為useCGroupMemoryLimitForHeap分支,推送到代碼倉庫里。再次運行流水線進(jìn)行構(gòu)建部署。
等 demo 服務(wù)部署成功后,再次調(diào)用/allocateMemory接口,容器的內(nèi)存占用情況如上圖所示(最右邊的那一部分連續(xù)曲線),內(nèi)存上升到一定程度后,JVM 拋出了 OOM 錯誤,沒有繼續(xù)申請堆內(nèi)存??磥磉@種方式也是有效果的。不過,仔細(xì)觀察容器的內(nèi)存占用情況,可以發(fā)現(xiàn)容器所使用的內(nèi)存僅為不到 300M,而我們對于這個容器的內(nèi)存配額限制為 512M,也就是還有 200M+ 是閑置的,并不會被 JVM 利用。這個利用率,比起上文中直接設(shè)置-Xmx256m的內(nèi)存利用率要低 : ( 。推測是因為 JVM 并不會感知到自己是部署在一個 docker 容器里的,所以它把當(dāng)前的環(huán)境當(dāng)成一個物理內(nèi)存只有 512M 的物理機(jī),按照比例來限制自己的最大堆內(nèi)存,另一部分就被閑置了。
如此看來,如果想要充分利用自己的服務(wù)器資源,還是得多花一點功夫,手動調(diào)整好-Xmx參數(shù)。
這里也為大家準(zhǔn)備了一節(jié)視頻:
視頻詳情
干貨列表
1,Docker安裝過程的各種坑
2,完成Docker倉庫環(huán)境搭建
3,增加模板基礎(chǔ)鏡像,供JAVA應(yīng)用項目使用
4,將定制化的鏡像上傳到倉庫及注意事項
5,Dockfile詳解,結(jié)合Java應(yīng)用程序,將JAVA應(yīng)用服務(wù)鏡像發(fā)布到倉庫
6,企業(yè)常用技能Maven打包創(chuàng)建本地鏡像,推送倉庫及部署;
7,創(chuàng)建Java應(yīng)用服務(wù)容器,完成代碼到容器的發(fā)布及應(yīng)用自啟
8,演示測試
什么是Docker?
docker 是一個開源的應(yīng)用容器引擎,讓開發(fā)者可以打包他們的應(yīng)用以及依賴包到一個可移植的容器中,然后發(fā)布到任何流行的Linux服務(wù)器上,可以實現(xiàn)虛擬化,容器是完全使用沙箱(沙盒)機(jī)制,相互之間不會有任何接口。
那些在玩docker的大佬
京東:http://www.infoq.com/cn/news/2015/06/jd-618-docker
騰訊內(nèi)部: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)內(nèi)核技術(shù),對現(xiàn)有基礎(chǔ)設(shè)施的侵入較少,所有實現(xiàn)在內(nèi)核中完成,所以性能幾乎與原生一致,依賴簡單,與進(jìn)程無本質(zhì)區(qū)別
正確理解Docker內(nèi)部流程
Docker線上環(huán)境操作流程
Docker實戰(zhàn)
1,Docker安裝過程的各種坑
2,完成Docker倉庫環(huán)境搭建
3,增加模板基礎(chǔ)鏡像,供JAVA應(yīng)用項目使用
4,將定制化的鏡像上傳到倉庫及注意事項
5,Dockfile詳解,結(jié)合Java應(yīng)用程序,將JAVA應(yīng)用服務(wù)鏡像發(fā)布到倉庫
6,企業(yè)常用技能Maven打包創(chuàng)建本地鏡像,推送倉庫及部署;
7,創(chuàng)建Java應(yīng)用服務(wù)容器,完成代碼到容器的發(fā)布及應(yīng)用自啟
資料獲取方式
加群即可獲取?群號:923116658