最近,我所在的團(tuán)隊(duì)面臨著部署微服務(wù)(Java+SpringMVC in Docker on AWS)的問題。主要問題是,很多非常輕巧的應(yīng)用程序消耗了太多的內(nèi)存。因此,我們經(jīng)過多方嘗試找到了在Docker中關(guān)于Java內(nèi)存消耗的問題,并通過重構(gòu)和遷移到Spring Boot實(shí)現(xiàn)了減少消耗的方法。本文,我將和大家分享這整個(gè)過程,希望能夠?qū)Υ蠹矣兴鶐椭?/p>
在部署之前,我們要估計(jì)應(yīng)用程序消耗多少內(nèi)存。為此,我們制定了一個(gè)清晰簡(jiǎn)單的方程來找到RSS:
RSS = Heap size + MetaSpace + OffHeap size
這里OffHeap由線程堆棧,緩沖區(qū),庫(* .jars)和JVM代碼組成。
Resident Set Size是當(dāng)前分配給進(jìn)程使用的RAM的數(shù)量。它包括代碼、數(shù)據(jù)和共享庫。
讓我們根據(jù)本地Java VisualVM值找到它:
RSS = 253(Heap) + 100(Metaspace) + 170(OffHeap) + 52*1(Threads) = 600Mb (max avarage)
所以,我們得出結(jié)果:大概600Mb就夠了。我們選擇了一個(gè)t2.micro AWS實(shí)例(具有1Gb RAM)進(jìn)行部署,并開始部署應(yīng)用程序。首先,通過JVM選項(xiàng)提供有關(guān)內(nèi)存配置的一些信息:
此外,作為應(yīng)用程序的基礎(chǔ)圖像,我們選擇了 ,因?yàn)槲覀儼l(fā)現(xiàn)它是 Jetty Java *.wars中最輕量級(jí)的圖像之一。
正如前文所提到的600Mb就足夠了,所以啟動(dòng)了一個(gè)容器,其容量如下:
docker run -m 600m
那么你覺得這樣會(huì)發(fā)生什么?我們的集裝箱會(huì)由于內(nèi)存不足被DD(Docker daemon)殺死。這個(gè)容器是在本地啟動(dòng)的,具有完全相同的參數(shù),通過逐步增加容器的內(nèi)存限制,我們達(dá)到了700…我開玩笑的,我們有850mb。
經(jīng)過觀察和閱讀有用的文章,我們決定進(jìn)行測(cè)量。結(jié)果是非常奇怪和有爭(zhēng)議的。
Heap Size與我們本地相同:
但Docker顯示了一些瘋狂的統(tǒng)計(jì)數(shù)據(jù):
發(fā)生了什么事?情況變得非?;靵y...
我們花了很多時(shí)間去搞清楚這些數(shù)據(jù)為什么會(huì)有爭(zhēng)議,在搜索過程中發(fā)現(xiàn)我們并不是個(gè)例。在查找了很多資料,分析了 應(yīng)用之后,我們幾乎找到了答案。大部分額外的內(nèi)存都是被用于存儲(chǔ)編譯的類及其元數(shù)據(jù)。
那么有關(guān)JavaVM / Docker統(tǒng)計(jì)數(shù)據(jù)的爭(zhēng)議數(shù)字呢?事實(shí)證明,Java VisualVM不了解OffHeap的內(nèi)容,因此,使用此工具調(diào)查Java應(yīng)用程序的內(nèi)存消耗會(huì)非常棘手。此外,了解您使用的JVM選項(xiàng)也是至關(guān)重要。在我看來,指定-Xmx=512m告訴JVN分配512mb Heap,但是它并不告訴JVM整個(gè)內(nèi)存使用要限制在512mb,所以就會(huì)出現(xiàn)代碼緩存和其它非Heap數(shù)據(jù)占用內(nèi)存直至超過。所以,要指定總內(nèi)存需要使用XX:MaxRAM參數(shù)。請(qǐng)注意,使用MaxRam = 512m,你的Heap約為250mb,這時(shí)就要注意應(yīng)用程序JVM選項(xiàng)。
星期一早上,尋找靈丹妙藥...
NMT和Java VisualVM Memory Sampler 可以幫助我們發(fā)現(xiàn)內(nèi)部核心框架在內(nèi)存中多次重寫的依賴,并且重復(fù)的數(shù)量等于微服務(wù)中子模塊的數(shù)量。如果想要更好的掌握這一點(diǎn),首先要說明一下我們的“微服務(wù)”結(jié)構(gòu):
這是NMT(在本地機(jī)器上)的一個(gè)模塊快照(具有73MB加載的類元數(shù)據(jù),42MB線程和37MB的代碼,包括lib):
據(jù)我所知,構(gòu)建這樣的應(yīng)用程序是一個(gè)很大的錯(cuò)誤。首先,每個(gè)* .war已經(jīng)作為一個(gè)單獨(dú)的應(yīng)用程序部署在Jetty servlet容器中,這是非常奇怪的。因?yàn)楦鶕?jù)定義,微服務(wù)應(yīng)該是一個(gè)部署應(yīng)用程序(部署單元)。其次,Jetty在內(nèi)存中分別保存每個(gè)* .war的所有必需庫,即使所有這些庫具有相同的版本。因此,DB連接、核心框架的各種基本功能等都會(huì)在內(nèi)存中重復(fù)。
一般的解決方案就是重構(gòu),讓應(yīng)用成為真正的微服務(wù)器。此外,我們可能還需要一整套的Jetty,但是一般人聽到它的報(bào)價(jià)就能望而卻步。圈內(nèi)有句著名的評(píng)論“不要在Jetty中部署應(yīng)用程序,而是要在應(yīng)用程序中部署Jetty。
我們決定嘗試使用嵌入式Jetty的Spring Boot,因?yàn)樗仟?dú)立應(yīng)用程序中最常用的工具,尤其是在我們的案例中。配置很少,沒有XML,每個(gè)Spring框架都有優(yōu)勢(shì),還有很多插件,自動(dòng)配置。除此之外,網(wǎng)上還有大量的實(shí)用教程和文章,這些都讓Spring Boot看起來是個(gè)不錯(cuò)的選擇。
由于我們不再需要單獨(dú)的Jetty應(yīng)用程序服務(wù)器,所以我們將基礎(chǔ)Docker的圖像更改為輕量級(jí)的openjdk。
openjdk:8-jre-alpine
根據(jù)新的要求重構(gòu)了應(yīng)用程序之后,我們得到了類似下圖的東西:
現(xiàn)在一切都準(zhǔn)備好了,我們來測(cè)試一下。
先從Java VirtualVM進(jìn)行測(cè)試:
從數(shù)據(jù)中可以看出,雖然新版本有了一些改進(jìn),但是與之前的應(yīng)用程序相比變化并沒有那么大:
然后,我們?cè)賮砜纯碊ocker的統(tǒng)計(jì)數(shù)據(jù):
結(jié)果很明顯,我們已經(jīng)減少了內(nèi)存消耗。
結(jié)論
對(duì)我們團(tuán)隊(duì)來說,這是一個(gè)很有意思的挑戰(zhàn)。找到導(dǎo)致錯(cuò)誤或者某件事情的根本原因,會(huì)讓你在特定領(lǐng)域中看得更深更廣。網(wǎng)上的資源非常豐富,如果你下定決心去查找答案,那么就一定會(huì)找得到。另外,從這次事件中也認(rèn)識(shí)到,不要完全信任Java VisualVM的內(nèi)存消耗預(yù)計(jì)。
對(duì)于技術(shù)的學(xué)習(xí)和性能的提高,我有三個(gè)更多和大家分享,閱讀更多,改進(jìn)更多,調(diào)查更多。另外,避免常規(guī),盡可能自動(dòng)化,這樣你可以更有效的進(jìn)行工作。
本文對(duì)你是否有幫助?歡迎在下方留言評(píng)論或者反饋您遇到的問題。