最佳化Spring Boot應用的Docker打包速度

banq發表於2018-09-25
重點介紹如何在進行迭代開發和部署時採用更快速方法為Spring Boot應用構建Docker映象?也就是提高構建映象的速度,降低等待時間。

Docker概念

Docker有四個關鍵概念:映象,圖層,Dockerfile和Docker快取。

1. Dockerfile描述瞭如何構建映象。
2. 映象由許多層組成。
3. Dockerfile以基本映象開始並新增其他層。
4. 將新內容新增到映象時會生成新圖層。
5. 構建的每個層都是快取的,因此可以在後續構建中重複使用。
6. 當Docker構建執行時,它可以使用快取中的任何現有層,這減少了每次構建所需的總時間和空間,任何已更改或之前未構建的內容都將會再次重新構建。

圖層內容
這是層發揮重要作用的地方。僅當該層的內容未更改時,才能使用Docker快取中的現有層。Docker構建過程中如果更改的層越多,Docker重建映象所需的工作就越多。

圖層順序也很重要。只有在所有父圖層都保持不變的情況下才能重複使用圖層,最好在後面放置更頻繁更改的圖層,以便對它們進行更改會影響更少的子圖層。

層的順序和內容很重要。將應用程式打包為Docker映象時,最簡單的方法是將整個應用程式推送到單個層。但是,如果在更改最少量的程式碼時該應用程式包含許多靜態庫依賴項,則需要重建整個層。這最終會在Docker快取中浪費大量的構建時間和空間。

圖層影響部署
部署Docker映象時,圖層也很重要。在部署Docker映象之前,它們將被推送到遠端Docker儲存庫,此儲存庫充當所有部署映象的源,並且通常包含同一映象的許多版本。

Docker非常高效,只儲存一層,但是,對於經常部署且具有不斷重建大圖層的映象,這種高效率無用了。大型圖層,即使其內部的變化很小,也必須單獨儲存在儲存庫中並在網路中推送,這會對部署時間產生負面影響,因為需要為不變化的部分移動和儲存重複的位bit。

Docker中的Spring Boot應用
使用超級jar打包方法的Spring Boot應用程式是一個獨立的部署單元。這種模型非常適合在虛擬機器或構建包上進行部署,因為應用程式可以隨身攜帶所需的一切。

但是,這對於Docker部署卻是一個缺點:Docker已經提供了打包依賴關係的方法。將整個Spring Boot JAR放入Docker映象是很常見的。但是,這會導致Docker映象的應用程式層中有太多不變的位bit。

Spring社群正在討論如何在執行Spring Boot應用程式時減少部署大小和時間,特別是在Docker中。

這最終是在簡單性和效率之間進行權衡。為Spring Boot應用程式構建Docker映象的最常用方法是我稱之為“單層”方法。這在技術上並不正確,因為實際上Dockerfile建立了多個層,但它足以滿足本次討論的目的。

單層方法
我們來看看單層方法。單層方法快速,直接,易於理解和使用。Docker的Spring Boot指南列出了單層Dockerfile來構建Docker映象:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
<p class="indent">


最終結果是一個正常執行的Docker映象,其執行方式與你期望執行Spring Boot應用程式的方式完全相同。但是,它受到分層效率問題的困擾,因為它基於整個應用程式JAR,隨著應用程式源的更改,整個Spring Boot JAR都會重建,下一次構建Docker映象時,將重建整個應用程式層,包括所有未更改的庫依賴項。

更深入地瞭解單層方法

單層方法使用Spring Boot JAR構建Docker映象,基於Open JDK基礎映象之上的Docker層如下所示:

$ docker images
REPOSITORY                    TAG         IMAGE ID            CREATED             SIZE
springio/spring-petclinic     latest      94b0366d5ba2        16 seconds ago 
<p class="indent">


生成的Docker映象為140 MB。你可以使用該docker history命令檢查圖層。你可以看到Spring Boot應用程式JAR已複製到大小為38.3 MB的映像中。

$ docker history springio/spring-petclinic
<p class="indent">


下次構建Docker映象時,將重新建立整個38 MB層,因為JAR檔案已重新打包。

在此示例中,應用程式大小相對較小,僅基於 spring-boot-starter-web 其他依賴項,例如 spring-actuator。在現實世界中,這些大小通常要大一些,因為它們不僅包括Spring Boot庫,還包括其他第三方庫。根據我的經驗,實際的Spring Boot應用程式的大小範圍可以從50 MB到250 MB(如果不是更大)。

仔細觀察應用程式,應用程式程式碼中只有372 KB的應用程式JAR。剩餘的38 MB是庫依賴項。這意味著只有0.1%的層實際上在變化。其餘99.9%未變。

層生命週期
這說明了分層的基本考慮:內容的生命週期。圖層和內容應該具有相同的生命週期。Spring Boot應用程式的內容有兩個不同的生命週期:不經常更改的庫依賴項和頻繁更改的應用程式類。

每次由於應用程式程式碼更改而重建層時,還包括不變的二進位制檔案。在應用程式程式碼不斷變化和重新部署的快速應用程式開發環境中,這種附加成本會變得非常昂貴。

想象一下,一個應用團隊在Pet Clinic上進行迭代。團隊每天更改和重新部署應用程式10次。這10個新層的成本將為每天383 MB。使用更多真實尺寸,每天最多可達2.5 GB或更多。這最終會浪費構建時間,部署時間和Docker儲存庫空間。

快速、漸進的開發和交付是在權衡變得重要的時候,必須繼續使用簡單的單層方法或採用更有效的替代方法。

擁抱Docker,Go Dual Layer
在簡單和效率之間的權衡中,我覺得正確的選擇是“雙層”方法。(更多層可能,但太多層可能是有害的,並且違反了 Docker最佳實踐)。在雙層方法中,我們構建Docker映象,以便Spring Boot應用程式的庫依賴項存在於應用程式程式碼下面的層中。這樣,層遵循內容的不同生命週期。透過將不經常更改的庫依賴關係推送到單獨的層並僅將應用程式類保留在頂層,迭代重建和重新部署將更快。

[img index=1]

雙層方法可加速迭代開發,並最大限度地縮短部署時間。結果因應用程式而異,但平均而言,這會將應用程式部署大小減少90%,同時相應減少部署週期時間。

我們需要一種方法將Spring Boot應用程式拆分為這些單獨的元件:
springBootUtility是Open Liberty中的一個新工具,它將Spring Boot應用程式分為兩部分:庫依賴項,例如Spring Boot啟動器和其他第三方庫,以及應用程式程式碼。庫依賴項放在庫快取記憶體中,應用程式程式碼用於構建精簡應用程式。瘦應用程式包含一個檔案,該檔案引用類路徑上所需的庫。然後可以將此瘦應用程式部署到Open Liberty,它將從庫快取記憶體生成完整的類路徑。

構建此雙層映象的Dockerfile使用多階段構建。多階段構建允許單個Dockerfile建立多個映象,其中一個映象的內容可以複製到另一個映象,丟棄臨時內容。這使您可以大幅減小最終映象的大小,而無需涉及多個Docker檔案。我們使用此函式在Docker構建過程中拆分Spring Boot應用程式。

Docker映象使用Open JDK與Open J9和Open Liberty。Open JDK為開源Java技術提供了堅實的基礎。Open J9比Open JDK附帶的預設Java虛擬機器帶來了一些效能改進。Open Liberty是一個多程式設計模型執行時,支援Java EE,MicroProfile和Spring。這允許開發團隊使用具有一致執行時堆疊的各種程式設計模型。

Dockerfile:

FROM adoptopenjdk/openjdk8-openj9 as staging

ARG JAR_FILE
ENV SPRING_BOOT_VERSION 2.0

# Install unzip; needed to unzip Open Liberty
RUN apt-get update \
    && apt-get install -y --no-install-recommends unzip \
    && rm -rf /var/lib/apt/lists/*

# Install Open Liberty
ENV LIBERTY_SHA 4170e609e1e4189e75a57bcc0e65a972e9c9ef6e
ENV LIBERTY_URL https://public.dhe.ibm.com/ibmdl/export/pub/software/openliberty/runtime/release/2018-06-19_0502/openliberty-18.0.0.2.zip

RUN curl -sL "$LIBERTY_URL" -o /tmp/wlp.zip \
   && echo "$LIBERTY_SHA  /tmp/wlp.zip" > /tmp/wlp.zip.sha1 \
   && sha1sum -c /tmp/wlp.zip.sha1 \
   && mkdir /opt/ol \
   && unzip -q /tmp/wlp.zip -d /opt/ol \
   && rm /tmp/wlp.zip \
   && rm /tmp/wlp.zip.sha1 \
   && mkdir -p /opt/ol/wlp/usr/servers/springServer/ \
   && echo spring.boot.version="$SPRING_BOOT_VERSION" > /opt/ol/wlp/usr/servers/springServer/bootstrap.properties \
   && echo \
'<?xml version="1.0" encoding="UTF-8"?> \
<server description="Spring Boot Server"> \
  <featureManager> \
    <feature>jsp-2.3</feature> \
    <feature>transportSecurity-1.0</feature> \
    <feature>websocket-1.1</feature> \
    <feature>springBoot-${spring.boot.version}</feature> \
  </featureManager> \
  <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="9080" httpsPort="9443" /> \
  <include location="appconfig.xml"/> \
</server>' > /opt/ol/wlp/usr/servers/springServer/server.xml \
   && /opt/ol/wlp/bin/server start springServer \
   && /opt/ol/wlp/bin/server stop springServer \
   && echo \
'<?xml version="1.0" encoding="UTF-8"?> \
<server description="Spring Boot application config"> \
  <springBootApplication location="app" name="Spring Boot application" /> \
</server>' > /opt/ol/wlp/usr/servers/springServer/appconfig.xml

# Stage the fat JAR
COPY ${JAR_FILE} /staging/myFatApp.jar

# Thin the fat application; stage the thin app output and the library cache
RUN /opt/ol/wlp/bin/springBootUtility thin \
 --sourceAppPath=/staging/myFatApp.jar \
 --targetThinAppPath=/staging/myThinApp.jar \
 --targetLibCachePath=/staging/lib.index.cache

# unzip thin app to avoid cache changes for new JAR
RUN mkdir /staging/myThinApp \
   && unzip -q /staging/myThinApp.jar -d /staging/myThinApp

# Final stage, only copying the liberty installation (includes primed caches)
# and the lib.index.cache and thin application
FROM adoptopenjdk/openjdk8-openj9

VOLUME /tmp

# Create the individual layers
COPY --from=staging /opt/ol/wlp /opt/ol/wlp
COPY --from=staging /staging/lib.index.cache /opt/ol/wlp/usr/shared/resources/lib.index.cache
COPY --from=staging /staging/myThinApp /opt/ol/wlp/usr/servers/springServer/apps/app

# Start the app on port 9080
EXPOSE 9080
CMD ["/opt/ol/wlp/bin/server", "run", "springServer"]
<p class="indent">


使用Docker的多階段構建和springBootUtilityOpen Liberty,Dockerfile分割Spring Boot應用程式:

我們從一個臨時映象開始。首先,我們安裝unzip。接下來,我們在某些配置中下載Open Liberty和stage。所有這些準備工作都需要準備好Open Liberty工具。我們知道它非常難看,這是我們在不久的將來發布Liberty 18.0.0.2 Docker映象時會改進的事情之一。

一旦映象具有所需的所有工具,JAR檔案就會被複制到暫存映象中並進行拆分。在下面建立瘦應用程式之後/staging/myFatApp.jar,將採取進一步的最佳化步驟來解壓縮它。此解壓縮導致應用程式直接從類檔案託管。如果類檔案未更改,這允許後續重建重用應用程式層。

現在,分段工作已經完成,我們重新開始,以便我們可以複製最終的Liberty安裝,依賴庫和瘦應用程式。Dockerfile中的單獨COPY命令生成單獨的層。較大的庫依賴層(34.2MB)和較小的應用層(1.01MB)是'雙層'的含義。

$ docker history openlibertyio / spring-petclinic

現在,當進行應用程式更改時,只需要更改應用程式層。



Optimizing Spring Boot apps for Docker - OpenLiber

[該貼被banq於2018-09-25 15:05修改過]

相關文章