無伺服器運算 微服務 Serverless Azure DevOps 雲原生

無伺服器整合微服務正夯 VS Code即可順暢開發測試

用Java語言Quarkus框架 實作輕快Serverless程式

2020-01-30
觀察微服務架構的歷史演進,發展至今,無伺服器運算整合至微服務架構已是必然的趨勢。一般常見使用NodeJS和C#來開發Lambda和Azure Functions,而本文則將以Visual Studio Code開發工具來做示範,說明怎樣利用企業主流的Java語言來實作小而快的Serverless程式。

 

在前兩期,分別介紹了無伺服器運算和適用之各種情境,以及早先介紹的微服務架構歷史演進,微服務架構已經成為構建雲原生應用的標準,根據國際數據資訊(IDC)的研究,預計到2022年,將有90%的新應用系統採用微服務架構(https://www.idc.com/getdoc.jsp?containerId=prUS44417618),隨著無伺服器運算之生態系日趨成熟,微服務架構勢必整合無伺服器運算。

在此,節錄IDC研究內文的第四點:「4. 到2022年,所有應用系統中的90%將採用微服務架構,這些架構可提升設計、除錯、更新和善用第三方程式的能力;所有營運環境應用程式中的35%將會是雲原生方式。在數位經濟的要求下,需以高質量應用系統來應付業務速度,並趨向「超敏捷應用系統(Hyperagile Apps)」之演變,以高度模組化、分散式、持續更新,並利用容器和無服務器運算等之雲原生技術,結合敏捷/DevOps方法及方法論,企業可以極大地加速其推動數位創新的能力,會比傳統方式快50~100倍(或更多)的週期下演進。」

日前,微軟推出整合無伺服器和微服務的開發框架Dapr,Dapr是開源、可移植的、可以事件驅動來運行,它讓開發人員能夠輕鬆地建構出彈性、微服務的無狀態和有狀態之應用程式,這些應用程式可運行在雲端和邊緣設備上,支援主流的開發語言和多樣性的開發框架,並簡化了建置應用程式的過程,如電子商務應用,其框架概念模型如圖1所示。

圖1  Dapr框架概念模型。

從上述兩大趨勢,可知無伺服器運算整合到微服務架構是確立的發展方向,之前只提到如何使用Node.js和C#來開發Lambda和Azure Functions,至於如何利用企業主流的Java語言來實作小而快的Serverless程式,本篇將以Visual Studio Code開發工具來示範說明。

何謂Quarkus

Quarkus是由Red Hat所帶領的一項開源專案,用於建構微服務之Java開發框架,建置出的服務通常只需要很少的記憶體,並且啟動非常快速。

換句話說,Quarkus是一套開發高性能Java容器的強大技術。打從專案啟動,Quarkus就採取容器優先理念來進行框架設計,事實上,這就意味著Quarkus主要是針對降低JVM記憶體使用量和快速啟動方面來優化。

Quarkus不是完整的Jakarta EE應用程式伺服器,但具備許多非常類似的功能,就像Spring Boot一樣,但與Spring Boot最大的區別在於,Quarkus透過在建置階段而非在運行時,盡其可能地耗費心思和時間來調校JVM和容器的工作負載,但這意味著不支援Java Reflection機制。

而使用Quarkus有兩種方式,第一種是可使用典型的JVM(例如Hotspot)來運行,第二種是透過GraalVM將Java Bytecode轉換為二進制機器碼,當使用二進制機器碼時,記憶體使用量更少,啟動時間更短,圖2是Quarkus官網上的量測數據。

圖2  Quarkus官網對比封裝後程式大小和啟動速度之比較圖。

認識GraalVM

至於為何Quarkus有如此優異的性能,底層技術其實是依賴Oracle GraalVM開源專案,GraalVM提供了一個全面的程式虛擬機生態系統,此生態系支援多種語言,包括Java和其他基於JVM的程式語言,以及JavaScript、Ruby、Python、R和C/C++及其他基於LLVM的程式語言,並可運行在不同的部署情境(OpenJDK、Node.js,以及Oracle資料庫或單獨執行),詳細的GraalVM可應用場景和功能可參閱「Top 10 Things To Do With GraalVM」部落格文章(https://medium.com/graalvm/graalvm-ten-things-12d9111f307d)。

對於現有的Java應用程式,GraalVM提供更快的執行效能,透過腳本語言可提供擴展性或以預先編譯(Ahead-of-time Compilation)來建立Native Image方式(二進制機器碼)達到加速目的。GraalVM能夠執行OpenJDK編譯出的中間碼內容,透過新的即時編譯技術(Just-in-time)讓Java應用程式運行得更快。GraalVM負責將Java Bytecode編譯成機器碼,特別是針對其他JVM-based的語言,如Scala,Twitter就是以這樣的配置在營運環境中運行GraalVM,而帶來效能上的優勢。當然,GraalVM在效能方面則取決於實際工作量而有不同的表現。

以Java VM運行應用程式會帶來啟動和資源占用的成本問題,GraalVM具備為現有基於JVM的應用程式建立為機器碼(Native Image)之功能,Native Image產製過程是使用靜態程式碼分析來尋找Java main方法會使用到的任何程式碼,接著執行完整的預先編譯(AOT),完成後的原生二進制碼包含整個機器碼,可馬上執行的整個過程,省掉Bytecode到二進制機器碼的轉譯過程,它可以連結到本機上其他程式,還可選擇包括GraalVM編譯器,提供彼此互補的即時(JIT)編譯方式,進而以高性能運行任何基於GraalVM的程式語言。圖3說明了以GraalVM作為系統架構的開源生態系。

圖3  GraalVM開源生態系。

建立Quarkus範例專案

由於Microsoft Visual Studio Code開發工具採取開源策略,並且不斷優化執行效能,加上VSC Marketplace生態系,早已不只有Node.js開發上可以使用,藉由Microsoft和Red Hat官方大力支持,連Java開發相關延伸工具皆已完成,能夠非常順暢開發並測試。關於Java語言方面的安裝使用,Microsoft官方亦有專屬頁面來說明(https://code.visualstudio.com/docs/languages/java),Microsoft甚至還提供Docker在VSC的延伸管理工具(https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)。

Quarkus框架也仿效Spring Boot,一樣提供Maven或Gradle專案產生器網頁(https://code.quarkus.io/),而在此範例,是透過Microsoft Visual Studio Code開發工具來產生,除了先自行安裝VSC和Java相關延伸工具,還須安裝由Red Hat所提供的Quarkus Extension延伸工具Quarkus Tools for Visual Studio Code(https://marketplace.visualstudio.com/items?itemName=redhat.vscode-quarkus),如圖4所示,詳細使用說明請見Red Hat Developers Blog(https://developers.redhat.com/blog/2019/09/23/how-the-new-quarkus-extension-for-visual-studio-code-improves-the-development-experience/)。

圖4  下載Quarkus Tools for Visual Studio Code。

首先,開啟VSC,按下〔F1〕,輸入「quarkus」,便可看到相關Quarkus延伸工具所提供的選項,選擇「Quarkus: Generate a Maven project」,如圖5所示,陸續輸入Maven Project的groupId、artifactId、version,如圖6所示,以及專案使用之功能函式庫,如圖7所示,最後選擇存放專案的目錄,便完成建立Quarkus模板專案。

圖5  建立Quarkus Maven Project。
圖6  輸入Maven groupId。
圖7  選擇Quarkus所提供之函式庫。

圖8是建立後的專案畫面,其中還包括「docker」目錄,當中包含建置一般的Docker Image和前面談到的Native Image之Dockerfile。

圖8  建立完成之Quarkus專案畫面。

測試Quarkus範例程式 並包裝成Docker Image

接下來,開啟「GreetingResource.java」檔案,插入Debug中斷點,按下〔F1〕,一樣輸入「quarkus」,選擇「Quarkus: Debug current Quarkus project」。

如圖9所示,如此一來,就可以在無Main Method的情況下,執行VSC Debug功能了。

圖9  加入Quarkus Debug功能。

完成後,點擊左邊除錯的蟲圖示,開啟Debug畫面,按下「start debugging」的箭頭,在瀏覽器網址輸入「http://localhost:8080/hello」,就能進入到Debug中斷點,並在右邊看到此程式所使用到的物件(圖10)。若只須執行程式而不用除錯,則在下方終端畫面輸入「./mvnw compile quarkus:dev」,便可啟動程式。

圖10  此Quarkus範例專案除錯畫面。

完成程式撰寫後,便可透過「docker」目錄中的Dockerfile.jvm來建置Docker Image,首先將程式包裝成jar檔,在終端畫面輸入「./mvnw package」,完成之後,在「target」目錄中就可看到建置完成後的「XXX-runner.jar」,如圖11所示。緊接著,輸入「docker build -f src/main/docker/Dockerfile.jvm -t philipz/quarkus-jvm .」命令,建置出可執行的Docker Image,執行過程如圖12所示,其Dockerfile.jvm內容如下:

圖11  執行「mvn package」建置jar檔。
圖12  Docker Build建置過程。

FROM fabric8/java-alpine-openjdk8-jre ENV JAVA_OPTIONS="-Dquarkus.http. host=0.0.0.0 -Djava.util.logging. manager=org.jboss.logmanager. LogManager" ENV AB_ENABLED=jmx_exporter COPY target/lib/* /deployments/lib/ COPY target/*-runner.jar /deployments/ app.jar EXPOSE 8080

# run with user 1001 and be prepared for be running in OpenShift too RUN adduser -G root --no-create- home --disabled-password 1001 \   && chown -R 1001 /deployments \   && chmod -R "g+rwX" /deployments \   && chown -R 1001:root /deployments USER 1001

ENTRYPOINT [ "/deployments/run- java.sh" ]

此Dockerfile.jvm是使用fabric8/java-alpine-openjdk8-jre為基礎映像檔,添加兩個環境變數後,再將相依之lib中的jar檔複製到容器中的「/deployments/lib」目錄中。接著,複製上述建置出的XXX-runner.jar,開通TCP 8080 Port,再新增User ID:1001的Linux使用者,賦予並限制檔案權限,以「USER 1001」來執行run-java.sh啟動腳本。若想了解更多Dockerfile相關說明,可參考網管人2017年10月號第141期《正確撰寫Dockerfile 製作最好用容器映像檔》一文。

完成之後,執行「docker run -d -p 8080:8080 philipz/quarkus-jvm」確認是否正常運作,並輸入「docker stats」查看其使用的記憶體空間,再利用「docker logs CONTAINER_ID」了解Quarkus範例專案啟動時間,如圖13所示,其中記憶體占用112MB,啟動速度1.296秒。

圖13  執行「ddocker stats」和「docker log」查看結果。

完成以JVM方式執行Quarkus範例專案後,再來使用GraalVM方式,編譯成Native Image執行檔。首先,輸入「./mvnw package -Pnative -Dquarkus.native.container-runtime=docker」,會藉由Docker執行GraalVM編譯,無須在本機上安裝GraalVM。大約幾分鐘之後,就可以在「target」目錄下看到XXX-runner的執行檔。

直接執行確認其功能,使用「./target/quarkus-getting-started-1.0.0-SNAPSHOT-runner」。確認運作正確之後,再輸入「docker build -f src/main/docker/Dockerfile.native -t philipz/quarkus-native .」,建置出可執行的GraalVM Native Image,如圖14所示,其Dockerfile.native內容如下:

圖14  測試Native Image執行檔及Dockerfile.native建置。

FROM registry.access.redhat.com/ ubi8/ubi-minimal WORKDIR /work/ COPY target/*-runner /work/ application RUN chmod 775 /work EXPOSE 8080 CMD ["./application", "-Dquarkus. http.host=0.0.0.0"]

此Dockerfile.native是使用「registry.access.redhat.com/ubi8/ubi-minimal」為基礎映像檔,因執行「./mvnw package -Pnative -Dquarkus.native.container-runtime=docker」過程中,會使用「quay.io/quarkus/ubi-quarkus-native-image:19.2.1」映像檔來編譯,其ubi是Red Hat官方所認可過,可以在OpenShift上運行的通用基礎映像檔(https://www.redhat.com/en/blog/introducing-red-hat-universal-base-image),顧及到編譯環境及執行環境的一致性,故皆使用ubi系列映像檔,接著複製建置出的XXX-runner到「/work/application」,修改「work」目錄之權限模式,開通TCP 8080 Port,最後執行application執行檔。

接下來,執行建置好的Native Image映像檔,輸入「docker run -d -p 8080:8080 philipz/quarkus-native」,再使用「docker stats」查看其使用之記憶體空間,並利用「docker logs CONTAINER_ID」了解Quarkus範例專案啟動時間,其中記憶體僅占用2.57MB,啟動速度只須0.014秒,如圖15所示,可見利用GraalVM Native Image編譯後,此Java程式快速啟動和低記憶體使用量,才適合應用於Serverless運行方式,像是AWS Lambda和早先幾篇所介紹的Knative無伺服器框架。

圖15  以「docker stats」和「docker log」查看Native Image結果。

最小化Quarkus範例程式 之Docker Image

比較過使用JVM和GraalVM Native Image兩種方式之啟動速度和記憶體使用量有如此大的差異後,接著查看兩者的映像檔大小,如圖16所示,反倒是Native Image還多了20MB左右,試問有可能同時顧慮到效率和映像檔尺寸呢?

圖16  JVM和Native Image的映像檔比較。

其實,透過映像檔之母scratch(https://hub.docker.com/_/scratch),只要將Native Image二進制執行檔和相依之Shared Library檔案一併放入scratch此空白映像檔,除了將不必要的Linux檔案移除之外,還可減少無關程式的漏洞和攻擊弱面,只剩下自己所寫的程式,當然這程式就得靠自己檢查安全性。以下便介紹如何透過scratch映像檔來產製最小化的Quarkus容器映像檔。

首先,比照Dockerfile.native使用ubi-minimal作為Quarkus範例程式執行環境,並利用多階段建置方式(multi-stage builds,https://docs.docker.com/develop/develop-images/multistage-build/),其Dockerfile內容如下:

FROM registry.access.redhat.com/ ubi8/ubi-minimal as lddpackage WORKDIR /work/ COPY copy_ldd.sh /work/ COPY target/*-runner /work/ application RUN chmod -R 775 /work && ./copy_ ldd.sh application build   FROM scratch COPY --from=lddpackage /work/build / ENTRYPOINT ["/app/application", "- Dquarkus.http.host=0.0.0.0"]

關鍵是第一階段將copy_ldd.sh腳本程式複製到容器的「/work」目錄下,並把XXX-runner執行檔複製成「/work/application」,再修改「/work」權限模式,並執行copy_ldd.sh腳本程式;而第二階段,把第一階段中的「/work/build」目錄複製到「/」根目錄,最後設定其ENTRYPOINT,只能執行「/app/application」,並加上必要參數。copy_ldd.sh腳本程式內容如下:

#!/bin/bash if [ $# != 2 ] ; then     echo "usage $0 PATH_TO_BINARY     TARGET_FOLDER"     exit 1 fi   PATH_TO_BINARY="$1" TARGET_FOLDER="$2" # if we cannot find the the binary # we have to abort if [ ! -f "$PATH_TO_BINARY" ] ; then    echo "The file '$PATH_TO_ BINARY' was not found. Aborting!"     exit 1 fi   # copy the binary to the target folder # create directories if required echo "---> copy binary itself" mkdir -p "$TARGET_FOLDER"/app cp --parents -v "$PATH_TO_BINARY" "$TARGET_FOLDER"/app   # copy the required shared libs to # the target folder # create directories if required echo "---> copy libraries" for lib in `ldd "$PATH_TO_BINARY" | cut -d'>' -f2 | awk '{print $1}' | grep "/"` ; do    if [ -f "$lib" ] ; then   cp -v --parents "$lib"    "$TARGET_FOLDER"    fi done echo "Done!!!"

當然,關鍵還是「copy_ldd.sh」腳本程式是如何運作?第一先判斷是否有兩個參數,$1是二進制執行檔的名稱,$2是運作目錄,前面已經將XXX-runner複製成application,並宣告使用「build」目錄,也就是執行「./copy_ldd.sh application build」。

接著,檢查是否有運作目錄「build」,並建立「build/app」子目錄,並複製application到「build/app」目錄中,最重要是使用ldd指令來分析此application執行檔相依哪些Linux動態函式庫,並依照其ubi-minimal之位置複製到「build」目錄下,這裡使用「ldd quarkus-getting-started-1.0.0-SNAPSHOT-runner」,執行結果如圖17所示,藉由這樣分析方式,可以得到正常運行且只需必要的程式和動態函式庫。

圖17  ldd分析結果。

將Dockerfile放置在「src/main/docker」目錄內,把*copy_ldd.sh*到在專案目錄,執行「docker build -f src/main/docker/Dockerfile -t philipz/quarkus-minimal .」,就可建置出最小化Quarkus範例程式之容器映像檔,建置過程如圖18所示。

圖18  最小化Quarkus容器映像檔建置過程。

當然,還須驗證是否能夠運行。執行「docker run -d -p 8080:8080 philipz/quarkus-minimal」,並比較三種建置方式之容器映像檔尺寸,quarkus-native為113MB,quarkus-minimal卻只有29.4MB,如圖19所示。

圖19  查看三種建置方式之映像檔,並驗證其功能。

為了證明此quarkus-minimal映像檔的內容,使用dive分析工具(https://github.com/wagoodman/dive)來查看映像檔的檔案結構,從圖20中可以看出,其映像檔之檔案與圖17的ldd分析結果是一致的,只有「linux-vdso.so.1」屬於Kernel所提供的,在啟動時載入到記憶體,所以無須也無法放入映像檔中。

圖20  以dive分析quarkus-minimal映像檔。

結語

Red Hat除了深耕容器技術和OpenShift容器管理平台外,逐漸朝向Java開發框架延伸,因為目前主流的Java微服務框架Spring Boot,其背後支持的公司Pivotal也推出容器管理平台Pivotal Container Service(PKS),加上被VMware收購,借重VMware現有IaaS虛擬化的市占,必然會成為Red Hat OpenShift的最大對手,因此培植自身的Java開發框架,除了可用於容器平台並因應未來Serverless無伺服器之發展趨勢外,便是Quarkus框架之戰略意義,從「Introducing Quarkus: a next-generation Kubernetes native Java framework」(https://developers.redhat.com/blog/2019/03/07/quarkus-next-generation-kubernetes-native-java-framework/)一文就可明白其目的。

<本文作者:鄭淳尹,Docker.Taipei社群共同發起人,國泰金控技術架構師,曾任微軟MVP、momo購物網架構師、臺北榮民總醫院資訊工程師、玉山銀行資訊處專員、宏碁eDC維運工程師,系統維護及開發設計超過16年。開源技術愛好者,曾在多間大學資工系擔任Docker容器技術講師,並翻譯審閱多本容器技術書籍。>

 


追蹤我們Featrue us

本站使用cookie及相關技術分析來改善使用者體驗。瞭解更多

我知道了!