1. 云原生時代的Java應(yīng)用優(yōu)化實踐

        共 9536字,需瀏覽 20分鐘

         ·

        2022-03-19 05:00

        點擊上方“服務(wù)端思維”,選擇“設(shè)為星標(biāo)

        回復(fù)”669“獲取獨家整理的精選資料集

        回復(fù)”加群“加入全國服務(wù)端高端社群「后端圈」


        作者 | 于善游
        出品?| 騰訊云中間件


        導(dǎo)語


        Java從誕生至今已經(jīng)走過了26年,在這26年的時間里,Java應(yīng)用從未停下腳步,從最開始的單機(jī)版到web應(yīng)用再到現(xiàn)在的微服務(wù)應(yīng)用,依靠其強(qiáng)大的生態(tài),它仍然占據(jù)著當(dāng)今語言之爭的“天下第一”的寶座。但在如今的云原生serverless時代,Java應(yīng)用卻遭遇到了前所未有的挑戰(zhàn)。


        在云原生時代,云原生技術(shù)利用各種公有云、私有云和混合云等新型動態(tài)環(huán)境,構(gòu)建和運行可彈性擴(kuò)展的應(yīng)用。而我們應(yīng)用也越來越呈現(xiàn)出以下特點:


        • 基于容器鏡像構(gòu)建


        Java誕生之初,靠著“一次編譯,到處運行”的口號,以語言層虛擬化的方式,在那個操作系統(tǒng)平臺尚不統(tǒng)一的年代,建立起了優(yōu)勢。但如今步入云原生時代,以Docker為首的容器技術(shù)同樣提出了“一次構(gòu)建,到處運行”的口號,通過操作系統(tǒng)虛擬化的方式,為應(yīng)用程序提供了環(huán)境兼容性和平臺無關(guān)性。因此,在云原生時代的今天,Java“一次編譯,到處運行”的優(yōu)勢,已經(jīng)被容器技術(shù)大幅度地削弱,不再是大多數(shù)服務(wù)端開發(fā)者技術(shù)選型的主要考慮因素了。此外,因為是基于鏡像,云原生時代對鏡像大小可以說是十分敏感,而包含了JDK的Java應(yīng)用動輒幾百兆的鏡像大小,無疑是越來越不符合時代的要求。


        • 生命周期縮短,并經(jīng)常需要彈性擴(kuò)縮容


        靈活和彈性可以說是云原生應(yīng)用的一個顯著特性,而這也意味著應(yīng)用需要具備更短的冷啟動時間,以應(yīng)對靈活彈性的要求。Java應(yīng)用往往面向長時間大規(guī)模程序而設(shè)計,JVM的JIT和分層編譯優(yōu)化技術(shù),會使得Java應(yīng)用在不斷的運行中進(jìn)行自我優(yōu)化,并在一段時間后達(dá)到性能頂峰。但與運行性能相反,Java應(yīng)用往往有著緩慢的啟動時間。流行的框架(例如Spring)中大量的類加載、字節(jié)碼增強(qiáng)和初始化邏輯,更是加重了這一問題。這無疑是與云原生時代的理念是相悖的。


        • 對計算資源用量敏感


        進(jìn)入公有云時代,應(yīng)用往往是按用量付費,這意味著應(yīng)用所需要的計算資源就變的十分重要。Java應(yīng)用固有的內(nèi)存占用多的劣勢,在云原生時代被放大,相對于其他語言,使用起來變得更加“昂貴”。


        由此可見,在云原生時代,Java應(yīng)用的優(yōu)勢正在不斷被蠶食,而劣勢卻在不斷的被放大。因此,如何讓我們的應(yīng)用更加順應(yīng)時代的發(fā)展,使Java語言能在云原生時代發(fā)揮更大的價值,就成了一個值得探討的話題。為此,筆者將嘗試跳出語言對比的固有思路,為大家從一個更全局的角度,來看看在云原生應(yīng)用發(fā)布的全流程中,我們都能夠做哪些優(yōu)化。


        鏡像構(gòu)建優(yōu)化


        Dockerfile


        從Dockerfile說起是因為它是最基礎(chǔ)的,也是最簡單的優(yōu)化,它可以簡單的加快我們的應(yīng)用構(gòu)建鏡像和拉取鏡像的時間。


        以一個Springboot應(yīng)用為例,我們通常會看到這種樣子的Dockerfile:


        FROM?openjdk:8-jdk-alpineCOPY app.jar /ENTRYPOINT?["java","-jar","/app.jar"]



        足夠簡單清晰,但很顯然,這并不是一個很好的Dockerfile,因為它沒有利用到Image layer去進(jìn)行效率更高的緩存。


        我們都知道,Docker擁有足夠高效的緩存機(jī)制,但如果不好好的應(yīng)用這一特性,而是簡單的將Jar包打成單一layer鏡像,就會導(dǎo)致,即使應(yīng)用只改動一行代碼,我們也需要重新構(gòu)建整個Springboot Jar包,而這其中Spring的龐大依賴類庫其實都沒有發(fā)生過更改,這無疑是一種得不償失的做法。因此,將應(yīng)用的所有依賴庫作為一個單獨的layer顯然是一個更好的方案。


        因此,一個更合理的Dockerfile應(yīng)該長這個樣子:


        FROM?openjdk:8-jdk-alpineARG DEPENDENCY=target/dependencyCOPY ${DEPENDENCY}/BOOT-INF/lib /app/libCOPY ${DEPENDENCY}/META-INF /app/META-INFCOPY ${DEPENDENCY}/BOOT-INF/classes /appENTRYPOINT?["java","-cp","app:app/lib/*","HelloApplication"]



        這樣,我們就可以充分利用Image layer cache來加快構(gòu)建鏡像和拉取鏡像的時間。


        構(gòu)建組件


        在Docker占有鏡像構(gòu)建的絕對話語權(quán)的今天,我們在實際開發(fā)過程中,往往會忽視構(gòu)建組件的選擇,但事實上,選擇一個高效的構(gòu)建組件,往往能使我們的構(gòu)建效率事半功倍。


        傳統(tǒng)的“docker build”存在哪些問題?


        在Docker v18.06之前的`docker build`會存在一些問題:


        • 改變Dockerfile中的任意一行,就會使之后的所有行的緩存失效

        #?假設(shè)只改變此Dockerfile中的EXPOSE端口號# 那么接下來的RUN命令的緩存就會失效FROM debianEXPOSE 80RUN?apt?update?&&?apt?install?–y?HEAVY-PACKAGES



        • 多階段并行構(gòu)建效率不佳


        # 即使stage0和stage1之間并沒有依賴# docker也無法并行構(gòu)建,而是選擇串行FROM openjdk:8-jdk AS stage0RUN ./gradlew clean build

        FROM openjdk:8-jdk AS stage1RUN ./gradlew clean build

        FROM openjdk:8-jdk-alpineCOPY --from=stage0 /app-0.jar /COPY?--from=stage1?/app-1.jar?/



        • 無法提供編譯歷史緩存

        #?單純的RUN命令無法提供編譯歷史緩存# 而RUN --mount的新語法在舊版本docker下無法支持RUN ./gradlew build# since Docker v18.06# syntax = docker/dockerfile:1.1-experimentalRUN?--mount=type=cache,target=/.cache?./gradlew?build



        • 鏡像push和pull的過程中存在壓縮和解壓的固有耗時

        如上圖所示,在傳統(tǒng)的docker pull push階段,存在著pack和unpack的耗時,而這一部分并非必須的。針對這些固有的弊病,業(yè)界也一直在積極的探討,并誕生了一些可以順應(yīng)新時代的構(gòu)建工具。


        新一代構(gòu)建組件:


        在最佳的新一代構(gòu)建工具選擇上,是一個沒有銀彈的話題,但通過一些簡單的對比,我們?nèi)阅苓x出一個最適合的構(gòu)建工具,我們認(rèn)為,一個適合云原生平臺的構(gòu)建工具應(yīng)該至少具備以下幾個特點:


        • 能夠支持完整的Dockerfile語法,以便應(yīng)用平順遷移;

        • 能夠彌補(bǔ)上述傳統(tǒng)Docker構(gòu)建的缺點;

        • 能夠在非root privilege模式下執(zhí)行(在基于Kubernetes的CICD環(huán)境中顯得尤為重要)。


        因此,Buildkit就脫穎而出,這個由Docker公司開發(fā),目前由社區(qū)和Docker公司合理維護(hù)的“含著金鑰匙出生”的新一代構(gòu)建工具,擁有良好的擴(kuò)展性、極大地提高了構(gòu)建速度,并提供了更好的安全性。Buildkit支持全部的Dockerfile語法,能更高效的命中構(gòu)建緩存,增量的轉(zhuǎn)發(fā)build context,多并發(fā)直接推送鏡像層至鏡像倉庫。

        Buildkit與其他構(gòu)建組件的對比

        Buildkit的構(gòu)建效率


        鏡像大小


        為了在拉取和推送鏡像過程中更高的控制耗時,我們通常會盡可能的減少鏡像的大小。


        Alpine Linux是許多Docker容器首選的基礎(chǔ)鏡像,因為它只有5 MB大小,比起其他Cent OS、Debain 等動輒一百多MB的發(fā)行版來說,更適合用于容器環(huán)境。不過Alpine Linux為了盡量瘦身,默認(rèn)是用musl作為C標(biāo)準(zhǔn)庫的,而非傳統(tǒng)的glibc(GNU C library),因此要以Alpine Linux為基礎(chǔ)制作OpenJDK鏡像,必須先安裝glibc,此時基礎(chǔ)鏡像大約有12 MB。


        在【JEP 386】(http://openjdk.java.net/jeps/386)中,OpenJDK將上游代碼移植到musl,并通過兼容性測試。這一特性已經(jīng)在Java 16中發(fā)布。這樣制作出來的鏡像僅有41MB,不僅遠(yuǎn)低于Cent OS的OpenJDK(大約 396 MB),也要比官方的slim版(約200MB)要小得多。


        應(yīng)用啟動加速


        讓我們首先來看一下,一個Java應(yīng)用在啟動過程中,會有哪些階段。

        這個圖代表了Java運行時各個階段的生命周期,可以看到它要經(jīng)過五個階段,首先是VM init虛擬機(jī)的初始化階段,然后是App init應(yīng)用的初始化階段,再經(jīng)過App active(warmup)的應(yīng)用預(yù)熱時期,在預(yù)熱一段時間后進(jìn)入App active(steady)達(dá)到性能巔峰期,最后應(yīng)用結(jié)束完成整個生命周期。


        使用AppCDS

        從上面的圖中,我們不難發(fā)現(xiàn),藍(lán)色的CL(ClassLoad)部分,實際長占用了Java應(yīng)用啟動的階段的一大部分時間。而Java也一直在致力于減少應(yīng)用啟動的ClassLoad時間。


        從JDK 1.5開始,HotSpot就提供了CDS(Class Data Sharing)功能,很長一段時間以來,它的功能都非常有限,并且只有部分商業(yè)化。早期的CDS致力于,在同一主機(jī)上的JVM實例之間“共享”同樣需要加載一次的類,但是遺憾的是早期的CDS不能處理由AppClassloader加載的類,這使得它在實際開發(fā)實踐中,顯得比較“雞肋”。


        但在從OpenJDK 10 (2018) 開始,AppCDS【JEP 310】(https://openjdk.java.net/jeps/310)在CDS的基礎(chǔ)上,加入了對AppClassloader的適配,它的出現(xiàn),使得CDS技術(shù)變得廣泛可用并且更加適用。尤其是對于動輒需要加載數(shù)千個類的Spring Boot程序,因為JVM不需要在每個實例的每次啟動時加載(解析和驗證)這些類,因此,啟動應(yīng)該變得更快并且內(nèi)存占用應(yīng)該更小??雌饋恚珹ppCDS的一切都很美好,但實際使用也確實如此嗎?

        當(dāng)我們試圖使用AppCDS時,它應(yīng)該包含以下幾個步驟:


        • 使用`-XX:DumpLoadedClassList`參數(shù)來獲取我們希望在應(yīng)用程序?qū)嵗g共享的類;

        • 使用`-Xshare:dump`參數(shù)將類存儲到適合內(nèi)存映射的存檔(.jsa文件)中;

        • 使用`-Xshare:on`參數(shù)在啟動時將存檔附加到每個應(yīng)用程序?qū)嵗?/p>


        乍一看,使用AppCDS似乎很容易,只需3個簡單的步驟。但是,在實際使用過程中,你會發(fā)現(xiàn)每一步都可能變成一次帶有特定JVM Options的應(yīng)用啟動,我們無法簡單的通過一次啟動來獲得可重復(fù)使用的類加載存檔文件。盡管在JDK 13中,提供了新的動態(tài)CDS【JEP 350】(https://openjdk.java.net/jeps/350),來將上述步驟1和步驟2合并為一步。但在目前流行的JDK 11中,我們?nèi)匀惶硬婚_上述三個步驟(三次啟動)。因此,使用AppCDS往往意味著對應(yīng)用的啟動過程進(jìn)行復(fù)雜的改造,并伴隨著更為漫長的首次編譯和啟動時間。


        同時需要注意的是,在使用AppCDS時,許多應(yīng)用的類路徑將會變得更加混亂:它們既位于原來的位置(JAR包)中,同時又位于新的共享存檔(.jsa文件)中。在我們應(yīng)用開發(fā)的過程中,我們會不斷更改、刪除原來的類,而JVM會從新的類中進(jìn)行解析。這種情況所帶來的危險是顯而易見的:如果類歸檔文件保持不變,那么類不匹配是遲早的事,我們會遇到典型的“Classpath Hell”問題。


        JVM無法阻止類的變化,但它至少應(yīng)該能夠在適當(dāng)?shù)臅r候檢測到類不匹配。然而,在JVM的實現(xiàn)中,并沒有檢測每一個單獨的類,而是選擇去比較整個類路徑,因此,在AppCDS的官方描述中,我們可以找到這樣一句話:


        The classpath used with -Xshare:dump must be the same as, or be a prefix of, the classpath used with -Xshare:on. Otherwise, the JVM will print an error message


        即第二部步歸檔文件創(chuàng)建時使用的類路徑必須與運行時使用的類路徑相同(或前者是后者的前綴)。


        但這是一個相當(dāng)含糊的陳述,因為類路徑可能以幾種不同的方式形成,例如:


        • 從帶有Jar包的目錄中直接加載.class文件,例如`java com.example.Main`;

        • 使用通配符,掃描帶有Jar包的目錄,例如`java -cp mydir/* com.example.Main`;

        • 使用明確的Jar包路徑,例如`java -cp lib1.jar:lib2.jar com.example.Main`。


        在這些方式中,AppCDS唯一支持的方式只有第三種,即是顯式列出Jar包路徑。這使得那些使用了大規(guī)模Jar包依賴的應(yīng)用的啟動語句變得十分繁瑣。


        同時,我們也要必須注意到,這種顯式列出Jar包路徑的方式并不會進(jìn)行遞歸查找,即它只會在包含所有class文件的FatJar中生效。這意味著使用SpringBoot框架的嵌套Jar包結(jié)構(gòu),將很難利用AppCDS技術(shù)所帶來的便利。


        因此,SpringBoot如果想在云原生環(huán)境中使用AppCDS,就必須進(jìn)行應(yīng)用侵入性的改造,不去使用SpringBoot默認(rèn)的嵌套Jar啟動結(jié)構(gòu),而是用類似【maven shade plugin】(https://maven.apache.org/plugins/maven-shade-plugin/)重新打FatJar,并在程序中顯示的聲明能讓程序自然關(guān)閉的接口或參數(shù),通過Volume掛載或者Dockerfile改造的方式,來存儲和加載類的歸檔文件。這里給出一個改造過的Dockerfile的示例:


        # 這里假設(shè)我們已經(jīng)做過FatJar改造,并且Jar包中包含應(yīng)用運行所需的全部class文件FROM eclipse-temurin:11-jre as APPCDS

        COPY target/helloworld.jar /helloworld.jar

        # 運行應(yīng)用,同時設(shè)置一個'--appcds'參數(shù)使程序在運行后能夠停止RUN java -XX:DumpLoadedClassList=classes.lst -jar helloworld.jar --appcds=true

        # 使用上一步得到的class列表來生成類歸檔文件RUN java -Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=appcds.jsa --class-path helloworld.jar

        FROM eclipse-temurin:11-jre

        # 同時復(fù)制Jar包和類歸檔文件COPY --from=APPCDS /helloworld.jar /helloworld.jarCOPY --from=APPCDS /appcds.jsa /appcds.jsa

        # 使用-Xshare:on參數(shù)來啟動應(yīng)用ENTRYPOINT?java?-Xshare:on?-XX:SharedArchiveFile=appcds.jsa?-jar?helloworld.jar



        由此可見,使用AppCDS還是要付出相當(dāng)多的學(xué)習(xí)和改造成本的,并且許多改造都會對我們的應(yīng)用產(chǎn)生入侵。


        JVM優(yōu)化


        除了構(gòu)建階段和啟動階段,我們還可以從JVM本身入手,根據(jù)云原生環(huán)境的特點,進(jìn)行針對性的優(yōu)化。


        使用可以感知容器內(nèi)存資源的JDK


        在虛擬機(jī)和物理機(jī)中,對于 CPU 和內(nèi)存分配,JVM會從常見位置(例如,Linux 中的`/proc/cpuinfo`和`/proc/meminfo`)查找其可以使用的CPU和內(nèi)存。但是,在容器中運行時,CPU和內(nèi)存限制條件存儲在`/proc/cgroups/...`中。較舊版本的JDK會繼續(xù)在`/proc`(而不是`/proc/cgroups`)中查找,這可能會導(dǎo)致CPU和內(nèi)存用量超出分配的上限,并因此引發(fā)多種嚴(yán)重的問題:


        • 線程過多,因為線程池大小由`Runtime.availableProcessors()`配置;

        • JVM的對內(nèi)存使用超出容器內(nèi)存上限。并導(dǎo)致容器被OOMKilled。


        JDK 8u131首先實現(xiàn)了`UseCGroupMemoryLimitForHeap`的參數(shù)。但這個參數(shù)存在缺陷,為應(yīng)用添加`UnlockExperimentalVMOptions`和`UseCGroupMemoryLimitForHeap`參數(shù)后,JVM確實可以感知到容器內(nèi)存,并控制應(yīng)用的實際堆大小。但是這并沒有充分利用我們?yōu)槿萜鞣峙涞膬?nèi)存。


        因此JVM提供`-XX:MaxRAMFraction`標(biāo)志來幫助更好的計算堆大小,`MaxRAMFraction`默認(rèn)值是4(即除以4),但它是一個分?jǐn)?shù),而不是一個百分比,因此很難設(shè)置一個能有效利用可用內(nèi)存的值。


        JDK 10附帶了對容器環(huán)境的更好支持。如果在Linux容器中運行Java應(yīng)用程序,JVM將使用`UseContainerSupport`選項自動檢測內(nèi)存限制。然后,通過`InitialRAMPercentage`、`MaxRAMPercentage`和`MinRAMPercentage`來進(jìn)行對內(nèi)存控制。這時,我們使用的是百分比而不是分?jǐn)?shù),這將更加準(zhǔn)確。


        默認(rèn)情況下,`UseContainerSupport`參數(shù)是激活的,`MaxRAMPercentage`是25%,`MinRAMPercentage`是50%。


        需要注意的是,這里`MinRAMPercentage`并不是用來設(shè)置堆大小的最小值,而是僅當(dāng)物理服務(wù)器(或容器)中的總可用內(nèi)存小于250MB時,JVM將用此參數(shù)來限制堆的大小。


        同理,`MaxRAMPercentage`是當(dāng)物理服務(wù)器(或容器)中的總可用內(nèi)存大小超過250MB時,JVM將用此參數(shù)來限制堆的大小。


        這幾個參數(shù)已經(jīng)向下移植到JDK 8u191。UseContainerSupport默認(rèn)情況下是激活的。我們可以設(shè)置`-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=80.0`來JVM感知并充分利用容器的可用內(nèi)存。需要注意的是,在指定`-Xms -Xmx`時,`InitialRAMPercentage`和`MaxRAMPercentage`將會失效。


        關(guān)閉優(yōu)化編譯器


        默認(rèn)情況下,JVM有多個階段的JIT編譯。雖然這些階段可以逐漸提高應(yīng)用的效率,但它們也會增加內(nèi)存使用的開銷,并增加啟動時間。


        對于短期運行的云原生應(yīng)用,可以考慮使用以下參數(shù)來關(guān)閉優(yōu)化階段,以犧牲長期運行效率來換取更短的啟動時間。


        `JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"`


        關(guān)閉類驗證


        當(dāng)JVM將類加載到內(nèi)存中以供執(zhí)行時,它會驗證該類未被篡改并且沒有惡意修改或損壞。但在云原生環(huán)境,CI/CD流水線通常也由云原生平臺提供,這表示我們的應(yīng)用的編譯和部署是可信的,因此我們應(yīng)該考慮使用以下參數(shù)關(guān)閉驗證。如果在啟動時加載大量類,則關(guān)閉驗證可能會提高啟動速度。


        `JAVA_TOOL_OPTIONS="-noverify"`


        減小線程棧大小


        大多數(shù)Java Web應(yīng)用都是基于每個連接一個線程的模式。每個Java線程都會消耗本機(jī)內(nèi)存(而不是堆內(nèi)存)。這稱為線程棧,并且每個線程默認(rèn)為1 MB。如果您的應(yīng)用處理100個并發(fā)請求,則它可能至少有100個線程,這相當(dāng)于使用了100MB的線程棧空間。該內(nèi)存不計入堆大小。我們可以使用以下參數(shù)來減小線程棧大小。


        `JAVA_TOOL_OPTIONS="-Xss256k"`


        需要注意如果減小得太多,則將出現(xiàn)`java.lang.StackOverflowError`。您可以對應(yīng)用進(jìn)行分析,并找到要配置的最佳線程棧大小。


        使用TEM進(jìn)行零改造的Java應(yīng)用云原生優(yōu)化


        通過上面的分析,我們可以看出,如果想要讓我們的Java應(yīng)用能在云原生時代發(fā)揮出最大實力,是需要付出許多侵入性的改造和優(yōu)化操作的。那么有沒有一種方式能夠幫助我們零改造的開展Java應(yīng)用云原生優(yōu)化?


        騰訊云的TEM彈性微服務(wù)就為廣大Java開發(fā)者提供了一種應(yīng)用零改造的最佳實踐,幫助您的Java應(yīng)用以最優(yōu)姿態(tài)快速上云。使用TEM您可以享受的以下優(yōu)勢:


        • ?零構(gòu)建部署

        直接選擇使用Jar包/War包交付,無需自行構(gòu)建鏡像。TEM默認(rèn)提供能充分利用構(gòu)建緩存的構(gòu)建流程,使用新一代構(gòu)建利器Buildkit進(jìn)行高速構(gòu)建,構(gòu)建速度優(yōu)化50%以上,并且整個構(gòu)建流程可追溯,構(gòu)建日志可查,簡單高效。

        直接使用Jar包部署

        構(gòu)建日志可查

        構(gòu)建速度對比


        • 零改造加速

        直接使用KONA Jdk 11/Open Jdk 11進(jìn)行應(yīng)用加速,并且默認(rèn)支持SpringBoot應(yīng)用零改造加速。您無需改造原有的SpringBoot嵌套Jar包結(jié)構(gòu),TEM將直接提供Java應(yīng)用加速的最佳實踐,實例擴(kuò)容時的啟動時間將縮短至10%~40%。

        不使用應(yīng)用加速,規(guī)格1c2g

        使用應(yīng)用加速,規(guī)格1c2g

        應(yīng)用啟動速度對比

        以[spring petclinic](https://github.com/spring-projects/spring-petclinic)為例,規(guī)格1c2g


        • 零運維監(jiān)控

        使用SkyWalking為您的Java應(yīng)用進(jìn)行應(yīng)用級別的監(jiān)控,您可以直觀的查看JVM堆內(nèi)存,GC次數(shù)/耗時,接口RT/QPS等關(guān)鍵參數(shù),幫助您即使找到應(yīng)用性能瓶頸。

        應(yīng)用JVM監(jiān)控

        • 極致彈性

        TEM默認(rèn)提供使用率較高的定時彈性策略和基于資源的彈性策略,為您的應(yīng)用提供秒級的彈性性能,幫助您應(yīng)對流量洪峰,并能在實例閑置時及時節(jié)省資源。

        指標(biāo)彈性策略

        定時彈性策略



        總結(jié)


        工欲善其事,必先利其器。在步入云原生時代的今天,如何讓您的Java應(yīng)用的部署效率和運行性能最大化,這對所有開發(fā)者都是一個挑戰(zhàn)。而TEM作為一款面向微服務(wù)應(yīng)用的Serverless PaaS平臺,將成為您手中的“云端利器”,TEM將致力于為企業(yè)和開發(fā)者服務(wù),幫助您的業(yè)務(wù)以最快速、便捷、省心的姿態(tài),無憂上云,享受云原生時代的便利。



        — 本文結(jié)束 —


        ●?漫談設(shè)計模式在 Spring 框架中的良好實踐

        ●?顛覆微服務(wù)認(rèn)知:深入思考微服務(wù)的七個主流觀點

        ●?人人都是 API 設(shè)計者

        ●?一文講透微服務(wù)下如何保證事務(wù)的一致性

        ●?要黑盒測試微服務(wù)內(nèi)部服務(wù)間調(diào)用,我該如何實現(xiàn)?



        關(guān)注我,回復(fù) 「加群」 加入各種主題討論群。



        對「服務(wù)端思維」有期待,請在文末點個在看

        喜歡這篇文章,歡迎轉(zhuǎn)發(fā)、分享朋友圈


        在看點這里


        瀏覽 55
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 波多野结衣被夫上司持续 | 非洲人与性动交CCZZ | 欧美大波大乳巨大乳 | 美女毛茸茸的阴户视频 | 他解开女警的乳罩慢慢揉李清 |