自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

提升內(nèi)存管理效率,攜程酒店查詢服務(wù)輕量化探索和實(shí)踐

開發(fā) 新聞
本文相對(duì)完整的記述了酒店查詢服務(wù)在輕量化中的一次優(yōu)化過程,希望其中的經(jīng)驗(yàn)和過程能對(duì)讀者有所幫助。

作者簡(jiǎn)介

NekoMatryoshka,攜程酒店資深后端開發(fā)工程師,主要工作是緩存類組件的開發(fā)維護(hù),并對(duì)業(yè)務(wù)應(yīng)用的排障和優(yōu)化有所關(guān)注。

一、背景和目標(biāo)

在容器化部署成為主流的現(xiàn)在,降低集群中單個(gè)容器的資源需求的意義已經(jīng)不只限于更少的硬件成本,同時(shí)也意味著整個(gè)集群更加輕量化,這通常會(huì)帶來一系列其他優(yōu)勢(shì):例如更短的恢復(fù)時(shí)間,更精確的資源控制和調(diào)度,和更快速的伸縮和部署等。但在另一方面,一味的追求壓縮容器配置必然會(huì)嚴(yán)重影響應(yīng)用在穩(wěn)定性、響應(yīng)耗時(shí)和吞吐量等方面的表現(xiàn),所以輕量化的措施需要在多個(gè)性能維度上進(jìn)行仔細(xì)的權(quán)衡取舍,以達(dá)到一個(gè)總體更優(yōu)的結(jié)果。

作為攜程計(jì)算量最大的接口之一,酒店查詢服務(wù)一直承擔(dān)著沉重的硬件成本壓力,僅僅詳情頁(yè)集群就包含了千余臺(tái)服務(wù)器實(shí)例和數(shù)十TB的Redis資源,因此對(duì)應(yīng)用進(jìn)行全面的輕量化有著很高的必要性和預(yù)期收益。在內(nèi)存方向上,我們的主要目標(biāo)是將單個(gè)容器的內(nèi)存從32GB壓縮到16GB,并在以下兩個(gè)基本方向上進(jìn)行了探索:

  • 減少內(nèi)存增長(zhǎng)速度:壓縮本地緩存,減少浮動(dòng)內(nèi)存的產(chǎn)生,并對(duì)線程,類庫(kù),參數(shù)和代碼邏輯進(jìn)行針對(duì)性的優(yōu)化和調(diào)整。
  • 提升內(nèi)存管理效率:加強(qiáng)JVM等服務(wù)依賴的基礎(chǔ)組件其本身的性能。

由于第一個(gè)方向需要根據(jù)應(yīng)用的具體代碼實(shí)現(xiàn)來分析和排查,普適性相對(duì)較差,所以本文將主要分享查詢服務(wù)在輕量化中對(duì)于內(nèi)存管理方向上的探索過程和實(shí)踐經(jīng)驗(yàn)。

二、堆內(nèi)內(nèi)存管理

我們的應(yīng)用原本運(yùn)行在JDK8的CMS收集器之上,但是在JDK11以后,CMS已經(jīng)被完全淘汰。于是,要提高堆內(nèi)的內(nèi)存管理效率,我們首先嘗試的便是對(duì)GC進(jìn)行升級(jí)和調(diào)優(yōu)。因此我們對(duì)G1、ZGC和ShenandoahGC等更現(xiàn)代的收集器進(jìn)行了性能上的測(cè)試和對(duì)比,來嘗試找出最合適的技術(shù)選型。

2.1 垃圾收集器的選型

首先,在JDK17上,ZGC第一次以生產(chǎn)環(huán)境可用的狀態(tài)登陸了LTS版本,所以我們這次選型起初的目標(biāo)也是嘗試將應(yīng)用遷移到ZGC之上。相對(duì)于大家熟悉的G1,ZGC最主要的優(yōu)勢(shì)在于其通過著色指針和讀屏障兩個(gè)特性,使得用戶線程幾乎可以全程與標(biāo)記-復(fù)制算法并行,基本解決了YGC的STW問題。簡(jiǎn)單來說,ZGC在標(biāo)記過程中會(huì)向64位指針的高位4bit中記錄三色標(biāo)記、重分配標(biāo)記和可達(dá)性標(biāo)記;當(dāng)應(yīng)用線程訪問對(duì)象時(shí),讀屏障機(jī)制會(huì)依據(jù)指針狀態(tài)和復(fù)制表信息去更新對(duì)象的地址和狀態(tài)。

這樣,即使GC線程正在后臺(tái)轉(zhuǎn)移、復(fù)制或清理對(duì)象,也可以保證前臺(tái)線程能始終訪問到正確的地址,這使得ZGC幾乎可以做到無停頓回收。除此之外,ZGC還向用戶承諾了可擴(kuò)展性:由于ZGC的停頓時(shí)間基本只和初始掃描中GC Roots的數(shù)量相關(guān),堆的大小和活躍對(duì)象的數(shù)量并不會(huì)導(dǎo)致停頓時(shí)間的增長(zhǎng)。

圖片

其次,ShenandoahGC與ZGC同為新一代的零停頓收集器,總體來看,其內(nèi)存布局非常類似于G1,而并發(fā)設(shè)計(jì)則與ZGC如出一轍,所以我們也將其作為一個(gè)可能的備選方案。

ShenandoahGC與ZGC的主要區(qū)別在于其使用的是Brook指針而非染色指針:即在對(duì)象頭中額

外記錄一個(gè)指向復(fù)制后正確地址的指針。但是由于額外信息記錄在對(duì)象頭中,Brook指針的讀屏障無法在第一次訪問后直接更新正確地址來自我恢復(fù)。另一方面,ShenandoahGC的區(qū)塊布局和回收階段則與G1非常相似,甚至部分代碼都是直接復(fù)用的。其不同主要在于ShenandoahGC利用了一個(gè)被稱為連接矩陣的二維數(shù)組來取代G1中開銷巨大的記憶集,來解決跨區(qū)引用問題:例如區(qū)塊N引用了區(qū)塊M,則在數(shù)組的`[N][M]`坐標(biāo)打上標(biāo)記。

最后,作為現(xiàn)在最主流的收集器,同時(shí)也是CMS的取代者,G1理所應(yīng)當(dāng)?shù)囊脖晃覀冏鳛樽畛墒旌头€(wěn)妥的一個(gè)選擇。G1本身的內(nèi)存布局使得其對(duì)可控的停頓耗時(shí)和吞吐量的平衡上有較好的兼顧,在理論上使它更適合查詢接口這種會(huì)短時(shí)間內(nèi)突然生成大量臨時(shí)對(duì)象的計(jì)算密集型應(yīng)用。

綜上所述,我們以原本在輕量化前的生產(chǎn)配置(16C32G+JDK1.8+CMS)作為基準(zhǔn),選取了以下幾個(gè)組合作為測(cè)試方案:

圖片

其他各相關(guān)參數(shù)都為默認(rèn)配置。

2.2 G1調(diào)優(yōu)實(shí)踐

在橫向比較不同收集器的性能之前,我們首先需要按應(yīng)用的需求對(duì)每個(gè)收集器做一些簡(jiǎn)單的適配和調(diào)整,以發(fā)揮這些收集器的全部性能。由于G1是為開箱即用而準(zhǔn)備的默認(rèn)收集器,使用起來相對(duì)簡(jiǎn)單,基本上只需要簡(jiǎn)單設(shè)置下堆大小和線程數(shù)等參數(shù)即可。然而在實(shí)際使用中,我們?nèi)匀挥龅搅艘恍┬栴},需要對(duì)關(guān)鍵某些參數(shù)進(jìn)行控制。

(1)現(xiàn)象1:某個(gè)高壓力場(chǎng)景的計(jì)算集群的YGC頻率相對(duì)較高,GC吞吐量不足,最終甚至?xí)l(fā)FGC。

由于輕量化配置的資源本身就比較緊張,很難通過增加`ConcGCThreads`線程數(shù)來提升吞吐量,于是我們嘗試放寬了`MaxGCPauseMillis`來減少G1的回收壓力。作為G1最核心的參數(shù),當(dāng)`MaxGCPauseMillis`過小時(shí),G1會(huì)自動(dòng)調(diào)整其他GC參數(shù)來盡可能滿足該目標(biāo),進(jìn)而導(dǎo)致YGC非常頻繁并影響吞吐量。由于各個(gè)應(yīng)用的情況不同,需要開發(fā)人員手動(dòng)進(jìn)行基準(zhǔn)測(cè)試來找到最合適的數(shù)值:當(dāng)我們把數(shù)值從200調(diào)整到300后,平均響應(yīng)有非常明顯的下降,而繼續(xù)上調(diào)的邊際效應(yīng)則很不明顯。

(2)現(xiàn)象2:應(yīng)用在長(zhǎng)時(shí)間穩(wěn)定運(yùn)行后,老年代突然大量上漲后卻不及時(shí)回收,在數(shù)小時(shí)內(nèi)老年代都維持高位,擠占年輕代的占比并影響到了YGC的停頓耗時(shí)。

由于查詢服務(wù)主要是無狀態(tài)的計(jì)算邏輯,除了一部分本地緩存外大部分的對(duì)象都是相對(duì)短命的,但是當(dāng)GC壓力上升的情況下仍然會(huì)有大量對(duì)象進(jìn)入老年代。這里我們通過縮小`InitiatingHeapOccupancyPercent`來降低MGC的閾值,讓G1可以及時(shí)回收掉進(jìn)入老年代的短命對(duì)象。

除此之外,我們也對(duì)JDK8、11和17上的G1進(jìn)行過縱向?qū)Ρ?,發(fā)現(xiàn)實(shí)際反映到機(jī)器性能和響應(yīng)耗時(shí)維度上的區(qū)別非常小,在排除掉宿主機(jī)本身的硬件區(qū)別后幾乎可以忽略不計(jì)。最終,由于依賴類庫(kù)和監(jiān)控平臺(tái)等各種原因,最終選擇了相對(duì)成熟的JDK11作為G1的平臺(tái)。

2.3 ZGC調(diào)優(yōu)實(shí)踐

與G1的普適明顯不同,對(duì)ZGC上的適配工作明顯困難的多,作為最現(xiàn)代的收集器之一,ZGC并不是萬用的銀彈,因此也并沒有成為JDK17的默認(rèn)收集器。相比G1,ZGC并不能簡(jiǎn)單的適配于所有場(chǎng)景,我們?cè)谠囘\(yùn)行過程中遇到了一系列難以解決的問題,經(jīng)過大量的參數(shù)調(diào)整和性能測(cè)試后,才能發(fā)揮其全部的實(shí)力。

(1)現(xiàn)象1:在訪問量突然上漲時(shí),會(huì)觀察到非常顯著的分鐘級(jí)響應(yīng)尖刺。

由于ZGC會(huì)使用之前數(shù)次的GC指標(biāo)來預(yù)測(cè)下一次GC的回收策略,使得其相比CMS等傳統(tǒng)收集器更容易受到流量波動(dòng)的沖擊,在高流量壓力下的魯棒性很差。解決響應(yīng)尖刺的辦法主要有兩個(gè):首先是通過提高`ZAllocationSpikeTolerance`的數(shù)值來減少觸發(fā)GC的閾值,其次是打開周期性的主動(dòng)回收參數(shù)`ZProactive`,并通過減少`ZCollectionInterval`來縮短兩次主動(dòng)GC的間隔。兩個(gè)參數(shù)的具體數(shù)值需要開發(fā)人員手動(dòng)調(diào)試確定,但是在高壓力下無論怎么調(diào)整參數(shù),提升都相對(duì)有限。

圖片

(2)現(xiàn)象2:GC日志出現(xiàn)大量Allocation Stall/Rellocation Stall,同時(shí)監(jiān)控上發(fā)現(xiàn)秒級(jí)的STW出現(xiàn),某些時(shí)候甚至伴有OOM報(bào)錯(cuò)。

雖然ZGC的停頓時(shí)間很短,但整個(gè)回收階段很長(zhǎng),期間用戶線程一直處于并發(fā)運(yùn)行的狀態(tài)。這使得回收過程中會(huì)產(chǎn)生大量的浮動(dòng)垃圾,只能等到下次GC時(shí)再回收。此時(shí)如果浮動(dòng)垃圾占滿了整個(gè)堆使得回收無法繼續(xù),ZGC就會(huì)直接暫停對(duì)應(yīng)的用戶線程,來優(yōu)先執(zhí)行回收任務(wù),同時(shí)在日志上記錄對(duì)應(yīng)線程的Allocation/Relocation Stall。簡(jiǎn)而言之,Allocation Stall是一種當(dāng)GC吞吐量不夠時(shí)觸發(fā)的用戶線程暫停,大量秒級(jí)的Allocation Stall甚至比FGC的影響更大。

這種情況一般都是GC回收速度跟不上內(nèi)存申請(qǐng)速度導(dǎo)致的,如果GC資源相對(duì)充足的話,可以通過上面兩個(gè)主動(dòng)GC參數(shù)來增加GC頻率,而如果GC資源本身就很匱乏,則只能通過增加GC線程數(shù)`ConcGCThreads`和`ParallelGCThreads`來根本性的解決問題。

圖片

(3)現(xiàn)象3:ZGC堆使用的RSS持續(xù)上升,其大小不會(huì)隨著內(nèi)存使用情況智能伸縮,最終導(dǎo)致了堆外溢出。

低版本的ZGC并不會(huì)主動(dòng)將長(zhǎng)期未使用的堆內(nèi)存返還給系統(tǒng),JDK13后ZGC提供了`ZUncommitDelay`參數(shù)來設(shè)置將空閑內(nèi)存返還給OS的期限,可以通過縮短這個(gè)值來使得RSS空間被更加靈活的使用。為了保證生產(chǎn)環(huán)境服務(wù)的穩(wěn)定性,我們直接通過讓堆大小的上下限相同來防止堆的伸縮。

2.4 基準(zhǔn)測(cè)試的結(jié)果

在實(shí)際試驗(yàn)中,我們首先觀察到ZGC確實(shí)無愧于其零停頓收集器的名號(hào),可以做到在全程任何情況下都達(dá)到百微秒級(jí)每次的停頓時(shí)間,每分鐘累計(jì)不超過1ms,同時(shí)CMS中令人困擾的FGC現(xiàn)象也不再成為問題。而與之原理相近的ShenandoahGC的性能表現(xiàn)也非常好,平均每分鐘的停頓時(shí)間也不超過10ms。G1與這些新一代的收集器相比雖然遜色許多,但是仍然能在僅僅使用生產(chǎn)配置一半的內(nèi)存下,達(dá)到比CMS更好的GC性能:其YGC停頓約下降了50%,每分鐘停頓約為200ms左右,并且MGC也基本可以滿足老年代的回收需要,數(shù)天時(shí)間內(nèi)沒有觀察到FGC。

但是隨著流量壓力的上升,我們很快發(fā)現(xiàn)作為首選的ZGC和ShenandoahGC等零停頓收集器實(shí)際上并不適合查詢接口這樣的運(yùn)算密集型應(yīng)用:他們的運(yùn)行顯著地依賴于資源開銷,最終嚴(yán)重?cái)D占了業(yè)務(wù)邏輯的計(jì)算資源,使得響應(yīng)耗時(shí)飆升,服務(wù)趨于崩潰。

為了穩(wěn)定服務(wù),我們不得不重新分配了更多計(jì)算資源并降低了流量,并得出了結(jié)論:即使使用了兩倍的線程資源,在CPU利用率三倍于CMS的情況下,ZGC和ShenandoahGC仍然只能達(dá)到相當(dāng)于生產(chǎn)環(huán)境約50%-60%的極限吞吐量。與之相對(duì),G1在這方面的表現(xiàn)則好得多,在輕量化配置下比起生產(chǎn)配置的整體吞吐量?jī)H稍有下降,在相同QPS下的CPU利用率變高了約5-7%,幾乎可以忽略不計(jì)。經(jīng)過后續(xù)排查,我們發(fā)現(xiàn)主要的原因是ZGC的四條ZWORKER線程幾乎每個(gè)都會(huì)100%的占用一個(gè)核心(如下圖),大量的吞吃了CPU資源并影響到了核心處理流程。

圖片

在內(nèi)存方面,ZGC的實(shí)際表現(xiàn)也并不盡如人意。ZGC不僅比傳統(tǒng)收集器記錄了更多的額外信息,且很多優(yōu)化和特性(例如字符串去重、分代、指針壓縮等)也暫時(shí)無法使用,這使得ZGC無論是堆內(nèi)還是堆外的內(nèi)存開銷都要明顯高于G1和CMS。測(cè)試中我們使用NMT和JMX簡(jiǎn)單對(duì)比了各收集器在未接入流量一段時(shí)間內(nèi)的平均內(nèi)存開銷,發(fā)現(xiàn)ZGC的堆內(nèi)開銷大約比CMS高三分之一,堆外則高達(dá)CMS的10倍左右。

圖片

綜合來看,ZGC等零停頓收集器雖然可以達(dá)到10ms以下的每分鐘停頓時(shí)間,但是其占用的大量GC資源會(huì)嚴(yán)重影響核心計(jì)算邏輯。在資源緊張的輕量化場(chǎng)景下,切換至ZGC導(dǎo)致了服務(wù)在極限壓測(cè)中損失了相當(dāng)于100%生產(chǎn)流量的吞吐量,同時(shí)接口的響應(yīng)耗時(shí)上升了70%。與之相對(duì),G1在響應(yīng)耗時(shí)上的表現(xiàn)則幾乎與原來相同,各細(xì)節(jié)指標(biāo)上也僅僅只有CPU利用率略差。

圖片

2.5 遷移到JDK11

由于ZGC和ShenandoahGC在測(cè)試中表現(xiàn)不佳,且各種問題在資源緊張的限制下幾乎無法解決,我們最終將目光轉(zhuǎn)向了更為成熟穩(wěn)定的G1+JDK11的組合。從JDK8遷移到11是兩個(gè)連續(xù)的LTS版本之間的遷移,比起直接遷移到17來說簡(jiǎn)單很多。我們?cè)趯?shí)際遷移中主要遇到了三個(gè)類型的問題:官方類庫(kù)缺失,權(quán)限控制,以及第三方類庫(kù)報(bào)錯(cuò)。

  • 官方類庫(kù)缺失:JDK11中移除了一系列官方類庫(kù),其中部分類庫(kù)只是從rt.jar中被拆分,可以簡(jiǎn)單的通過maven補(bǔ)回,例如javax.*,但是其他一些包含危險(xiǎn)操作的類庫(kù)則被直接刪除,例如jdk.nashorn,sun.misc等,一般也可以通過重寫來繞過這些代碼。
  • ?權(quán)限控制:JDK11中對(duì)各種權(quán)限做了更精細(xì)的控制。例如自代理需要使用參數(shù)??-Djdk.attach.allowAttachSelf???控制,而跨類庫(kù)的反射權(quán)限則需要用??--add-exports=???和??--add-exports??來打開。如果未能注意到這些參數(shù)則會(huì)造成大量權(quán)限控制報(bào)錯(cuò)。?
  • 第三方類庫(kù)報(bào)錯(cuò):此類報(bào)錯(cuò)一般都是兼容性問題導(dǎo)致,Lombok和AspectJ等類庫(kù)需要根據(jù)JDK版本來選擇對(duì)應(yīng)的類庫(kù)版本。主要排查難點(diǎn)在于報(bào)錯(cuò)的形式五花八門,報(bào)錯(cuò)信息對(duì)定位幾乎沒有幫助,有時(shí)候很難確定是哪個(gè)類庫(kù)導(dǎo)致的。

三、堆外內(nèi)存管理

一般來說,對(duì)于運(yùn)行在容器中的單個(gè)Java應(yīng)用,大部分堆外相關(guān)的細(xì)節(jié)都會(huì)被虛擬機(jī)給屏蔽掉,導(dǎo)致開發(fā)人員往往很少會(huì)深入到相關(guān)問題。然而,由于這次輕量化后剩余的內(nèi)存資源非常緊張,我們被迫給堆外留下了非常有限的空間,導(dǎo)致了后續(xù)測(cè)試過程中出現(xiàn)的一系列問題。

3.1 問題表象與排查

具體來說,在生產(chǎn)測(cè)試中,我們經(jīng)常觀察到應(yīng)用經(jīng)常在半夜多次無故宕機(jī)后被拉起,最終反復(fù)點(diǎn)火失敗導(dǎo)致應(yīng)用崩潰無法繼續(xù)服務(wù)。經(jīng)過大量試驗(yàn)后,我們發(fā)現(xiàn)這一現(xiàn)象與JDK版本和GC無關(guān),可以在多個(gè)輕量化配置上復(fù)現(xiàn),現(xiàn)象為RSS在較長(zhǎng)的一段時(shí)間內(nèi)持續(xù)上升,最終導(dǎo)致了應(yīng)用多次崩潰重啟。

圖片

由于是RSS持續(xù)上升,我們排查時(shí)首先懷疑是堆內(nèi)溢出。但是卻并沒有在應(yīng)用日志上發(fā)現(xiàn)OOM報(bào)錯(cuò),且使用JFR檢查堆內(nèi)存增長(zhǎng)情況后也并沒有找到明顯的溢出跡象。所以我們進(jìn)一步檢查了機(jī)器的dmesg日志,發(fā)現(xiàn)反復(fù)崩潰的原因是malloc申請(qǐng)不到內(nèi)存,導(dǎo)致內(nèi)核的oom_killer線程直接kill掉了DMESGTomcact進(jìn)程,然后又被重啟腳本重新拉起。

由于堆內(nèi)存是基本穩(wěn)定的,我們使用NMT baseline對(duì)JVM的堆外內(nèi)存使用情況進(jìn)行了檢查。雖然現(xiàn)代GC本身的native占用相對(duì)較高,但增長(zhǎng)并不顯著,總體內(nèi)存使用很穩(wěn)定,也沒有泄露的傾向。

圖片

進(jìn)一步向下排查,我們用 `gdb --batch --pid 36563 --ex 'call malloc_trim()'` 強(qiáng)行回收內(nèi)存后,發(fā)現(xiàn)RSS有明顯下降,至此基本判斷是glibc這一塊造成了泄露問題。

圖片

3.2 內(nèi)存分配器

要解釋溢出的原因,首先需要了解一下內(nèi)存管理的機(jī)制。對(duì)于Java應(yīng)用來說,內(nèi)存管理一般分為四層:內(nèi)核負(fù)責(zé)管理和映射虛擬頁(yè),glibc進(jìn)行通用的內(nèi)存算法管理,JVM負(fù)責(zé)屏蔽內(nèi)存申請(qǐng)和回收的細(xì)節(jié),而最上層才是Java應(yīng)用。

圖片

由于mmap和brk是系統(tǒng)調(diào)用,如果應(yīng)用每次申請(qǐng)內(nèi)存時(shí)都直接訪問內(nèi)核函數(shù)的話,性能會(huì)非常差,代碼實(shí)現(xiàn)也更加困難。所以,linux會(huì)使用通用的內(nèi)存分配算法來緩沖和規(guī)劃內(nèi)存的使用,其主要關(guān)注點(diǎn)在三個(gè)指標(biāo)上:

  • 減少申請(qǐng)和釋放操作的時(shí)間和開銷?
  • 減少小對(duì)象分配帶來的內(nèi)存碎片?
  • 減少分配器本身數(shù)據(jù)結(jié)構(gòu)的額外內(nèi)存開
    ?

3.3 默認(rèn)分配器PTMALLOC的優(yōu)缺點(diǎn)

在默認(rèn)情況下,glibc使用的是其原生的ptmalloc2內(nèi)存分配器,由以下三層數(shù)據(jù)結(jié)構(gòu)組成:

  • arena(分配區(qū))是ptmalloc中的內(nèi)存緩沖區(qū),在一個(gè)環(huán)形鏈表上被管理。也是ptmalloc中最小的鎖顆粒度。?
  • bin(空閑鏈表)是arena中用于管理可用內(nèi)存塊的鏈表。不同的bins根據(jù)其管理的內(nèi)存塊大小而被分為fast、unsorted、small和large四種。
  • chunk(內(nèi)存塊)則是用戶申請(qǐng)和釋放內(nèi)存的最小單位。bins的頭部永遠(yuǎn)是一個(gè)被稱為top chunk的空閑塊,當(dāng)沒有合適的chunk時(shí),會(huì)擴(kuò)容并返回top chunk來處理請(qǐng)求。

圖片

ptmalloc作為標(biāo)準(zhǔn)實(shí)現(xiàn),主要使用了以下幾個(gè)方法來優(yōu)化以上三個(gè)重要指標(biāo):

內(nèi)存池(減少頻繁的系統(tǒng)調(diào)用和內(nèi)存碎片):用戶free掉的內(nèi)存,不會(huì)被直接被歸還給系統(tǒng),而是暫存到bins中,供下次申請(qǐng)時(shí)直接分配。

多分配區(qū)(減少鎖競(jìng)爭(zhēng)):所有內(nèi)存操作都需要加鎖,如果沒有找到未上鎖的arena,則會(huì)新增一個(gè)副arena并上鎖,直到arena的數(shù)量上限。?

ptmalloc雖然滿足了內(nèi)存分配器的基本需求,但是本身實(shí)現(xiàn)有很多缺陷,導(dǎo)致了內(nèi)在的OOM傾向:

  • 額外內(nèi)存開銷大:每個(gè)chunk都需要額外消耗8b的內(nèi)存,而chunk是內(nèi)存操作的最小單位,這會(huì)導(dǎo)致整體上浪費(fèi)了非常多內(nèi)存。
  • 內(nèi)存利用率不穩(wěn)定:由于多分配區(qū)的機(jī)制,激烈的鎖競(jìng)爭(zhēng)會(huì)導(dǎo)致副arena數(shù)量快速增多。并且,新增的副arena永遠(yuǎn)不會(huì)被銷毀,且保留會(huì)其初始的chunk。這意味著在一臺(tái)16核的標(biāo)準(zhǔn)機(jī)器上最多會(huì)有128個(gè)arena,并占用高達(dá)8G的堆外緩沖區(qū)。
  • 多線程性能差:所有的內(nèi)存操作都需要進(jìn)行悲觀鎖的加鎖解鎖操作,導(dǎo)致其性能較差。同時(shí),即使有多分配區(qū)機(jī)制,在動(dòng)輒500個(gè)以上線程的生產(chǎn)環(huán)境中這個(gè)并發(fā)量完全不夠。?
  • 回收機(jī)制簡(jiǎn)陋:由于bins是鏈表結(jié)構(gòu),ptmalloc的內(nèi)存收縮必須從上向下收縮,這意味著只要后申請(qǐng)的內(nèi)存沒有被釋放,之前申請(qǐng)的所有chunk都無法被收縮,這導(dǎo)致了在管理長(zhǎng)周期內(nèi)存時(shí),有內(nèi)存泄漏的可能性。

具體到這次詳情頁(yè)的溢出,則主要有三個(gè)原因:

  • 前置條件:由于輕量化的需要,應(yīng)用目前僅僅能給堆外大約2.5G的空間。并且G1本身使用的堆外空間是CMS的4-6倍之多。而原來的32G的堆外空間充足,所以之前沒有發(fā)現(xiàn)類似問題。?
  • 由于大量的緩存、快照和報(bào)文的處理,應(yīng)用本身有非常頻繁和重量級(jí)的NIO和序列化/壓縮操作(尤其是點(diǎn)火的時(shí)候,線程數(shù)非常多),這導(dǎo)致了應(yīng)用會(huì)高頻的申請(qǐng)和釋放堆外內(nèi)存作為IO緩沖區(qū)。因此ptmalloc在這種情況下新增了大量的arena來避免頻繁的鎖競(jìng)爭(zhēng)(下圖中有大量64M大小的內(nèi)存塊)。
  • ptmalloc本身的釋放機(jī)制就導(dǎo)致申請(qǐng)的內(nèi)存被歸還的特別慢,甚至有內(nèi)存溢出的傾向,這些因素綜合在一起引起了OOM的發(fā)生。

圖片

3.4 解決方案JEMALLOC

考慮到ptmalloc的性能相對(duì)較差,我們將目光轉(zhuǎn)向了第三方的內(nèi)存管理器。無論是谷歌的tcmalloc還是臉書的jemalloc都完全是默認(rèn)分配器的上位替代,各項(xiàng)性能遠(yuǎn)超ptmalloc,并且遷移起來非常方便。雖然tcmalloc和jemalloc兩者之間優(yōu)劣差別不大,但是由于jemalloc相對(duì)優(yōu)秀的工具鏈,我們最終優(yōu)先對(duì)它進(jìn)行了測(cè)試。

jemalloc是專精于多核多線程場(chǎng)景的內(nèi)存分配器,可以說在并發(fā)量越大的情況下,jemalloc的優(yōu)勢(shì)越明顯。對(duì)比pemalloc,jemalloc有以下的優(yōu)勢(shì):

(1)內(nèi)存碎片率:jemalloc承諾至多20%的內(nèi)存碎片。

通過將內(nèi)存塊根據(jù)大小進(jìn)一步細(xì)分為232個(gè)小類,同一個(gè)bins中的內(nèi)存塊大小一致,向上取整申請(qǐng),來提高每個(gè)內(nèi)存塊在分配時(shí)的利用率和性能。(同樣處理10kb內(nèi)存的申請(qǐng),返回10kb的 chunk和返回20kb的chunk之間肯定有區(qū)別)

采用了低地址優(yōu)先的分配策略,進(jìn)一步降低了內(nèi)存碎片率。(使用紅黑樹記錄了地址排序,總是從低地址開始分配,使高地址的內(nèi)存更整塊)

(2)鎖的顆粒度:jemalloc在大部分場(chǎng)景下幾乎是無鎖的。

每個(gè)線程都擁有動(dòng)態(tài)伸縮的緩存tcache,在小內(nèi)存操作時(shí)是無鎖的。

大部分的線程都會(huì)被綁定到專屬的arena上,使其操作無鎖化(類似于JVM的偏向鎖)。即使多個(gè) 線程共享一個(gè)arena,也會(huì)在arena內(nèi)部細(xì)化為局部鎖,而不是直接使用全局鎖。

(3)內(nèi)存回收:除了類似于ptmalloc的回收機(jī)制外,jemalloc還有兩種機(jī)制。

當(dāng)發(fā)現(xiàn)某個(gè)chunk全部都是臟頁(yè)后,會(huì)直接釋放整個(gè)chunk。

當(dāng)臟頁(yè)數(shù)量超過某個(gè)閾值的時(shí)候,進(jìn)行主動(dòng)的purge操作。

(4)額外開銷:僅僅占用約2%的額外內(nèi)存,用于存儲(chǔ)一些meta信息。

(5)工具鏈:jemalloc有完善的內(nèi)存分析工具,可以更好的定位溢出和泄露問題。

圖片

3.5 遷移和收益

對(duì)于簡(jiǎn)單的性能測(cè)試,手動(dòng)安裝jemalloc非常容易,甚至不需要重新編譯代碼,直接在一臺(tái)正常運(yùn)行的機(jī)器上安裝好jemalloc后,修改tomcat的sh文件中將LD_PRELOAD變量指定為對(duì)應(yīng)的so文件覆蓋glibc動(dòng)態(tài)庫(kù)并重啟tomcat即可。而后續(xù)容器部署也只需要在dockerfile中自定義數(shù)行代碼模擬上述操作,然后構(gòu)建并上傳自定義鏡像便能完成。 

目前查詢服務(wù)已經(jīng)在jemalloc上生產(chǎn)運(yùn)行了數(shù)個(gè)月,至今還沒有觀察到再次出現(xiàn)堆外溢出的問題;同時(shí)RSS的波動(dòng)非常穩(wěn)定,即使遇到流量高峰也不會(huì)出現(xiàn)內(nèi)存尖刺,可以保持良好的響應(yīng)時(shí)間和穩(wěn)定。

圖片

從實(shí)際的情況來看,jemalloc與ptmalloc相比主要有以下收益:

  • 從運(yùn)維方面來看,集群為了方便調(diào)度,一般會(huì)限制幾個(gè)預(yù)設(shè)的容器配置以供選擇。在資源相對(duì)緊張的情況下,jemalloc可以使得應(yīng)用整體的部署更加靈活,而使用默認(rèn)的ptmalloc則會(huì)被迫將容器配置向上升級(jí),否則就需要額外對(duì)特殊配置進(jìn)行審批和調(diào)度,這樣不但會(huì)造成不必要的資源浪費(fèi),同時(shí)在流量尖峰時(shí)也難以對(duì)集群進(jìn)行調(diào)度和擴(kuò)容。
  • 在成本方面,從測(cè)試結(jié)果出發(fā),僅僅使用jemalloc本身就能比ptmalloc在每臺(tái)機(jī)器上節(jié)省1-1.5G的堆外內(nèi)存,雖然在單機(jī)上可能不夠顯著,但是推廣到整個(gè)云的范圍時(shí)收益應(yīng)該是非常可觀的。
  • 性能上,jemalloc的內(nèi)存回收和多線程機(jī)制更加高效和智能化,對(duì)低配置機(jī)器更加友好,能大大加強(qiáng)內(nèi)存資源緊張的機(jī)器上服務(wù)的魯班性,同時(shí)對(duì)IO、GC、類加載等多線程native操作有較大的優(yōu)化。
  • 從遷移角度看,遷移到j(luò)emalloc幾乎是無成本的操作,僅僅需要簡(jiǎn)單的鏡像自定義和一定的灰度測(cè)試,就可以完成優(yōu)化。

故綜合來看,jemalloc的收益相比于成本大得多的,有一定的分享和推廣的意義。

四、結(jié)語

本文相對(duì)完整的記述了酒店查詢服務(wù)在輕量化中的一次優(yōu)化過程,希望其中的經(jīng)驗(yàn)和過程能對(duì)讀者有所幫助。然而,對(duì)于應(yīng)用的優(yōu)化過程是一個(gè)從猜想到驗(yàn)證的循環(huán)。在有了可能的猜測(cè)和方向之后,比起反復(fù)的調(diào)研,更重要的則是不斷向著落地驗(yàn)證去推進(jìn)。雖然這些經(jīng)驗(yàn)有一些普適性,但是由于應(yīng)用之間各有不同,仍然需要讀者根據(jù)實(shí)際情況親手試驗(yàn)后,才能最終確定是否有借鑒意義。

責(zé)任編輯:張燕妮 來源: 攜程技術(shù)
相關(guān)推薦

2024-04-18 09:41:53

2024-03-22 15:09:32

2022-07-08 09:38:27

攜程酒店Flutter技術(shù)跨平臺(tái)整合

2022-06-03 08:58:24

APP攜程流暢度

2022-10-21 10:40:08

攜程酒店MySQL慢查詢

2024-09-10 16:09:58

2023-08-18 10:49:14

開發(fā)攜程

2023-08-25 09:51:21

前端開發(fā)

2022-09-03 21:13:19

攜程供應(yīng)商直連平臺(tái)

2024-12-13 10:50:00

數(shù)據(jù)開發(fā)攜程

2022-06-17 10:44:49

實(shí)體鏈接系統(tǒng)旅游AI知識(shí)圖譜攜程

2023-10-13 09:34:27

算法數(shù)據(jù)

2022-04-14 17:53:50

攜程AWS上云

2022-12-14 10:09:44

研發(fā)效能

2023-11-13 11:27:58

攜程可視化

2017-07-06 19:57:11

AndroidMVP攜程酒店

2023-03-10 09:32:31

ANY功能短數(shù)據(jù)通信功能

2023-11-24 09:44:07

數(shù)據(jù)攜程

2024-09-25 15:37:46

2023-08-04 09:35:18

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)