使用Spring Boot建立docker image

flydean發表於2020-10-16

簡介

在很久很久以前,我們是怎麼建立Spring Boot的docker image呢?最最通用的辦法就是將Spring boot的應用程式打包成一個fat jar,然後寫一個docker file,將這個fat jar製作成為一個docker image然後執行。

今天我們來體驗一下Spring Boot 2.3.3 帶來的快速建立docker image的功能。

傳統做法和它的缺點

現在我們建立一個非常簡單的Spring Boot程式:

@SpringBootApplication
@RestController
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @GetMapping("/getInfo")
    public String getInfo() {
        return "www.flydean.com";
    }
}

預設情況下,我們build出來的是一個fat jar:springboot-with-docker-0.0.1-SNAPSHOT.jar

我們解壓看一下它的內容:

Spring boot的fat jar分為三個部分,第一部分就是BOOT-INF, 裡面的class目錄放的是我們自己編寫的class檔案。而lib目錄存放的是專案依賴的其他jar包。

第二部分是META-INF,裡面定義了jar包的屬性資訊。

第三部分是Spring Boot的類載入器,fat jar包的啟動是通過Spring Boot的jarLauncher來建立LaunchedURLClassLoader,通過它來載入lib下面的jar包,最後以一個新執行緒啟動應用的Main函式。

這裡不多講Spring Boot的啟動。

我們看一下,如果想要用這個fat jar來建立docker image應該怎麼寫:

FROM openjdk:8-jdk-alpine
EXPOSE 8080
ARG JAR_FILE=target/springboot-with-docker-0.0.1-SNAPSHOT.jar
ADD ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

這樣寫有兩個問題。

第一個問題:我們是用的far jar,在使用far jar的過程中會有一定的效能問題,肯定要比解壓過後的效能要低,尤其是在容器環境中執行的情況下,可能會更加突出。

第二個問題:我們知道docker的image是按layer來構建的,按layer構建的好處就是可以減少image構建的時間和重用之前的layer。

但是如果使用的是fat jar包,即使我們只修改了我們自己的程式碼,也會導致整個fat jar重新更新,從而影響docker image的構建速度。

使用Buildpacks

傳統的辦法除了有上面的兩個問題,還有一個就是需要自己構建docker file,有沒有一鍵構建docker image的方法呢?

答案是肯定的。

Spring Boot在2.3.0之後,引入了Cloud Native 的buildpacks,通過這個工具,我們可以非常非常方便的建立docker image。

在Maven和Gradle中,Spring Boot引入了新的phase: spring-boot:build-image

我們可以直接執行:

 mvn  spring-boot:build-image

執行之,很不幸的是,你可能會遇到下面的錯誤:

[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) on project springboot-with-docker: Execution default-cli of goal org.springframework.boot:spring-boot-maven-plugin:2.3.3.RELEASE:build-image failed: Docker API call to 'localhost/v1.24/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase-platform-api-0.3' failed with status code 500 "Internal Server Error" and message "Get https://gcr.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)" -> [Help 1]

這是因為我們無法從gcr.io中拉取映象!

沒關係,如果你會正確的上網方式的話,那麼我估計你已經找到了一個代理。

將你的代理配置到Docker的代理項裡面,我使用的是Docker desktop,下面是我的配置:

重新執行 mvn spring-boot:build-image

等待執行結果:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ springboot-with-docker ---
[INFO] Building image 'docker.io/library/springboot-with-docker:0.0.1-SNAPSHOT'
[INFO] 
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%

你可以看到,我們的確是需要從gcr.io拉取image。

Layered Jars

如果你不想使用Cloud Native Buildpacks,還是想使用傳統的Dockerfile。 沒關係,SpringBoot為我們提供了獨特的分層jar包系統。

怎麼開啟呢? 我們需要在POM檔案中加上下面的配置:

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layers>
                        <enabled>true</enabled>
                    </layers>
                </configuration>
            </plugin>
        </plugins>
    </build>

再次打包,看下jar包的內容:

看起來和之前的jar包沒什麼不同,只不過多了一個layers.idx 這個index檔案:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

index檔案主要分為4個部分:

  • dependencies - 非SNAPSHOT的依賴jar包
  • snapshot-dependencies - SNAPSHOT的依賴jar包
  • spring-boot-loader - Spring boot的class loader檔案
  • application - 應用程式的class和resources檔案

注意,這裡的index檔案是有順序的,它和我們將要新增到docker image中的layer順序是一致的。

最少變化的將會最先新增到layer中,變動最大的放在最後面的layer。

我們可以使用layertools jarmode來對生成的fat jar進行校驗或者解壓縮:

java -Djarmode=layertools -jar springboot-with-docker-0.0.1-SNAPSHOT.jar 
Usage:
  java -Djarmode=layertools -jar springboot-with-docker-0.0.1-SNAPSHOT.jar

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command

使用list命令,我們可列出jar包中的layer資訊。使用extract我們可以解壓出不同的layer。

我們執行下extract命令,看下結果:

可以看到,我們根據layers.idx解壓出了不同的資料夾。

我們看一下使用layer的dockerFile應該怎麼寫:

FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM adoptopenjdk:11-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

這樣我們的一個分層的DockerImage就建立完成了。

自定義Layer

如果我們需要自定義Layer該怎麼做呢?

我們可以建立一個獨立的layers.xml檔案:

<layers xmlns="http://www.springframework.org/schema/boot/layers"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
              https://www.springframework.org/schema/boot/layers/layers-2.3.xsd">
    <application>
        <into layer="spring-boot-loader">
            <include>org/springframework/boot/loader/**</include>
        </into>
        <into layer="application" />
    </application>
    <dependencies>
        <into layer="snapshot-dependencies">
            <include>*:*:*SNAPSHOT</include>
        </into>
        <into layer="company-dependencies">
            <include>com.flydean:*</include>
        </into>
        <into layer="dependencies"/>
    </dependencies>
    <layerOrder>
        <layer>dependencies</layer>
        <layer>spring-boot-loader</layer>
        <layer>snapshot-dependencies</layer>
        <layer>company-dependencies</layer>
        <layer>application</layer>
    </layerOrder>
</layers>

怎麼使用這個layer.xml呢?

新增到build plugin中就可以了:

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layers>
                        <enabled>true</enabled>
                        <configuration>${project.basedir}/src/main/resources/layers.xml</configuration>
                    </layers>
                </configuration>
            </plugin>
        </plugins>
    </build>

本文的例子:springboot-with-docker

本文作者:flydean程式那些事

本文連結:http://www.flydean.com/springboot-docker-image/

本文來源:flydean的部落格

歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

相關文章