http://www.mydlq.club/article/16/
前言:PS:已經在生產實踐中驗證,解決在生產環境下,網速頻寬小,每次推拉映象影響線上服務問題,按本文方式構建映象,除了第一次拉取、推送、構建映象慢,第二、三…次都是幾百K大小傳輸,速度非常快,構建、打包、推送幾秒內完成。
以前的 SpringCloud 微服務時代以 “Jar包” 為服務的基礎,每個服務都打成 Jar 供服務間相互關聯與呼叫。而 現在隨著 Kubernetes 流行,已經變遷到一個映象一個服務,依靠 Kubernetes 對映象的統一編排進行對服務進行統一管理。在對 Kubernetes 微服務實踐過程中,接觸最多的肯定莫過於 Docker 映象。由於本人使用的程式語言是 Java,所以對 Java SpringBoot 專案接觸比較多,所以比較關心如何更好的通過 Dockerfile 編譯 Docker 的映象。
Kubernetes 微服務簡單說就是一群映象間的排列組合與相互間調的關係,故而如何編譯映象會使服務效能更優,使映象構建、推送、拉取速度更快,使其佔用網路資源更少這裡優化,更易使用成為了一個重中之重的事情,也是一個非常值得琢磨的問題。
這裡我將對 SpringBoot 專案打包 Docker 映象如何寫 Dockerfile 的探究進行簡單敘述。
系統環境:一、探究常規 Springboot 如何編譯 Docker 映象Docker 版本:18.09.3
Open JDK 基礎映象版本:openjdk:8u212-b04-jre-slim
測試用的映象倉庫:阿里雲 Docker Hub
專案 Github:https://github.com/my-dlq/blog-example/tree/master/springboot/springboot-dockerfile
這裡將用常規 SpringBoot 編譯 Docker 映象的 Dockerfile 寫法,感受下這種方式編譯的映象用起來如何。
1、準備編譯映象的 SpringBoot 專案這裡準備一個經過 Maven 編譯後的普通的 springboot 專案來進行 Docker 映象構建,專案內容如下圖所示,可以看到要用到的就是裡面的應用程式的 Jar 檔案,將其存入映象內完成映象構建任務。
jar 檔案大小:70.86mb
2、準備 Dockerfile 檔案構建 Docker 映象需要提前準備 Dockerfile 檔案,這個 Dockerfile 檔案中的內容為構建 Docker 映象執行的指令。
下面是一個常用的 SpringBoot 構建 Docker 映象的 Dockerfile,將它放入 Java 原始碼目錄(target 的上級目錄),確保下面設定的 Dockerfile 指令碼中設定的路徑和 target 路徑對應。
FROM openjdk:8u212-b04-jre-slimVOLUME /tmpADD target/*.jar app.jarRUN sh -c 'touch /app.jar'ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai"ENV APP_OPTS=""ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ]
3、構建 Docker 映象
通過 Docker build 命令構建 Docker 映象,觀察編譯的時間。
由於後續需要將映象推送到 Aliyun Docker 倉庫,所以映象字首用了 Aliyun。
time:此引數會顯示執行過程經過的時間
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 .
構建過程
Sending build context to Docker daemon 148.7MBStep 1/7 : FROM openjdk:8u212-b04-jre-slim8u212-b04-jre-slim: Pulling from library/openjdk743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete Digest: sha256:a5bcd678408a5fe94d13e486d500983ee6fa594940cbbe137670fbb90030456cStatus: Downloaded newer image for openjdk:8u212-b04-jre-slim ---> 7c6b62cf60eeStep 2/7 : VOLUME /tmp ---> Running in 13a67ab65d2bRemoving intermediate container 13a67ab65d2b ---> 52011f49ddefStep 3/7 : ADD target/*.jar app.jar ---> 26aa41a404fdStep 4/7 : RUN sh -c 'touch /app.jar' ---> Running in 722e7e44e04dRemoving intermediate container 722e7e44e04d ---> 7baedb10ec62Step 5/7 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in 2681d0c5edacRemoving intermediate container 2681d0c5edac ---> 5ef4a794b992Step 6/7 : ENV APP_OPTS="" ---> Running in 5c8924a2a49dRemoving intermediate container 5c8924a2a49d ---> fba87c19053aStep 7/7 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in c4cf97009b3cRemoving intermediate container c4cf97009b3c ---> d5f30cdfeb81Successfully built d5f30cdfeb81Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1real 0m13.778suser 0m0.078ssys 0m0.153s
看到這次編譯在 14s 內完成。
4、將映象推送到映象倉庫將映象推送到 Aliyun 倉庫,然後檢視並記錄推送時間
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1
執行過程
The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot]cc1a2376d7c0: Pushed 2b940d07e9e7: Pushed 9544e87fb8dc: Pushed feb5d0e1e192: Pushed 8fd22162ddab: Pushed 6270adb5794c: Pushed 0.0.1: digest: sha256:dc60d304383b1441941ca4e9abc08db775d7be57ccb7c534c929b34ff064a62f size: 1583real 0m24.335suser 0m0.052ssys 0m0.059s
看到這次在 25s 內完成。擴充套件:面試官:你簡歷中寫用過docker,能說說容器和映象的區別嗎?
5、拉取映象這裡切換到另一臺伺服器上進行映象拉取操作,觀察映象拉取時間。
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1
拉取過程
0.0.1: Pulling from mydlq/springboot743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete 4847672cbfa5: Pull complete b60476972fc4: Pull complete Digest: sha256:dc60d304383b1441941ca4e9abc08db775d7be57ccb7c534c929b34ff064a62fStatus: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1real 0m27.528suser 0m0.033ssys 0m0.192s
看到這次拉取總共用時 28s 內完成。
6、修改 Java 原始碼重新打包 Jar 後再次嘗試這裡將原始碼的 JAVA 檔案內容修改,然後重新打 Jar 包,這樣再次嘗試編譯、推送、拉取過程,由於 Docker 在執行構建時會採用分層快取,所以這是一個執行較快過程。
(1)、編譯
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 .Sending build context to Docker daemon 148.7MBStep 1/7 : FROM openjdk:8u212-b04-jre-slim ---> 7c6b62cf60eeStep 2/7 : VOLUME /tmp ---> Using cache ---> 52011f49ddefStep 3/7 : ADD target/*.jar app.jar ---> c67160dd2a23Step 4/7 : RUN sh -c 'touch /app.jar' ---> Running in 474900d843a2Removing intermediate container 474900d843a2 ---> 3ce9a8bb2600Step 5/7 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in f48620b1ad36Removing intermediate container f48620b1ad36 ---> 0478f8f14e5bStep 6/7 : ENV APP_OPTS="" ---> Running in 98485fb15fc8Removing intermediate container 98485fb15fc8 ---> 0b567c848027Step 7/7 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in e32242fc6efeRemoving intermediate container e32242fc6efe ---> 7b223b23ebfdSuccessfully built 7b223b23ebfdSuccessfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2real 0m3.190suser 0m0.039ssys 0m0.403s
可以看到在編譯映象過程中,前1、2層用的快取,所以速度非常快。總編譯過程耗時 4s 內完成。
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot]d66a2fec30b5: Pushed f4da2c7581aa: Pushed 9544e87fb8dc: Layer already exists feb5d0e1e192: Layer already exists 8fd22162ddab: Layer already exists 6270adb5794c: Layer already exists real 0m20.816suser 0m0.024ssys 0m0.081s
可以看到只推送了前兩層,其它四次由於遠端倉庫未變化,所以沒有推送。整個推送過程耗時 21s 內完成。
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.20.0.2: Pulling from mydlq/springboot743f2d6c1f65: Already exists b83e581826a6: Already exists 04305660f45e: Already exists bbe7020b5561: Already exists d7e364f0d94a: Pull complete 8d688ada35b1: Pull complete Digest: sha256:7c13c40fa92ec2fdc3a8dfdd3232be1be9c1a1a99bf123743ff2a43907ee03dcStatus: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2real 0m23.214suser 0m0.053ssys 0m0.097s
本地以及快取前四層,只拉取有變化的後兩層。這個過程耗時 24s 內完成。
7、使用映象過程中的感受通過這種方式對 SpringBoot 專案構建 Docker 映象來使用,給我的感受就是隻要原始碼中發生一點點變化,那麼 SpringBoot 專案就需要將專案經過 Maven 編譯後再經過 Docker 映象構建,每次都會將一個 70M+ 的應用 Jar 檔案存入 Docker 中,有時候明明就改了一個字母,可能又得把整個程式 Jar 重新存入 Docker 映象中,然後在推送和拉取過程中,每次都得推一個大的映象或者拉取一個大的映象來進行傳輸,感覺非常不方便。
二、了解 Docker 分層及快取機制1、Docker 分層快取簡介Docker 為了節約儲存空間,所以採用了分層儲存概念。共享資料會對映象和容器進行分層,不同映象可以共享相同資料,並且在映象上為容器分配一個 RW 層來加快容器的啟動順序。
在構建映象的過程中 Docker 將按照 Dockerfile 中指定的順序逐步執行 Dockerfile 中的指令。隨著每條指令的檢查,Docker 將在其快取中查詢可重用的現有映象,而不是建立一個新的(重複)映象。
Dockerfile 的每一行命令都建立新的一層,包含了這一行命令執行前後文件系統的變化。為了優化這個過程,Docker 使用了一種快取機制:只要這一行命令不變,那麼結果和上一次是一樣的,直接使用上一次的結果即可。擴充套件:終於有人把 Docker 講清楚了,萬字詳解!
為了充分利用層級快取,我們必須要理解 Dockerfile 中的命令列是如何工作的,尤其是RUN,ADD和COPY這幾個命令。
2、SpringBoot Docker 映象的分層參考 Docker 文件了解 Docker 映象快取:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
SpringBoot 編譯成映象後,底層會是一個系統,如 Ubantu,上一層是依賴的 JDK 層,然後才是 SpringBoot 層,最下面兩層我們無法操作,考慮優化只能是 SpringBoot 層琢磨。
三、是什麼導致 Jar 包臃腫從上面實驗中了解到之所以每次編譯、推送、拉取過程中較為緩慢,原因就是龐大的映象檔案。了解到 Docker 快取概念後就就產生一種想法,如果不經常改變的檔案快取起來,將常改動的檔案不進行快取。擴充套件:SpringBoot快取應用實踐
由於 SpringBoot 專案是經常變換的,那麼應該怎麼利用快取機制來實現呢?如果強行利用快取那麼每次打的映象不都是快取中的舊的程式內容嗎。
所以就考慮一下應用 Jar 包裡面都包含了什麼檔案, Java 的哪些檔案是經常變動的,哪些不經常變動,對此,下面將針對 SpringBoot 打的應用 Jar 包進行分析。
1、解壓 Jar 包檢視內容顯示解壓後的列表,檢視各個資料夾大小
$ tree -L 3 --si --du.├── [ 74M] BOOT-INF │ ├── [2.1k] classes│ └── [ 74M] lib├── [ 649] META-INF│ ├── [ 552] MANIFEST.MF│ └── [ 59] maven└── [ 67] org └── [ 38] springframework
可以看到最大的檔案就是 lib 這個資料夾,開啟這個資料夾,裡面是一堆相關依賴 Jar,這其中一個 Jar 不大,但是一堆 Jar 組合起來就非常大了,一般 SpringBoot 的專案依賴 Jar 大小維持在 40MB ~ 160MB。
在看看 org 資料夾,裡面程式碼加起來才幾百 KB。故此 SpringBoot 程式 Jar 包就是這些 Classes 檔案和依賴的 Jar 組成,這些依賴 Jar 總共 74 MB,幾乎佔了這個應用 Jar 包的全部大小。
2、解決臃腫的新思路如果一個 Jar 包只包含 class 檔案,那麼這個 Jar 包的大小可能就幾百 KB。現在要探究一下,如果將 lib 依賴的 Jar 和 class 分離,設定應用的 Jar 包只包含 class 檔案,將 lib 資料夾下的 Jar 檔案放在 SpringBoot Jar 的外面。
當我們寫一個程式的時候,常常所依賴的 Jar 不會經常變動,變動多的是原始碼程式,依賴的 Jar 包非常大而原始碼非常小。
仔細思考一下,如果在打包成 Docker 映象的時候將應用依賴的 Jar 包單獨設定一層快取,而應用 Jar 包只包含 Class 檔案,這樣在 Docker 執行編譯、推送、拉取過程中,除了第一次是全部都要執行外,再往後的執行編譯、推送、拉取過程中,只會操作改動的那個只包含 Class 的 Jar 檔案,就幾百 KB,可以說是能夠瞬間完成這個過程。所以思考一下,如何將 lib 資料夾下的依賴 Jar 包和應用 Jar 包分離開來。
3、如何解決 lib 和 class 檔案分離
經過查詢很多相關資料,發現 SpringBoot 的 Maven 外掛在執行 Maven 編譯打 Jar 包時候做了很多事情,如果改變某些外掛的打包邏輯,致使打應用 Jar 時候將 lib 資料夾下所有的 Jar 包都拷貝到應用 Jar 外面,只留下編譯好的位元組碼檔案。
將這幾個 Maven 工具引入到專案 pom.xml 中
$ mvn clean install
當 Maven 命令執行完成後,檢視 target 目錄如下圖:
然後測試下這個 Jar 檔案是否能正常執行
$ java -jar springboot-helloworld-0.0.1.jar
然後看到執行日誌,OK!下面將繼續進行 Dockerfile 改造工作。
1、修改 Dockerfile 檔案專案 Github 地址:https://github.com/my-dlq/blog-example/tree/master/springboot-dockerfile
這裡修改上面的 Dockerfile 檔案,需要新增一層指令用於將 lib 目錄裡面的依賴 Jar 複製到映象中,其它保持和上面 Dockerfile 一致。
FROM openjdk:8u212-b04-jre-slimVOLUME /tmpCOPY target/lib/ ./lib/ADD target/*.jar app.jarRUN sh -c 'touch /app.jar'ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai"ENV APP_OPTS=""ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ]
這裡新增了一層指令,作用為將 lib 資料夾複製到映象之中,由於 Docker 快取機制原因,這層一定要在複製應用 Jar 之前,這樣改造後每次只要 lib/ 資料夾裡面的依賴 Jar 不變,就不會新建立層,而是複用快取。
2、改造 Docker 映象後的首次編譯、推送、拉取在執行編譯、推送、拉取之前,先將伺服器上次映象相關的所有資源都清除掉,然後再執行。
(1)、編譯
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 .Sending build context to Docker daemon 223.2MBStep 1/8 : FROM openjdk:8u212-b04-jre-slim8u212-b04-jre-slim: Pulling from library/openjdk743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete Digest: sha256:a5bcd678408a5fe94d13e486d500983ee6fa594940cbbe137670fbb90030456cStatus: Downloaded newer image for openjdk:8u212-b04-jre-slim ---> 7c6b62cf60eeStep 2/8 : VOLUME /tmp ---> Running in 529369acab24Removing intermediate container 529369acab24 ---> ad689d937118Step 3/8 : COPY target/lib/ ./lib/ ---> 029a64c15853Step 4/8 : ADD target/*.jar app.jar ---> 6265a83a1b90Step 5/8 : RUN sh -c 'touch /app.jar' ---> Running in 839032a58e6bRemoving intermediate container 839032a58e6b ---> 5d877dc35b2bStep 6/8 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in 4043994c5fedRemoving intermediate container 4043994c5fed ---> 7cf32beb571fStep 7/8 : ENV APP_OPTS="" ---> Running in b7dcfa10458aRemoving intermediate container b7dcfa10458a ---> b6b332bcf0e6Step 8/8 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in 539093461b59Removing intermediate container 539093461b59 ---> d4c095c4ffecSuccessfully built d4c095c4ffecSuccessfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1real 0m22.983suser 0m0.051ssys 0m0.540s
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot]c16749205e05: Pushed 7fef1a146748: Pushed a3bae74bbdf2: Pushed 9544e87fb8dc: Pushedfeb5d0e1e192: Pushed8fd22162ddab: Pushed6270adb5794c: Pushed 0.0.1: digest: sha256:e2f4db740880dbe5338b823112ba9467fedf8b27cd75572611d0d3837c80f157 size: 1789real 0m30.335suser 0m0.052ssys 0m0.059s
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.10.0.1: Pulling from mydlq/springboot743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete de6c4f15d75b: Pull complete 7066947b7d89: Pull complete e0742de67c75: Pull complete Digest: sha256:e2f4db740880dbe5338b823112ba9467fedf8b27cd75572611d0d3837c80f157Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1real 0m36.585suser 0m0.024ssys 0m0.092s
3、再次編譯、推送、拉取(1)、編譯
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 .Sending build context to Docker daemon 223.2MBStep 1/8 : FROM openjdk:8u212-b04-jre-slim ---> 7c6b62cf60eeStep 2/8 : VOLUME /tmp ---> Using cache ---> ad689d937118Step 3/8 : COPY target/lib/ ./lib/ ---> Using cache ---> 029a64c15853Step 4/8 : ADD target/*.jar app.jar ---> 563773953844Step 5/8 : RUN sh -c 'touch /app.jar' ---> Running in 3b9df57802bdRemoving intermediate container 3b9df57802bd ---> 706a0d47317fStep 6/8 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in defda61452bfRemoving intermediate container defda61452bf ---> 742c7c926374Step 7/8 : ENV APP_OPTS="" ---> Running in f09b81d054ddRemoving intermediate container f09b81d054dd ---> 929ed5f8b12aStep 8/8 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in 5dc66a8fc1e6Removing intermediate container 5dc66a8fc1e6 ---> c4942b10992cSuccessfully built c4942b10992cSuccessfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2real 0m2.524suser 0m0.051ssys 0m0.493s
可以看到,這次在第 3 層直接用的快取,整個編譯過程才花了 2.5 秒時間
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot]d719b9540809: Pushed d45bf4c5fb92: Pushed a3bae74bbdf2: Layer already exists 9544e87fb8dc: Layer already exists feb5d0e1e192: Layer already exists 8fd22162ddab: Layer already exists 6270adb5794c: Layer already exists 0.0.2: digest: sha256:b46d81b153ec64321caaae7ab28da0e362ed7d720a7f0775ea8d1f7bef310d00 size: 1789real 0m0.168suser 0m0.016ssys 0m0.032s
可以看到在 0.2s 內就完成了映象推送
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.20.0.2: Pulling from mydlq/springboot743f2d6c1f65: Already exists b83e581826a6: Already exists 04305660f45e: Already exists bbe7020b5561: Already exists de6c4f15d75b: Already exists 1c77cc70cc41: Pull complete aa5b8cbca568: Pull complete Digest: sha256:b46d81b153ec64321caaae7ab28da0e362ed7d720a7f0775ea8d1f7bef310d00Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2real 0m1.947suser 0m0.017ssys 0m0.042s
可以看到在 2s 內就完成了映象拉取
五、最後總結由於網路波動和系統變化,所以時間只能當做參考,不過執行編譯、推送、拉取過程的確快了不少,大部分用檔案都進行了快取,只有幾百 KB 的流量互動自然速度比幾十 MB 甚至幾百 MB 速度要快很多。
最後說明一下,這種做法只是提供了一種參考,現在的微服務服務 Docker 映象化以來,維護的是整個映象而不是一個服務程式,所以關心的是 Docker 映象能否能正常執行,怎麼構建映象會使構建的映象更好用。
在生產環境下由於版本變化較慢,不會動不動就更新,所以在生產環境下暫時最好還是按部就班,應用原來 SpringBoot 映象編譯方式以確保安裝(除非已大量例項驗證該構建方法)。
END