歡迎關注個人博客:http://www.mydlq.clubjava
前言:git
之前的 SpringCloud 微服務時代以 「Jar包" 爲服務的基礎,每一個服務都打成 Jar 供服務間相互關聯與調用。而 如今隨着 Kubernetes 流行,已經變遷到一個鏡像一個服務,依靠 Kubernetes 對鏡像的統一編排進行對服務進行統一管理。在對 Kubernetes 微服務實踐過程當中,接觸最多的確定莫過於 Docker 鏡像。因爲本人使用的編程語言是 Java,因此對 Java SpringBoot 項目接觸比較多,因此比較關心如何更好的經過 Dockerfile 編譯 Docker 的鏡像。github
Kubernetes 微服務簡單說就是一羣鏡像間的排列組合與相互間調的關係,故而如何編譯鏡像會使服務性能更優,更易使用成爲了一個重中之重的事情,也是一個很是值得琢磨的問題。這裏我將對 SpringBoot 項目打包 Docker 鏡像如何寫 Dockerfile 的探究進行簡單敘述。spring
系統環境:docker
這裏將用常規 SpringBoot 編譯 Docker 鏡像的 Dockerfile 寫法,感覺下這種方式編譯的鏡像用起來如何。apache
這裏準備一個通過 Maven 編譯後的普通的 springboot 項目來進行 Docker 鏡像構建,項目內容以下圖所示,能夠看到要用到的就是裏面的應用程序的 Jar 文件,將其存入鏡像內完成鏡像構建任務。編程
構建 Docker 鏡像須要提早準備 Dockerfile 文件,這個 Dockerfile 文件中的內容爲構建 Docker 鏡像執行的指令。下面是一個經常使用的 SpringBoot 構建 Docker 鏡像的 Dockerfile,將它放入 Java 源碼目錄(target 的上級目錄),確保下面設置的 Dockerfile 腳本中設置的路徑和 target 路徑對應。緩存
FROM openjdk:8u212-b04-jre-slim VOLUME /tmp ADD target/*.jar app.jar RUN 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" ]
經過 Docker build 命令構建 Docker 鏡像,觀察編譯的時間。springboot
因爲後續須要將鏡像推送到 Aliyun Docker 倉庫,因此鏡像前綴用了 Aliyun。bash
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 .
構建過程
Sending build context to Docker daemon 148.7MB Step 1/7 : FROM openjdk:8u212-b04-jre-slim 8u212-b04-jre-slim: Pulling from library/openjdk 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete Digest: sha256:a5bcd678408a5fe94d13e486d500983ee6fa594940cbbe137670fbb90030456c Status: Downloaded newer image for openjdk:8u212-b04-jre-slim ---> 7c6b62cf60ee Step 2/7 : VOLUME /tmp ---> Running in 13a67ab65d2b Removing intermediate container 13a67ab65d2b ---> 52011f49ddef Step 3/7 : ADD target/*.jar app.jar ---> 26aa41a404fd Step 4/7 : RUN sh -c 'touch /app.jar' ---> Running in 722e7e44e04d Removing intermediate container 722e7e44e04d ---> 7baedb10ec62 Step 5/7 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in 2681d0c5edac Removing intermediate container 2681d0c5edac ---> 5ef4a794b992 Step 6/7 : ENV APP_OPTS="" ---> Running in 5c8924a2a49d Removing intermediate container 5c8924a2a49d ---> fba87c19053a Step 7/7 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in c4cf97009b3c Removing intermediate container c4cf97009b3c ---> d5f30cdfeb81 Successfully built d5f30cdfeb81 Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m13.778s user 0m0.078s sys 0m0.153s
看到此次編譯在 14s 內完成。
將鏡像推送到 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: 1583 real 0m24.335s user 0m0.052s sys 0m0.059s
看到此次在 25s 內完成。
這裏切換到另外一臺服務器上進行鏡像拉取操做,觀察鏡像拉取時間。
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1
拉取過程
0.0.1: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete 4847672cbfa5: Pull complete b60476972fc4: Pull complete Digest: sha256:dc60d304383b1441941ca4e9abc08db775d7be57ccb7c534c929b34ff064a62f Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m27.528s user 0m0.033s sys 0m0.192s
看到此次拉取總共用時 28s 內完成。
這裏將源碼的 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.7MB Step 1/7 : FROM openjdk:8u212-b04-jre-slim ---> 7c6b62cf60ee Step 2/7 : VOLUME /tmp ---> Using cache ---> 52011f49ddef Step 3/7 : ADD target/*.jar app.jar ---> c67160dd2a23 Step 4/7 : RUN sh -c 'touch /app.jar' ---> Running in 474900d843a2 Removing intermediate container 474900d843a2 ---> 3ce9a8bb2600 Step 5/7 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in f48620b1ad36 Removing intermediate container f48620b1ad36 ---> 0478f8f14e5b Step 6/7 : ENV APP_OPTS="" ---> Running in 98485fb15fc8 Removing intermediate container 98485fb15fc8 ---> 0b567c848027 Step 7/7 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in e32242fc6efe Removing intermediate container e32242fc6efe ---> 7b223b23ebfd Successfully built 7b223b23ebfd Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m3.190s user 0m0.039s sys 0m0.403s
能夠看到在編譯鏡像過程當中,前一、2層用的緩存,因此速度很是快。總編譯過程耗時 4s 內完成。
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 The 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.816s user 0m0.024s sys 0m0.081s
能夠看到只推送了前兩層,其它四次因爲遠程倉庫未變化,因此沒有推送。整個推送過程耗時 21s 內完成。
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 0.0.2: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Already exists 04305660f45e: Already exists bbe7020b5561: Already exists d7e364f0d94a: Pull complete 8d688ada35b1: Pull complete Digest: sha256:7c13c40fa92ec2fdc3a8dfdd3232be1be9c1a1a99bf123743ff2a43907ee03dc Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m23.214s user 0m0.053s sys 0m0.097s
本地以及緩存前四層,只拉取有變化的後兩層。這個過程耗時 24s 內完成。
經過這種方式對 SpringBoot 項目構建 Docker 鏡像來使用,給個人感覺就是隻要源碼中發生一點點變化,那麼 SpringBoot 項目就須要將項目通過 Maven 編譯後再通過 Docker 鏡像構建,每次都會將一個 70M+ 的應用 Jar 文件存入 Docker 中,有時候明明就改了一個字母,可能又得把整個程序 Jar 從新存入 Docker 鏡像中,而後在推送和拉取過程當中,每次都得推一個大的鏡像或者拉取一個大的鏡像來進行傳輸,感受很是不方便。
Docker 爲了節約存儲空間,因此採用了分層存儲概念。共享數據會對鏡像和容器進行分層,不一樣鏡像能夠共享相同數據,而且在鏡像上爲容器分配一個 RW 層來加快容器的啓動順序。
在構建鏡像的過程當中 Docker 將按照 Dockerfile 中指定的順序逐步執行 Dockerfile 中的指令。隨着每條指令的檢查,Docker 將在其緩存中查找可重用的現有鏡像,而不是建立一個新的(重複)鏡像。
Dockerfile 的每一行命令都建立新的一層,包含了這一行命令執行先後文件系統的變化。爲了優化這個過程,Docker 使用了一種緩存機制:只要這一行命令不變,那麼結果和上一次是同樣的,直接使用上一次的結果便可。
爲了充分利用層級緩存,咱們必需要理解 Dockerfile 中的命令行是如何工做的,尤爲是RUN,ADD和COPY這幾個命令。
參考 Docker 文檔瞭解 Docker 鏡像緩存:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
SpringBoot 編譯成鏡像後,底層會是一個系統,如 Ubantu,上一層是依賴的 JDK 層,而後纔是 SpringBoot 層,最下面兩層咱們沒法操做,考慮優化只能是 SpringBoot 層琢磨。
從上面實驗中瞭解到之因此每次編譯、推送、拉取過程當中較爲緩慢,緣由就是龐大的鏡像文件。瞭解到 Docker 緩存概念後就就產生一種想法,若是不常常改變的文件緩存起來,將常改動的文件不進行緩存。因爲 SpringBoot 項目是常常變換的,那麼應該怎麼利用緩存機制來實現呢?若是強行利用緩存那麼每次打的鏡像不都是緩存中的舊的程序內容嗎。
因此就考慮一下應用 Jar 包裏面都包含了什麼文件, Java 的哪些文件是常常變更的,哪些不常常變更,對此,下面將針對 SpringBoot 打的應用 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 包的所有大小。
若是一個 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 包分離開來。
通過查找不少相關資料,發現 SpringBoot 的 Maven 插件在執行 Maven 編譯打 Jar 包時候作了不少事情,若是改變某些插件的打包邏輯,導致打應用 Jar 時候將 lib 文件夾下全部的 Jar 包都拷貝到應用 Jar 外面,只留下編譯好的字節碼文件。
將這幾個 Maven 工具引入
<build> <plugins> <!--設置應用 Main 參數啓動依賴查找的地址指向外部 lib 文件夾--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> </manifest> </archive> </configuration> </plugin> <!--設置 SpringBoot 打包插件不包含任何 Jar 依賴包--> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <includes> <include> <groupId>nothing</groupId> <artifactId>nothing</artifactId> </include> </includes> </configuration> </plugin> <!--設置將 lib 拷貝到應用 Jar 外面--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>prepare-package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </build>
執行 Maven 命令打包 Jar
$ mvn clean install
當 Maven 命令執行完成後,查看 target 目錄以下圖:
而後測試下這個 Jar 文件是否能正常運行
$ java -jar springboot-helloworld-0.0.1.jar
而後看到運行日誌,OK!下面將繼續進行 Dockerfile 改造工做。
項目 Github 地址:https://github.com/my-dlq/blog-example/tree/master/springboot-dockerfile
這裏修改上面的 Dockerfile 文件,須要新增一層指令用於將 lib 目錄裏面的依賴 Jar 複製到鏡像中,其它保持和上面 Dockerfile 一致。
FROM openjdk:8u212-b04-jre-slim VOLUME /tmp COPY target/lib/ ./lib/ ADD target/*.jar app.jar RUN 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 不變,就不會新建立層,而是複用緩存。
在執行編譯、推送、拉取以前,先將服務器上次鏡像相關的全部資源都清除掉,而後再執行。
(1)、編譯
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 . Sending build context to Docker daemon 223.2MB Step 1/8 : FROM openjdk:8u212-b04-jre-slim 8u212-b04-jre-slim: Pulling from library/openjdk 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete Digest: sha256:a5bcd678408a5fe94d13e486d500983ee6fa594940cbbe137670fbb90030456c Status: Downloaded newer image for openjdk:8u212-b04-jre-slim ---> 7c6b62cf60ee Step 2/8 : VOLUME /tmp ---> Running in 529369acab24 Removing intermediate container 529369acab24 ---> ad689d937118 Step 3/8 : COPY target/lib/ ./lib/ ---> 029a64c15853 Step 4/8 : ADD target/*.jar app.jar ---> 6265a83a1b90 Step 5/8 : RUN sh -c 'touch /app.jar' ---> Running in 839032a58e6b Removing intermediate container 839032a58e6b ---> 5d877dc35b2b Step 6/8 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in 4043994c5fed Removing intermediate container 4043994c5fed ---> 7cf32beb571f Step 7/8 : ENV APP_OPTS="" ---> Running in b7dcfa10458a Removing intermediate container b7dcfa10458a ---> b6b332bcf0e6 Step 8/8 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in 539093461b59 Removing intermediate container 539093461b59 ---> d4c095c4ffec Successfully built d4c095c4ffec Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m22.983s user 0m0.051s sys 0m0.540s
(2)、推送
$ 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] c16749205e05: Pushed 7fef1a146748: Pushed a3bae74bbdf2: Pushed 9544e87fb8dc: Pushed feb5d0e1e192: Pushed 8fd22162ddab: Pushed 6270adb5794c: Pushed 0.0.1: digest: sha256:e2f4db740880dbe5338b823112ba9467fedf8b27cd75572611d0d3837c80f157 size: 1789 real 0m30.335s user 0m0.052s sys 0m0.059s
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 0.0.1: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete de6c4f15d75b: Pull complete 7066947b7d89: Pull complete e0742de67c75: Pull complete Digest: sha256:e2f4db740880dbe5338b823112ba9467fedf8b27cd75572611d0d3837c80f157 Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m36.585s user 0m0.024s sys 0m0.092s
(1)、編譯
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 . Sending build context to Docker daemon 223.2MB Step 1/8 : FROM openjdk:8u212-b04-jre-slim ---> 7c6b62cf60ee Step 2/8 : VOLUME /tmp ---> Using cache ---> ad689d937118 Step 3/8 : COPY target/lib/ ./lib/ ---> Using cache ---> 029a64c15853 Step 4/8 : ADD target/*.jar app.jar ---> 563773953844 Step 5/8 : RUN sh -c 'touch /app.jar' ---> Running in 3b9df57802bd Removing intermediate container 3b9df57802bd ---> 706a0d47317f Step 6/8 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ---> Running in defda61452bf Removing intermediate container defda61452bf ---> 742c7c926374 Step 7/8 : ENV APP_OPTS="" ---> Running in f09b81d054dd Removing intermediate container f09b81d054dd ---> 929ed5f8b12a Step 8/8 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] ---> Running in 5dc66a8fc1e6 Removing intermediate container 5dc66a8fc1e6 ---> c4942b10992c Successfully built c4942b10992c Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m2.524s user 0m0.051s sys 0m0.493s
能夠看到,此次在第 3 層直接用的緩存,整個編譯過程才花了 2.5 秒時間
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 The 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: 1789 real 1m0.168s user 0m0.016s sys 0m0.032s
能夠看到在 0.2s 內就完成了鏡像推送
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 0.0.2: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Already exists 04305660f45e: Already exists bbe7020b5561: Already exists de6c4f15d75b: Already exists 1c77cc70cc41: Pull complete aa5b8cbca568: Pull complete Digest: sha256:b46d81b153ec64321caaae7ab28da0e362ed7d720a7f0775ea8d1f7bef310d00 Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m1.947s user 0m0.017s sys 0m0.042s
能夠看到在 2s 內就完成了鏡像拉取
因爲網絡波動和系統變化,因此時間只能當作參考,不過執行編譯、推送、拉取過程的確快了很多,大部分用文件都進行了緩存,只有幾百 KB 的流量交互天然速度比幾十 MB 甚至幾百 MB 速度要快不少。
最後說明一下,這種作法只是提供了一種參考,如今的微服務服務 Docker 鏡像化以來,維護的是整個鏡像而不是一個服務程序,因此關心的是 Docker 鏡像可否能正常運行,怎麼構建鏡像會使構建的鏡像更好用。
在生產環境下因爲版本變化較慢,不會動不動就更新,因此在生產環境下暫時最好仍是循序漸進,應用原來 SpringBoot 鏡像編譯方式以確保安裝(除非已大量實例驗證該構建方法)。
歡迎關注個人博客:http://www.mydlq.club