SpringBoot 2.3.x 分層構建 Docker 映象實踐
目錄[-]
- . 一、什麼是映象分層
- . 二、SpringBoot 2.3.x 新增對分層的支援
- . 三、建立測試的 SpringBoot 應用
- . 1、Maven 中引入相關依賴和外掛
- . 2、建立測試的 Controller 類
- . 3、建立 SpringBoot 啟動類
- . 四、建立兩種構建映象的 Dockerfile 指令碼
- . 1、普通映象構建指令碼檔案 dockerfile-number
- . 2、分層映象構建指令碼檔案 dockerfile-layer
- . 五、使用兩種 Dockerfile 構建專案映象
- . 1、在伺服器一構建普通 Docker 映象
- . 2、在伺服器二構建分層 Docker 映象
- . 六、映象推送到映象倉庫測試
- . 1、推送映象到映象倉庫測試
- . 2、映象倉庫拉取映象測試
- . 七、映象構建、推送、拉取時間彙總
- . 1、不使用分層構建映象
- . 2、使用分層構建映象
- . 3、總結
系統環境:
- Docker 版本: 19.03.13
- Open JDK 基礎映象版本: openjdk:8u275
- 私有的 Harbor 映象倉庫: 自建 Harbor 私庫
- 專案 Github:SpringBoot 2.3.x 分層構建 Docker 映象示例
參考地址:
一、什麼是映象分層
映象的構成
現在一談起映象大部分都是指 Docker 引擎構建的映象,一般 Docker 映象是由很多層組成,底層是作業系統,然後在其之上是基礎映象或者使用者自定義 Dockerfile 指令碼中定義的中間層。其中映象在構建完成後,使用者只能對映象進行讀操作,而不能進行寫操作,只有映象啟動後變為容器,才能進行讀寫操作。映象整體結構,可以觀看下圖:
該圖中展示了映象的基本組成,但是圖中這一個箇中間層是什麼呢?要想了解這些層具體是什麼,那得知道如何構建 Docker 映象了。平時我們構建 Docker 映象時候,都是編寫 Dockerfile 指令碼,然後使用 Docker 映象構建命令,按照指令碼一行行執行構建。
如下就是一個 Dockerfile 指令碼,指令碼內容就構建 Java 專案映象常用的 Dockerfile 命令:
FROM openjdk:8u275 VOLUME /tmp ADD target/*.jar app.jar ENV TZ="Asia/Shanghai" ENV JAVA_OPTS="" ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0" ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar"]
有了 Dockerfile 指令碼,我們需要執行 Docker 的構建映象命令對執行 Dockerfile 指令碼構建映象,其中構建映象的過程如下:
## 構建映象的命令
$ docker build -t java-test:latest .
## 命令執行的過程
Step 1/7 : FROM openjdk:8u275
---> 82f24ce79de6
Step 2/7 : VOLUME /tmp
---> Running in a6361fdfc193
Removing intermediate container a6361fdfc193
---> a43948bf1b98
Step 3/7 : ADD target/*.jar app.jar
---> 18f4bc60818f
Step 4/7 : ENV TZ="Asia/Shanghai"
---> Running in cc738aa5865b
Removing intermediate container cc738aa5865b
---> 538adb85609e
Step 5/7 : ENV JAVA_OPTS=""
---> Running in f8b635d32b2b
Removing intermediate container f8b635d32b2b
---> 34e7a8cd7b6e
Step 6/7 : ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0"
---> Running in 9331cb6e443e
Removing intermediate container 9331cb6e443e
---> 232b9c6c1d29
Step 7/7 : ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar" ]
---> Running in c3a24fba3a10
Removing intermediate container c3a24fba3a10
---> a41974d5f0e3
可以看到總共存在 7 個構建步驟,每步都與 Dockerfile 裡面一行指令對應。樣子和下圖相似:
如果這時候,我們改變原來 Dockerfile 內容,建立一個新的映象,其 Dockerfile 如下:
FROM openjdk:8u275
VOLUME /tmp
ADD target/*.jar app.jar
ENV TZ="Asia/Macao" #與原來 Dockerfile 不同
ENV JVM_OPTS="-Xmx512m -Xss256k" #與原來 Dockerfile 不同
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar" ]
執行 Docker 命令構建映象:
$ docker build -t java-test2:latest .
Step 1/6 : FROM openjdk:8u275
---> 82f24ce79de6
Step 2/6 : VOLUME /tmp
---> Using cache
---> a43948bf1b98
Step 3/6 : ADD target/*.jar app.jar
---> Using cache
---> 18f4bc60818f
Step 4/6 : ENV TZ="Asia/Macao"
---> Running in fd98b90a5485
Removing intermediate container fd98b90a5485
---> afab3fcdab07
Step 5/6 : ENV JVM_OPTS="-Xmx512m -Xss256k"
---> Running in 19a99576fba9
Removing intermediate container 19a99576fba9
---> 4eeab7d7c720
Step 6/6 : ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar" ]
---> Running in 2dba72e1eef4
Removing intermediate container 2dba72e1eef4
---> 7c706ecf7698
可以觀察到執行過程中,從一開始執行的構建步驟中顯示,並沒有生成新的中間層映象,而是直接使用了已經存在的快取映象。直至4⁄6這部中,由於新的 Dockerfile 與原來 Dockerfile 發生變動,所以這部中間層映象直接是新建立的,並沒有使用快取中間層映象。
然後往下觀察,發現之後的全部構建都是新建立的中間層映象,即是指令碼最後的一行和原來相同,也沒有使用快取中間層映象。
上面現象說明,Docker 映象在構建過程中按照 Dockerfile 自上往下的執行順序中,如果從最上層開始,其指令碼內容和已有的快取中間層映象內容一致,就會引入快取中的中間層映象(並不是直接複製快取映象,而是引入映象檔案地址,多個映象共享這些中間層映象)。但是,如果執行過程中中間任意一行映象構建的內容發生變化,那麼當前行和之後的全部行在執行時就不會使用快取中的中間層映象,而是全部建立新的映象。
這就是 Docker 映象中快取中間層映象的複用,學會使用快取構建映象將大大減少儲存空間的佔用以及映象的構建的構建速度,映象的快取不僅僅體現在映象的構建上,在執行”映象推送”、”映象拉取”操作時都能觀察到其的好處。
- 映象緩在映象推送的體現:如映象推送時候,也是將映象整體構成的中間層映象並行推送到映象倉庫,如果映象倉庫中已經存某個中間層映象,那麼推送過程就不會再次將該層映象推送到映象倉庫,而是將倉庫中並不存在中間層映象推送到其中。
- 映象快取在映象拉取的體現:在拉取映象時候,如果本地某個大映象的中間層映象的組成中,已經包含新拉取映象的中間層部分映象,那麼將直接複用本地已經映象的中間層映象,不必再將其進行拉取,而本地不存在的中間層映象將會被繼續拉取。
說了這麼多,相信大家已經對映象快取的使用有了初步瞭解,那麼再談及為什麼需要映象分層就很好解釋,其原因就是 Docker 想提高資源的複用率,將一個大映象拆分成很多層小映象組成,以達到映象中間層的複用的目的。
二、SpringBoot 2.3.x 新增對分層的支援
SpringBoot 2.3.x 以後支援分層打包應用,需要 Pom.xml 中引入 SpringBoot 2.3.x 後的父依賴和使用 SpringBoot 打包外掛spring-boot-maven-plugin
,並且開啟layers
功能,然後執行 Maven 編譯原始碼構建 Jar 包,使用該 Jar 包就可以構建基於分層模式的 Docker 映象:
專案 pom.xml 中引入 SpringBoot 2.3.x 依賴:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
<relativePath/>
</parent>
專案 pom.xml 中引入 spring-boot-maven-plugin 打包外掛,並且開啟分層功能:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--開啟分層編譯支援-->
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
執行 Maven 命令,構建分層的 JAR 包,命令和平時的 Maven 構建命令相同:
$ mvn install
觀察 Jar 結構,可以看到裡面多了classpath.idx
與layers.idx
兩個檔案:
- classpath.idx:檔案列出了依賴的 jar 包列表,到時候會按照這個順序載入。
- layers.idx:檔案清單,記錄了所有要被複制到 Dokcer 映象中的檔案資訊。
根據官方介紹,在構建 Docker 映象前需要從 Jar 中提起出對應的分層檔案到 Jar 外面,可用使用下面命令列出可以從分層 Jar 中提取出的資料夾資訊:
$ java -Djarmode=layertools -jar target/springboot-layer-0.0.1.jar list
可用該看到以下輸出,下面的內容就是接下來使用分層構建後,生成的 Jar 提取出對應資源後的結構:
dependencies
spring-boot-loader
snapshot-dependencies
application
上面即是使用分層工具提取 Jar 的內容後生成的資料夾,其中各個資料夾作用是:
- dependencies:儲存專案正常依賴 Jar 的資料夾。
- snapshot-dependencies:儲存專案快照依賴 Jar 的資料夾。
- resources:用於儲存靜態資源的資料夾。
- application:用於儲存應用程式類相關檔案的資料夾。
三、建立測試的 SpringBoot 應用
建立測試的 SpringBoot 專案,並且在 pom.xml 中開啟映象分層。
1、Maven 中引入相關依賴和外掛
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
</parent>
<artifactId>springboot-dockerfile-layer</artifactId>
<packaging>jar</packaging>
<name>springboot-dockerfile-layer</name>
<description>springboot build layer example</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、建立測試的 Controller 類
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/hello")
public String hello() {
return "hello world!";
}
}
3、建立 SpringBoot 啟動類
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
四、建立兩種構建映象的 Dockerfile 指令碼
為了方便體現出 SpringBoot 2.3.x 支援的分層構建 Dockerfile 的優點,這裡在 Java 原始碼資料夾下,建立普通與分層兩種構建映象的 Dockerfile 指令碼,後續會使用這兩種指令碼構建 Docker 映象進行構建速度、推送速度、拉取速度的對比。
1、普通映象構建指令碼檔案 dockerfile-number
FROM openjdk:8u275
VOLUME /tmp
ADD target/*.jar app.jar
RUN sh -c 'touch /app.jar'
ENV TZ="Asia/Shanghai"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0"
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ]
說明:
- TZ:時區設定,而 Asia/Shanghai 表示使用中國上海時區。
- JVM_OPTS:指定 JVM 啟動時候的引數,-XX:MaxRAMPercentage 引數和 -Xmx 類似,都是限制堆記憶體大小,只不過 -Xmx 需要手動指定限制大小,而 -XX:MaxRAMPercentage 則是根據虛擬機器可用記憶體百分比限制。
- JAVA_OPTS:在映象啟動時指定的自定義 Java 引數,例如 -Dspring.application.name=xxx。
2、分層映象構建指令碼檔案 dockerfile-layer
FROM openjdk:8u275 as builder
WORKDIR application
COPY target/*.jar application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM openjdk:8u275
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/application/ ./
ENV TZ="Asia/Shanghai"
ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0"
ENV JAVA_OPTS=""
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]
說明:
- TZ:時區設定,而 Asia/Shanghai 表示使用中國上海時區。
- -Djarmode=layertools:指定構建 Jar 的模式。
- extract:從 Jar 包中提取構建映象所需的內容。
- -from=builder多級映象構建中,從上一級映象複製檔案到當前映象中。
五、使用兩種 Dockerfile 構建專案映象
1、在伺服器一構建普通 Docker 映象
(1)、第一次構建
## 執行 Maven 命令,將原始碼構建 Jar 包
$ mvn clean install
## 構建 SpringBoot 應用的 Docker 映象
$ time docker build -t hub.mydlq.club/library/springboot-normal:0.0.1 .
Docker 映象構建總共花費24.98s時間。
(2)、第二次構建(修改依賴 pom.xml 檔案)
## 修改 pom.xml 裡面的依賴,隨意新增一個新的依賴包,然後再次將原始碼構建 Jar 包
$ mvn clean install
## 構建 SpringBoot 應用的 Docker 映象
$ time docker build -t hub.mydlq.club/library/springboot-normal:0.0.2 .
Docker 映象構建總共花費1.27s時間。
(3)、第三次構建(修改程式碼內容)
## 修改原始碼任意內容後,然後再次將原始碼構建 Jar 包
$ mvn clean install
## 構建 SpringBoot 應用的 Docker 映象
$ time docker build -t hub.mydlq.club/library/springboot-normal:0.0.3 .
Docker 映象構建總共花費1.32s時間。
2、在伺服器二構建分層 Docker 映象
(1)、第一次構建
## 執行 Maven 命令,將原始碼構建 Jar 包
$ mvn clean install
## 構建 SpringBoot 應用的 Docker 映象
$ time docker build -t hub.mydlq.club/library/springboot-layer:0.0.1 .
Docker 映象構建總共花費26.12s時間。
(2)、第二次構建(修改依賴 pom.xml 檔案)
## 修改 pom.xml 裡面的依賴,隨意新增一個新的依賴包,然後再次將原始碼構建 Jar 包
$ mvn clean install
## 構建 SpringBoot 應用的 Docker 映象
$ time docker build -t hub.mydlq.club/library/springboot-layer:0.0.2 .
Docker 映象構建總共花費3.44s時間。
(3)、第三次構建(修改程式碼內容)
## 修改原始碼任意內容後,然後再次將原始碼構建 Jar 包
$ mvn clean install
## 構建 SpringBoot 應用的 Docker 映象
$ time docker build -t hub.mydlq.club/library/springboot-layer:0.0.3 .
Docker 映象構建總共花費2.82s時間。
六、映象推送到映象倉庫測試
1、推送映象到映象倉庫測試
伺服器一推送普通映象到映象倉庫1:
## 第一次推送映象
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.1
real 0m35.215s
## 第二次推送映象
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.2
real 0m14.051s
## 第三次推送映象
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.3
real 0m14.183s
伺服器二推送分層映象到映象倉庫2:
## 第一次推送映象
$ time docker push hub.mydlq.club/library/springboot-layer:0.0.1
real 0m34.121s
## 第二次推送映象
$ time docker push hub.mydlq.club/library/springboot-layer:0.0.2
real 0m13.605s
## 第三次推送映象
$ time docker push hub.mydlq.club/library/springboot-layer:0.0.3
real 0m4.805s
2、映象倉庫拉取映象測試
伺服器一推送從映象倉庫1拉取映象:
## 清理全部映象
$ docker rm --force $(docker images -qa)
## 拉取映象 springboot-normal:0.0.1
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.1
real 0m35.395s
## 拉取映象 springboot-normal:0.0.2
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.2
real 0m6.501s
## 拉取映象 springboot-normal:0.0.3
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.3
real 0m6.993s
伺服器二推送從映象倉庫2拉取映象:
## 清理全部映象
$ docker rm --force $(docker images -qa)
## 拉取映象 springboot-layer:0.0.1
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.1
real 0m30.615s
## 拉取映象 springboot-layer:0.0.2
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.2
real 0m4.811s
## 拉取映象 springboot-layer:0.0.3
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.3
real 0m1.293s
七、映象構建、推送、拉取時間彙總
1、不使用分層構建映象
次數\階段 | 構建(s) | 推送(s) | 拉取(s) |
---|---|---|---|
第一次構建 | 24.98 | 35.21 | 35.39 |
第二次構建-修改依賴包 | 1.27 | 14.05 | 6.50 |
第三次構建-修改程式碼 | 1.32 | 14.18 | 6.99 |
如下圖:
2、使用分層構建映象
次數\階段 | 構建(s) | 推送(s) | 拉取(s) |
---|---|---|---|
第一次構建 | 26.12 | 34.12 | 30.61 |
第二次構建-修改依賴包 | 3.44 | 13.60 | 4.81 |
第三次構建-修改程式碼 | 2.82 | 4.80 | 1.29 |
如下圖:
3、總結
上面進行了使用 SpringBoot2.3.x 分層的方式構建映象與普通的方式構建映象,在映象的構建、推送、拉取方面進行了執行速度對比,總結出如下結論:
- 映象構建:在構建上,使用分層 Jar 構建映象可能比普通方式構建映象更繁瑣,所以也更耗時,故而在構建上分層 Jar 構建映象沒有太多優勢。
- 映象推送:在推送上,如果每次構建映象都只是修改構建映象專案的原始碼,使用分層 Jar 構建映象,可以大大加快映象推送速度。如果是修改構建映象專案中的依賴包,則和普通構建一樣速度很慢。
- 映象拉取:拉取和推送類似,如果只修改構建映象專案的原始碼,只會拉取原始碼相關的中間層映象,該層非常小(一般幾百KB),拉取速度自然非常快。而對構建映象專案的依賴包進行變動(增加依賴、刪除依賴、修改依賴版本等),則會和普通方式構建映象一樣,拉取速度很慢,這是因為依賴包層是中間層映象最大的一層(一般在10MB~200MB之間),如果該層發生變動則整個層會進行重新拉取,這樣速度自然會很慢。
—END—