許多人使用容器來包裝他們的 Spring Boot 應用程序,而構建容器并不是一件簡單的事情。這是針對 Spring Boot 應用程序開發(fā)人員的指南,容器對于開發(fā)人員來說并不總是一個好的抽象。它們迫使你去了解和思考低層次的問題。但是,有時可能會要求您創(chuàng)建或使用容器,因此了解構建塊是值得的。在本指南中,我們旨在向您展示如果您面臨需要創(chuàng)建自己的容器的前景,您可以做出的一些選擇。
我們假設您知道如何創(chuàng)建和構建基本的 Spring Boot 應用程序。如果沒有,請轉到入門指南之一?——例如,關于構建REST 服務的指南。從那里復制代碼并練習本指南中包含的一些想法。
還有一個關于Docker的入門指南,這也是一個很好的起點,但它沒有涵蓋我們在此處介紹的選擇范圍或詳細介紹它們。
一個基本的 Dockerfile
Spring Boot 應用程序很容易轉換為可執(zhí)行的 JAR 文件。所有的入門指南都是這樣做的,你從Spring Initializr下載的每個應用程序都有一個構建步驟來創(chuàng)建一個可執(zhí)行的 JAR。使用 Maven,你運行./mvnw install,使用 Gradle,你運行./gradlew build。運行該 JAR 的基本 Dockerfile 將如下所示,位于項目的頂層:
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]復制
JAR_FILE您可以作為命令的一部分傳入docker(Maven 和 Gradle 不同)。對于 Maven,以下命令有效:
docker build --build-arg JAR_FILE=target/*.jar -t myorg/myapp .復制
對于 Gradle,以下命令有效:
docker build --build-arg JAR_FILE=build/libs/*.jar -t myorg/myapp .復制
一旦你選擇了一個構建系統(tǒng),你就不需要ARG. 您可以對 JAR 位置進行硬編碼。對于 Maven,如下所示:
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]復制
然后我們可以使用以下命令構建鏡像:
docker build -t myorg/myapp .復制
然后我們可以通過運行以下命令來運行它:
docker run -p 8080:8080 myorg/myapp復制
輸出類似于以下示例輸出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.2.RELEASE)
Nov 06, 2018 2:45:16 PM org.springframework.boot.StartupInfoLogger logStarting
INFO: Starting Application v0.1.0 on b8469cdc9b87 with PID 1 (/app.jar started by root in /)
Nov 06, 2018 2:45:16 PM org.springframework.boot.SpringApplication logStartupProfileInfo
...復制
如果你想在鏡像內(nèi)部四處尋找,你可以通過運行以下命令在其中打開一個 shell(注意基礎鏡像沒有bash):
docker run -ti --entrypoint /bin/sh myorg/myapp復制
輸出類似于以下示例輸出:
/ # ls
app.jar dev home media proc run srv tmp var
bin etc lib mnt root sbin sys usr
/ #
我們在示例中使用的 alpine 基礎容器沒有bash,所以這是一個ashshell。它具有一些但不是全部的特性bash。
如果你有一個正在運行的容器并且你想查看它,你可以通過運行docker exec:
docker run --name myapp -ti --entrypoint /bin/sh myorg/myapp
docker exec -ti myapp /bin/sh
/ #復制
傳遞給命令myapp的位置在哪里。如果您沒有使用,docker 會分配一個助記名稱,您可以從. 您還可以使用容器的 SHA 標識符而不是名稱。SHA 標識符在輸出中也可見。--namedocker run--namedocker psdocker ps
入口點
使用Dockerfile的exec 形式ENTRYPOINT,以便沒有外殼包裝 Java 進程。優(yōu)點是java進程響應KILL發(fā)送到容器的信號。實際上,這意味著(例如)如果您docker run在本地使用圖像,則可以使用CTRL-C. 如果命令行有點長,您可以COPY在運行之前將其提取到 shell 腳本中并放入映像中。以下示例顯示了如何執(zhí)行此操作:
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY run.sh .
COPY target/*.jar app.jar
ENTRYPOINT ["run.sh"]復制
請記住使用exec java …啟動 java 進程(以便它可以處理KILL信號):
run.sh
#!/bin/sh
exec java -jar /app.jar復制
入口點的另一個有趣方面是您是否可以在運行時將環(huán)境變量注入 Java 進程。例如,假設您想要在運行時添加 Java 命令行選項。您可以嘗試這樣做:
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","${JAVA_OPTS}","-jar","/app.jar"]復制
然后您可以嘗試以下命令:
docker build -t myorg/myapp .
docker run -p 9000:9000 -e JAVA_OPTS=-Dserver.port=9000 myorg/myapp復制
這失敗了,因為${}替換需要一個外殼。exec 表單不使用 shell 來啟動進程,因此不應用選項。您可以通過將入口點移動到腳本(如run.sh前面顯示的示例)或在入口點顯式創(chuàng)建 shell 來解決此問題。以下示例顯示了如何在入口點中創(chuàng)建 shell:
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]復制
然后,您可以通過運行以下命令來啟動此應用程序:
docker run -p 8080:8080 -e "JAVA_OPTS=-Ddebug -Xmx128m" myorg/myapp復制
該命令產(chǎn)生類似于以下的輸出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.RELEASE)
...
2019-10-29 09:12:12.169 DEBUG 1 --- [ main] ConditionEvaluationReportLoggingListener :
============================
CONDITIONS EVALUATION REPORT
============================
...復制
(前面的輸出顯示了 Spring BootDEBUG生成的完整輸出的一部分。)-Ddebug
將 anENTRYPOINT與顯式 shell 一起使用(如前面的示例所做的那樣)意味著您可以將環(huán)境變量傳遞給 Java 命令。但是,到目前為止,您還不能為 Spring Boot 應用程序提供命令行參數(shù)。以下命令不會在端口 9000 上運行應用程序:
docker run -p 9000:9000 myorg/myapp --server.port=9000復制
該命令產(chǎn)生以下輸出,將端口顯示為 8080 而不是 9000:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.RELEASE)
...
2019-10-29 09:20:19.718 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080復制
它不起作用,因為 docker 命令(該--server.port=9000部分)被傳遞到入口點 ( sh),而不是它啟動的 Java 進程。要解決此問題,您需要將命令行從以下添加CMD到ENTRYPOINT:
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"]復制
然后您可以運行相同的命令并將端口設置為 9000:
$ docker run -p 9000:9000 myorg/myapp --server.port=9000復制
如以下輸出示例所示,端口確實設置為 9000:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.RELEASE)
...
2019-10-29 09:30:19.751 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 9000復制
注意${0}“命令”(在這種情況下是第一個程序參數(shù))和${@}“命令參數(shù)”(程序參數(shù)的其余部分)的使用。如果您使用腳本作為入口點,那么您不需要${0}(/app/run.sh在前面的示例中)。以下列表顯示了腳本文件中的正確命令:
run.sh
#!/bin/sh
exec java ${JAVA_OPTS} -jar /app.jar ${@}復制
docker配置到現(xiàn)在都非常簡單,生成的鏡像效率不是很高。docker 鏡像有一個文件系統(tǒng)層,其中包含 fat JAR,我們對應用程序代碼所做的每一次更改都會更改該層,這可能是 10MB 或更多(對于某些應用程序甚至高達 50MB)。我們可以通過將 JAR 拆分為多個層來改進這一點。
較小的圖像
請注意,前面示例中的基本映像是openjdk:8-jdk-alpine. 這些alpine圖像小于Dockerhubopenjdk的標準庫圖像。您還可以通過使用標簽而不是. 并非所有應用程序都使用 JRE(與 JDK 相對),但大多數(shù)應用程序都可以。一些組織強制執(zhí)行一個規(guī)則,即每個應用程序都必須使用 JRE,因為存在濫用某些 JDK 功能(例如編譯)的風險。jrejdk
另一個可以讓您獲得更小的映像的技巧是使用JLink,它與 OpenJDK 11 捆綁在一起。JLink 允許您從完整 JDK 中的模塊子集構建自定義 JRE 分發(fā),因此您不需要 JRE 或 JDK基礎圖像。原則上,這將使您獲得比使用openjdk官方 docker 圖像更小的總圖像大小。在實踐中,您(還)不能將alpine基礎鏡像與 JDK 11 一起使用,因此您對基礎鏡像的選擇是有限的,并且可能會導致最終鏡像的大小更大。此外,您自己的基本映像中的自定義 JRE 不能在其他應用程序之間共享,因為它們需要不同的自定義。因此,您的所有應用程序可能都有較小的圖像,但它們?nèi)匀恍枰L的時間才能啟動,因為它們沒有從緩存 JRE 層中受益。
最后一點突出了圖像構建者的一個非常重要的問題:目標不一定總是盡可能地構建最小的圖像。較小的圖像通常是一個好主意,因為它們需要更少的時間來上傳和下載,但前提是它們中的所有圖層都沒有被緩存。如今,圖像注冊非常復雜,您很容易通過嘗試巧妙地構建圖像而失去這些功能的好處。如果您使用通用基礎層,圖像的總大小就不再那么重要了,而且隨著注冊中心和平臺的發(fā)展,它可能變得更不重要。話雖如此,嘗試優(yōu)化應用程序映像中的層仍然很重要且有用。然而,
更好的 Dockerfile
由于 JAR 本身的打包方式,Spring Boot fat JAR 自然有“層”。如果我們先解包,它已經(jīng)分為外部依賴和內(nèi)部依賴。要在 docker 構建中一步完成此操作,我們需要先解壓縮 JAR。以下命令(堅持使用 Maven,但 Gradle 版本非常相似)解壓縮 Spring Boot fat JAR:
mkdir target/dependency
(cd target/dependency; jar -xf ../*.jar)
docker build -t myorg/myapp .復制
然后我們可以使用下面的Dockerfile
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]復制
現(xiàn)在有三層,所有應用程序資源都在后面兩層。如果應用程序依賴沒有改變,第一層(from BOOT-INF/lib)不需要改變,所以構建更快,并且容器在運行時的啟動也更快,只要基礎層已經(jīng)被緩存。
我們使用了一個硬編碼的主應用程序類:hello.Application. 這對于您的應用程序可能有所不同。如果你愿意,你可以用另一個參數(shù)化它ARG。您還可以將 Spring Boot fat 復制JarLauncher到映像中并使用它來運行應用程序。它可以工作,您不需要指定主類,但啟動時會慢一些。
Spring Boot 層索引
從 Spring Boot 2.3.0 開始,使用 Spring Boot Maven 或 Gradle 插件構建的 JAR 文件在 JAR 文件中包含層信息。該層信息根據(jù)應用程序構建之間更改的可能性來分離應用程序的各個部分。這可以用來使 Docker 鏡像層更加高效。
層信息可用于將 JAR 內(nèi)容提取到每個層的目錄中:
mkdir target/extracted
java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
docker build -t myorg/myapp .復制
然后我們可以使用以下內(nèi)容Dockerfile:
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG EXTRACTED=/workspace/app/target/extracted
COPY ${EXTRACTED}/dependencies/ ./
COPY ${EXTRACTED}/spring-boot-loader/ ./
COPY ${EXTRACTED}/snapshot-dependencies/ ./
COPY ${EXTRACTED}/application/ ./
ENTRYPOINT ["java","org.springframework.boot.loader.JarLauncher"]
Spring Boot fatJarLauncher是從 JAR 中提取到鏡像中的,因此它可以用于啟動應用程序,而無需對主應用程序類進行硬編碼。
有關使用分層功能的更多信息,請參閱Spring Boot 文檔。
調整
如果您想盡快啟動您的應用程序(大多數(shù)人都這樣做),您可能會考慮一些調整:
- 使用spring-context-indexer(鏈接到文檔)。它不會為小型應用程序增加太多,但每一點都有幫助。
- 如果您負擔得起,請不要使用執(zhí)行器。
- 使用 Spring Boot 2.1(或更高版本)和 Spring 5.1(或更高版本)。
- 使用(通過命令行參數(shù)、系統(tǒng)屬性或其他方法)修復Spring Boot 配置文件的位置。spring.config.location
- 通過設置來關閉 JMX(您可能不需要在容器中使用它)spring.jmx.enabled=false。
- 使用-noverify. 還要考慮-XX:TieredStopAtLevel=1(這會在以后減慢 JIT 但會縮短啟動時間)。
- 使用 Java 8 的容器內(nèi)存提示:-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap. 在 Java 11 中,默認情況下這是自動的。
您的應用程序在運行時可能不需要完整的 CPU,但它確實需要多個 CPU 才能盡快啟動(至少兩個,四個更好)。如果您不介意啟動速度較慢,則可以將 CPU 限制在四個以下。如果您被迫從少于四個 CPU 開始,設置 可能會有所幫助
-Dspring.backgroundpreinitializer.ignore=true,因為它可以防止 Spring Boot 創(chuàng)建一個它可能無法使用的新線程(這適用于 Spring Boot 2.1.0 及更高版本)。
多階段構建
A Better Dockerfile中Dockerfile所示的假設假設胖 JAR 已經(jīng)在命令行上構建。您還可以通過使用多階段構建并將結果從一個圖像復制到另一個圖像來在 docker 中執(zhí)行該步驟。以下示例通過使用 Maven 來實現(xiàn):
Dockerfile
FROM openjdk:8-jdk-alpine as build
WORKDIR /workspace/app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]復制
第一個圖像標記為build,它用于運行 Maven、構建胖 JAR 并解壓縮它。解包也可以由 Maven 或 Gradle 完成(這是入門指南中采用的方法)。沒有太大區(qū)別,只是必須編輯構建配置并添加插件。
請注意,源代碼已分為四層。后面的層包含構建配置和應用程序的源代碼,前面的層包含構建系統(tǒng)本身(Maven 包裝器)。這是一個小的優(yōu)化,也意味著我們不必將target目錄復制到 docker 鏡像,即使是用于構建的臨時鏡像。
RUN每個源代碼更改的構建都很慢,因為必須在第一部分重新創(chuàng)建 Maven 緩存。但是你有一個完全獨立的構建,只要他們有 docker,任何人都可以運行它來運行你的應用程序。這在某些環(huán)境中可能非常有用——例如,您需要與不了解 Java 的人共享您的代碼。
實驗功能
Docker 18.06 帶有一些“實驗性”特性,包括緩存構建依賴項的方法。要打開它們,您需要在守護進程 ( dockerd) 中有一個標志,并在運行客戶端時需要一個環(huán)境變量。然后你可以添加一個“神奇”的第一行到你的Dockerfile:
Dockerfile
# syntax=docker/dockerfile:experimental復制
然后該RUN指令接受一個新標志:--mount. 以下清單顯示了一個完整示例:
Dockerfile
# syntax=docker/dockerfile:experimental
FROM openjdk:8-jdk-alpine as build
WORKDIR /workspace/app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]復制
然后你可以運行它:
DOCKER_BUILDKIT=1 docker build -t myorg/myapp .復制
以下清單顯示了示例輸出:
...
=> /bin/sh -c ./mvnw install -DskipTests 5.7s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:3defa...
=> => naming to docker.io/myorg/myapp復制
使用實驗性功能,您會在控制臺上獲得不同的輸出,但您可以看到,如果緩存是熱的,現(xiàn)在 Maven 構建只需幾秒鐘而不是幾分鐘。
這個Dockerfile配置的 Gradle 版本非常相似:
Dockerfile
# syntax=docker/dockerfile:experimental
FROM openjdk:8-jdk-alpine AS build
WORKDIR /workspace/app
COPY . /workspace/app
RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build
RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/build/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
雖然這些功能處于實驗階段,但打開和關閉 buildkit 的選項取決于docker您使用的版本。檢查您擁有的版本的文檔(前面顯示的示例對于docker18.0.6 是正確的)。
安全方面
就像在經(jīng)典 VM 部署中一樣,進程不應以 root 權限運行。相反,映像應包含運行應用程序的非 root 用戶。
在 aDockerfile中,您可以通過添加另一個添加(系統(tǒng))用戶和組并將其設置為當前用戶(而不是默認的 root)的層來實現(xiàn)此目的:
Dockerfile
FROM openjdk:8-jdk-alpine
RUN addgroup -S demo && adduser -S demo -G demo
USER demo
...復制
如果有人設法突破您的應用程序并在容器內(nèi)運行系統(tǒng)命令,這種預防措施會限制他們的能力(遵循最小權限原則)。
一些進一步的Dockerfile命令只能以 root 身份運行,因此您可能必須將 USER 命令進一步向下移動(例如,如果您計劃在容器中安裝更多包,它只能以 root 身份運行)。
對于其他方法,不使用 aDockerfile可能更適合。例如,在后面描述的 buildpack 方法中,大多數(shù)實現(xiàn)默認使用非 root 用戶。
另一個考慮因素是大多數(shù)應用程序在運行時可能不需要完整的 JDK,因此一旦我們進行了多階段構建,我們就可以安全地切換到 JRE 基礎映像。因此,在前面顯示的多階段構建中,我們可以將其用于最終的可運行映像:
Dockerfile
FROM openjdk:8-jre-alpine
...復制
如前所述,這也節(jié)省了映像中的一些空間,這些空間將被運行時不需要的工具占用。
審核編輯:湯梓紅
-
容器
+關注
關注
0文章
499瀏覽量
22120 -
應用程序
+關注
關注
38文章
3292瀏覽量
57912 -
spring
+關注
關注
0文章
340瀏覽量
14388 -
Docker
+關注
關注
0文章
492瀏覽量
11964
發(fā)布評論請先 登錄
相關推薦
評論