本文主要介紹了Netty的內(nèi)存管理和性能。
專注于為中小企業(yè)提供網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)新邵免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了上1000家企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。HBase的offheap現(xiàn)狀HBase作為一款流行的分布式NoSQL數(shù)據(jù)庫,被各個(gè)公司大量應(yīng)用,其中有很多業(yè)務(wù)場(chǎng)景,例如信息流和廣告業(yè)務(wù),對(duì)訪問的吞吐和延遲要求都非常高。HBase2.0為了盡大可能避免Java GC對(duì)其造成的性能影響,已經(jīng)對(duì)讀寫兩條核心路徑做了offheap化,也就是對(duì)象的申請(qǐng)都直接向JVM offheap申請(qǐng),而offheap分出來的內(nèi)存都是不會(huì)被JVM GC的,需要用戶自己顯式地釋放。在寫路徑上,客戶端發(fā)過來的請(qǐng)求包都會(huì)被分配到offheap的內(nèi)存區(qū)域,直到數(shù)據(jù)成功寫入WAL日志和Memstore,其中維護(hù)Memstore的ConcurrentSkipListSet其實(shí)也不是直接存Cell數(shù)據(jù),而是存Cell的引用,真實(shí)的內(nèi)存數(shù)據(jù)被編碼在MSLAB的多個(gè)Chunk內(nèi),這樣比較便于管理offheap內(nèi)存。類似地,在讀路徑上,先嘗試去讀BucketCache,Cache未命中時(shí)則去HFile中讀對(duì)應(yīng)的Block,這其中占用內(nèi)存最多的BucketCache就放在offheap上,拿到Block后編碼成Cell發(fā)送給用戶,整個(gè)過程基本上都不涉及heap內(nèi)對(duì)象申請(qǐng)。
但是在小米內(nèi)部最近的性能測(cè)試結(jié)果中發(fā)現(xiàn),100% Get的場(chǎng)景受Young GC的影響仍然比較嚴(yán)重,在HBASE-21879貼的兩幅圖中,可以非常明顯的觀察到Get操作的p999延遲跟G1 Young GC的耗時(shí)基本相同,都在100ms左右。按理說,在HBASE-11425之后,應(yīng)該是所有的內(nèi)存分配都是在offheap的,heap內(nèi)應(yīng)該幾乎沒有內(nèi)存申請(qǐng)。但是,在仔細(xì)梳理代碼后,發(fā)現(xiàn)從HFile中讀Block的過程仍然是先拷貝到堆內(nèi)去的,一直到BucketCache的WriterThread異步地把Block刷新到Offheap,堆內(nèi)的DataBlock才釋放。而磁盤型壓測(cè)試驗(yàn)中,由于數(shù)據(jù)量大,Cache命中率并不高(~70%),所以會(huì)有大量的Block讀取走磁盤IO,于是Heap內(nèi)產(chǎn)生大量的年輕代對(duì)象,最終導(dǎo)致Young區(qū)GC壓力上升。
消除Young GC直接的思路就是從HFile讀DataBlock的時(shí)候,直接往Offheap上讀。之前留下這個(gè)坑,主要是HDFS不支持ByteBuffer的Pread接口,當(dāng)然后面開了HDFS-3246在跟進(jìn)這個(gè)事情。但后面發(fā)現(xiàn)的一個(gè)問題就是:Rpc路徑上讀出來的DataBlock,進(jìn)了BucketCache之后其實(shí)是先放到一個(gè)叫做RamCache的臨時(shí)Map中,而且Block一旦進(jìn)了這個(gè)Map就可以被其他的RPC給命中,所以當(dāng)前RPC退出后并不能直接就把之前讀出來的DataBlock給釋放了,必須考慮RamCache是否也釋放了。于是,就需要一種機(jī)制來跟蹤一塊內(nèi)存是否同時(shí)不再被所有RPC路徑和RamCache引用,只有在都不引用的情況下,才能釋放內(nèi)存。自然而言的想到用reference Count機(jī)制來跟蹤ByteBuffer,后面發(fā)現(xiàn)其實(shí)Netty已經(jīng)較完整地實(shí)現(xiàn)了這個(gè)東西,于是看了一下Netty的內(nèi)存管理機(jī)制。
Netty作為一個(gè)高性能的基礎(chǔ)框架,為了保證GC對(duì)性能的影響降到最低,做了大量的offheap化。而offheap的內(nèi)存是程序員自己申請(qǐng)和釋放,忘記釋放或者提前釋放都會(huì)造成內(nèi)存泄露問題,所以一個(gè)好的內(nèi)存管理器很重要。首先,什么樣的內(nèi)存分配器,才算一個(gè)是一個(gè)“好”的內(nèi)存分配器:
高并發(fā)且線程安全。一般一個(gè)進(jìn)程共享一個(gè)全局的內(nèi)存分配器,得保證多線程并發(fā)申請(qǐng)釋放既高效又不出問題。
高效的申請(qǐng)和釋放內(nèi)存,這個(gè)不用多說。
方便跟蹤分配出去內(nèi)存的生命周期和定位內(nèi)存泄露問題。
高效的內(nèi)存利用率。有些內(nèi)存分配器分配到一定程度,雖然還空閑大量?jī)?nèi)存碎片,但卻再也沒法分出一個(gè)稍微大一點(diǎn)的內(nèi)存來。所以需要通過更精細(xì)化的管理,實(shí)現(xiàn)更高的內(nèi)存利用率。
盡量保證同一個(gè)對(duì)象在物理內(nèi)存上存儲(chǔ)的連續(xù)性。例如分配器當(dāng)前已經(jīng)無法分配出一塊完整連續(xù)的70MB內(nèi)存來,有些分配器可能會(huì)通過多個(gè)內(nèi)存碎片拼接出一塊70MB的內(nèi)存,但其實(shí)合適的算法設(shè)計(jì),可以保證更高的連續(xù)性,從而實(shí)現(xiàn)更高的內(nèi)存訪問效率。
為了優(yōu)化多線程競(jìng)爭(zhēng)申請(qǐng)內(nèi)存帶來額外開銷,Netty的PooledByteBufAllocator默認(rèn)為每個(gè)處理器初始化了一個(gè)內(nèi)存池,多個(gè)線程通過Hash選擇某個(gè)特定的內(nèi)存池。這樣即使是多處理器并發(fā)處理的情況下,每個(gè)處理器基本上能使用各自獨(dú)立的內(nèi)存池,從而緩解競(jìng)爭(zhēng)導(dǎo)致的同步等待開銷。
Netty的內(nèi)存管理設(shè)計(jì)的比較精細(xì)。首先,將內(nèi)存劃分成一個(gè)個(gè)16MB的Chunk,每個(gè)Chunk又由2048個(gè)8KB的Page組成。這里需要提一下,對(duì)每一次內(nèi)存申請(qǐng),都將二進(jìn)制對(duì)齊,例如需要申請(qǐng)150B的內(nèi)存,則實(shí)際待申請(qǐng)的內(nèi)存其實(shí)是256B,而且一個(gè)Page在未進(jìn)Cache前(后續(xù)會(huì)講到Cache)都只能被一次申請(qǐng)占用,也就是說一個(gè)Page內(nèi)申請(qǐng)了256B的內(nèi)存后,后面的請(qǐng)求也將不會(huì)在這個(gè)Page中申請(qǐng),而是去找其他完全空閑的Page。有人可能會(huì)疑問,那這樣豈不是內(nèi)存利用率超低?因?yàn)橐粋€(gè)8KB的Page被分配了256B之后,就再也分配了。其實(shí)不是,因?yàn)楹竺孢M(jìn)了Cache后,還是可以分配出31個(gè)256B的ByteBuffer的。
多個(gè)Chunk又可以組成一個(gè)ChunkList,再根據(jù)Chunk內(nèi)存占用比例(Chunk使用內(nèi)存/16MB * 100%)劃分成不同等級(jí)的ChunkList。例如,下圖中根據(jù)內(nèi)存使用比例不同,分成了6個(gè)不同等級(jí)的ChunkList,其中q050內(nèi)的Chunk都是占用比例在[50,100)這個(gè)區(qū)間內(nèi)。隨著內(nèi)存的不斷分配,q050內(nèi)的某個(gè)Chunk占用比例可能等于100,則該Chunk被挪到q075這個(gè)ChunkList中。因?yàn)閮?nèi)存一直在申請(qǐng)和釋放,上面那個(gè)Chunk可能因某些對(duì)象釋放后,導(dǎo)致內(nèi)存占用比小于75,則又會(huì)被放回到q050這個(gè)ChunkList中;當(dāng)然也有可能某次分配后,內(nèi)存占用比例再次到達(dá)100,則會(huì)被挪到q100內(nèi)。這樣設(shè)計(jì)的一個(gè)好處在于,可以盡量讓申請(qǐng)請(qǐng)求落在比較空閑的Chunk上,從而提高了內(nèi)存分配的效率。
仍以上述為例,某對(duì)象A申請(qǐng)了150B內(nèi)存,二進(jìn)制對(duì)齊后實(shí)際申請(qǐng)了256B的內(nèi)存。對(duì)象A釋放后,對(duì)應(yīng)申請(qǐng)的Page也就釋放,Netty為了提高內(nèi)存的使用效率,會(huì)把這些Page放到對(duì)應(yīng)的Cache中,對(duì)象A申請(qǐng)的Page是按照256B來劃分的,所以直接按上圖所示,進(jìn)入了一個(gè)叫做TinySubPagesCaches的緩沖池。這個(gè)緩沖池實(shí)際上是由多個(gè)隊(duì)列組成,每個(gè)隊(duì)列內(nèi)代表Page劃分的不同尺寸,例如queue->32B,表示這個(gè)隊(duì)列中,緩存的都是按照32B來劃分的Page,一旦有32B的申請(qǐng)請(qǐng)求,就直接去這個(gè)隊(duì)列找未占滿的Page。這里,可以發(fā)現(xiàn),隊(duì)列中的同一個(gè)Page可以被多次申請(qǐng),只是他們申請(qǐng)的內(nèi)存大小都一樣,這也就不存在之前說的內(nèi)存占用率低的問題,反而占用率會(huì)比較高。
當(dāng)然,Cache又按照Page內(nèi)部劃分量(稱之為elemSizeOfPage,也就是一個(gè)Page內(nèi)會(huì)劃分成8KB/elemSizeOfPage個(gè)相等大小的小塊)分成3個(gè)不同類型的Cache。對(duì)那些小于512B的申請(qǐng)請(qǐng)求,將嘗試去TinySubPagesCaches中申請(qǐng);對(duì)那些小于8KB的申請(qǐng)請(qǐng)求,將嘗試去SmallSubPagesDirectCaches中申請(qǐng);對(duì)那些小于16MB的申請(qǐng)請(qǐng)求,將嘗試去NormalDirectCaches中申請(qǐng)。若對(duì)應(yīng)的Cache中,不存在能用的內(nèi)存,則直接去下面的6個(gè)ChunkList中找Chunk申請(qǐng),當(dāng)然這些Chunk有可能都被申請(qǐng)滿了,那么只能向Offheap直接申請(qǐng)一個(gè)Chunk來滿足需求了。
Chunk內(nèi)部分配的連續(xù)性(cache coherence)
上文基本理清了Chunk之上內(nèi)存申請(qǐng)的原理,總體來看,Netty的內(nèi)存分配還是做的非常精細(xì)的,從算法上看,無論是申請(qǐng)/釋放效率還是內(nèi)存利用率都比較有保障。這里簡(jiǎn)單闡述一下Chunk內(nèi)部如何分配內(nèi)存。
一個(gè)問題就是:如果要在一個(gè)Chunk內(nèi)申請(qǐng)32KB的內(nèi)存,那么Chunk應(yīng)該怎么分配Page才比較高效,同時(shí)用戶的內(nèi)存訪問效率比較高?
一個(gè)簡(jiǎn)單的思路就是,把16MB的Chunk劃分成2048個(gè)8KB的Page,然后用一個(gè)隊(duì)列來維護(hù)這些Page。如果一個(gè)Page被用戶申請(qǐng),則從隊(duì)列中出隊(duì);Page被用戶釋放,則重新入隊(duì)。這樣內(nèi)存的分配和釋放效率都非常高,都是O(1)的復(fù)雜度。但問題是,一個(gè)32KB對(duì)象會(huì)被分散在4個(gè)不連續(xù)的Page,用戶的內(nèi)存訪問效率會(huì)受到影響。
Netty的Chunk內(nèi)分配算法,則兼顧了申請(qǐng)/釋放效率和用戶內(nèi)存訪問效率。提高用戶內(nèi)存訪問效率的一種方式就是,無論用戶申請(qǐng)多大的內(nèi)存量,都讓它落在一塊連續(xù)的物理內(nèi)存上,這種特性我們稱之為Cache coherence。
來看一下Netty的算法設(shè)計(jì):
首先,16MB的Chunk分成2048個(gè)8KB的Page,這2048個(gè)Page正好可以組成一顆完全二叉樹(類似堆數(shù)據(jù)結(jié)構(gòu)),這顆完全二叉樹可以用一個(gè)int[] map來維護(hù)。例如,map[1]就表示root,map[2]就表示root的左兒子,map[3]就表示root的右兒子,依次類推,map[2048]是第一個(gè)葉子節(jié)點(diǎn),map[2049]是第二個(gè)葉子節(jié)點(diǎn)…,map[4095]是最后一個(gè)葉子節(jié)點(diǎn)。這2048個(gè)葉子節(jié)點(diǎn),正好依次對(duì)應(yīng)2048個(gè)Page。
這棵樹的特點(diǎn)就是,任何一顆子樹的所有Page都是在物理內(nèi)存上連續(xù)的。所以,申請(qǐng)32KB的物理內(nèi)存連續(xù)的操作,可以轉(zhuǎn)變成找一顆正好有4個(gè)Page空閑的子樹,這樣就解決了用戶內(nèi)存訪問效率的問題,保證了Cache Coherence特性。
但如何解決分配和釋放的效率的問題呢?
思路其實(shí)不是特別難,但是Netty中用各種二進(jìn)制優(yōu)化之后,顯的不那么容易理解。所以,我畫了一副圖。其本質(zhì)就是,完全二叉樹的每個(gè)節(jié)點(diǎn)id都維護(hù)一個(gè)map[id]值,這個(gè)值表示以id為根的子樹上,按照層次遍歷,第一個(gè)完全空閑子樹對(duì)應(yīng)根節(jié)點(diǎn)的深度。例如在step.3圖中,id=2,層次遍歷碰到的第一顆完全空閑子樹是id=5為根的子樹,它的深度為2,所以map[2]=2。
理解了map[id]這個(gè)概念之后,再看圖其實(shí)就沒有那么難理解了。圖中畫的是在一個(gè)64KB的chunk(由8個(gè)page組成,對(duì)應(yīng)樹最底層的8個(gè)葉子節(jié)點(diǎn))上,依次分配8KB、32KB、16KB的維護(hù)流程??梢园l(fā)現(xiàn),無論是申請(qǐng)內(nèi)存,還是釋放內(nèi)存,操作的復(fù)雜度都是log(N),N代表節(jié)點(diǎn)的個(gè)數(shù)。而在Netty中,N=2048,所以申請(qǐng)、釋放內(nèi)存的復(fù)雜度都可以認(rèn)為是常數(shù)級(jí)別的。
通過上述算法,Netty同時(shí)保證了Chunk內(nèi)部分配/申請(qǐng)多個(gè)Pages的高效和用戶內(nèi)存訪問的高效。
上文提到,HBase的ByteBuf也嘗試采用引用計(jì)數(shù)來跟蹤一塊內(nèi)存的生命周期,被引用一次則其refCount++,取消引用則refCount--,一旦refCount=0則認(rèn)為內(nèi)存可以回收到內(nèi)存池。思路很簡(jiǎn)單,只是需要考慮下線程安全的問題。
但事實(shí)上,即使有了引用計(jì)數(shù),可能還是容易碰到忘記顯式refCount--的操作,Netty提供了一個(gè)叫做ResourceLeakDetector的跟蹤器。在Enable狀態(tài)下,任何分出去的ByteBuf都會(huì)進(jìn)入這個(gè)跟蹤器中,回收ByteBuf時(shí)則從跟蹤器中刪除。一旦發(fā)現(xiàn)某個(gè)時(shí)間點(diǎn)跟蹤器內(nèi)的ByteBuff總數(shù)太大,則認(rèn)為存在內(nèi)存泄露。開啟這個(gè)功能必然會(huì)對(duì)性能有所影響,所以生產(chǎn)環(huán)境下都不開這個(gè)功能,只有在懷疑有內(nèi)存泄露問題時(shí)開啟用來定位問題用。
Netty的內(nèi)存管理其實(shí)做的很精細(xì),對(duì)HBase的Offheap化設(shè)計(jì)有不少啟發(fā)。目前HBase的內(nèi)存分配器至少有3種:
Rpc路徑上offheap內(nèi)存分配器。實(shí)現(xiàn)較為簡(jiǎn)單,以定長(zhǎng)64KB為單位分配Page給對(duì)象,發(fā)現(xiàn)Offheap池?zé)o法分出來,則直接去Heap申請(qǐng)。
Memstore的MSLAB內(nèi)存分配器,核心思路跟RPC內(nèi)存分配器相差不大。應(yīng)該可以合二為一。
BucketCache上的BucketAllocator。
就第1點(diǎn)和第2點(diǎn)而言,我覺得今后嘗試改成用Netty的PooledByteBufAllocator應(yīng)該問題不大,畢竟Netty在多核并發(fā)/內(nèi)存利用率以及CacheCoherence上都做了不少優(yōu)化。由于BucketCache既可以存內(nèi)存,又可以存SSD磁盤,甚至HDD磁盤。所以BucketAllocator做了更高程度的抽象,維護(hù)的都是一個(gè)(offset,len)這樣的二元組,Netty現(xiàn)有的接口并不能滿足需求,所以估計(jì)暫時(shí)只能維持現(xiàn)狀。
可以預(yù)期的是,HBase2.0性能必定是朝更好方向發(fā)展的,尤其是GC對(duì)P999的影響會(huì)越來越小。
- end -
參考資料:
https://people.freebsd.org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf
https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919/
https://netty.io/wiki/reference-counted-objects.html
名稱欄目:從HBaseoffheap到Netty的內(nèi)存管理-創(chuàng)新互聯(lián)
轉(zhuǎn)載來于:http://jinyejixie.com/article42/dipihc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站設(shè)計(jì)公司、Google、服務(wù)器托管、網(wǎng)站改版、網(wǎng)站建設(shè)、定制網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容