Elastic Memory Management for Cloud Data Analytics

Abstract:

本文提出了一種自動的、彈性的內存管理方案——ElasticMem,該方案的應用環境是共享式集羣中執行的衆多數據分析應用程序(後面稱之爲query)。ElasticMem主要包含以下三部分:

  1. 在JVM中動態改變內存限制
  2. 預測內存使用情況(memory usage)和gc開銷的模型
  3. 在應用(query)之間動態分配內存的調度算法

通過和靜態內存分配方式進行比較,實驗結果顯示:

  1. 在內存稀缺時,應用(query)執行失敗的情況變少了
  2. gc開銷減少了80%
  3. 在內存充足時,減少了30%的應用(query)執行時間

 

Section 1:Introduction:

         許多大數據系統都在shared-nothing(每個結點都有自己的CPU、內存、存儲) 集羣中處理大規模數據集,在這樣的集羣中,資源管理器(負責系統和應用程序之間的資源分配)通常都是通過container(將相同機器上的應用程序隔離開來,每個應用程序擁有單獨的硬件資源限制)實現。資源管理器啓動具有資源限制的容器後,將應用程序放入容器中執行。()

                

         在使用基於容器的調度時,需要提前估計應用程序的資源需求,如果當前的資源能夠滿足其需求則啓動該應用程序。問題的關鍵在於對數據分析應用程序所需要的內存進行提前估計是困難的,因爲所需要的內存和很多運行時因素有關,這些因素往往很難估計。

         估計內存過大或過小都會影響應用程序的性能:

  1. 如果內存估計過多,集羣的資源(內存)利用率太低
  2. 如果估計內存太少,甚至少於運行所需要的最低內存,可能會導致虛擬內存技術的使用(同樣會降低性能)或者直接拋出OOM異常(拋出異常之後程序不執行導致之前分配給他的內存完全被浪費)

對內存的估計在各種大數據系統中都是一個挑戰,尤其是使用GC的自動內存管理。由於GC的不確定性,即使分配的內存足夠,在某些時候GC也會顯著地降低應用程序的性能。我們會在Section 2中通過改變堆的最大空間進行詳細演示。

        爲了解決上述問題,我們提出了ElasticMem——資源管理器能夠彈性地爲不同container分配內存,這些container中執行着數據分析程序(query)。優化目標是同時減少query執行失敗的情況和執行時間。

        在container之間彈性分配內存遇到了如下幾個困難:

  1. 大多數系統裏並不支持運行時內存限制的改變
  2. 需要了解額外的內存防止query執行失敗和減少執行時間的原理,同時我們還需要一個預測GC收益和開銷的模型
  3. 需要一個內存分配的算法,用於協調衆多數據分析應用程序之間的內存分配,這個算法會用到2中的GC模型

        怎樣解決上述三個困難呢?我們首先確定一些前提條件:大數據處理系統是基於Java的,數據分析程序我們選用大數據的關係代數查詢。這篇paper的主要貢獻如下:

  1.  在Section 3.1中,我們會展示如何修改JVM實現堆佈局的動態變化
  2. 在Section 3.3中,我們會介紹一種預測堆狀態和GC開銷的算法(whether it is expected to run out of memory),該算法是以機器學習爲基礎的。因爲常見的關係代數查詢使用的數據結構都是哈希表,所以我們使用哈希表的統計數據作爲算法的輸入
  3. 在Section 3.2中,我們會介紹爲應用程序分配內存的算法。應用的場景是一臺機器上有多個JVM,每個JVM中執行一個query

關於評估性能:使用的環境是Myria,query使用的是TPC-H query,參照對象是靜態內存分配(即container的內存限制是固定的)。實現結果和摘要中顯示的一致,與此同時,資源利用率也得到了提高。

 

        Section 2:自動內存管理的性能影響

現今,很多大數據系統都是用Java寫的,這也導致了GC和自動管理內存機制爲性能帶來了一定的不確定性。因爲GC策略在運行時由JVM內部控制,並且由於策略不同,GC的種類也會有所不同。

現代的JVM中最常見的GC就是基於generation實現的GC,在這種設計下,堆空間會根據對象的壽命分成不同的generation(代)。初始化內存分配通常在年輕代,當年輕代滿時,GC會清除那些dead objects。下圖中有幾種不同的GC:

  1. Young Collection(YGC):在YGC中,年輕代中的live object會被移動到老年代(我們把這個過程叫做promotion),同時年輕代中的dead object會被清除
  2. Full Collection(FGCp和FGCc,兩者的區別會在Section 3.2.3中詳細介紹):在FGC中,除了promotion之外,年輕代和老年代的dead object都會被清除

調用哪一種GC取決於promotion是否執行失敗。在這篇paper中,我們以OpenJDK作爲JVM的實現參考,同時我們選擇的GC算法也是常用的(使用了年輕代和老年代)。對於其他語言和GC算法,我們把這個作爲future work。

                在下圖,我們在如下環境中演示GC是如何影響query的執行時間

  1. query:採用自連接(self-join)
  2. dataset:一千萬條包含兩個column的tuple
  3. 環境:Myria,Spark 1.1和Spark 2.0,每臺機器上運行一個進程
  4. GC:使用默認的GC(-XX:+UseParallelGC)
  5. 橫軸代表堆空間大小,縱軸代表執行時間

我們得出瞭如下結論:

  1. 當堆空間很大時,Myria和Spark2.0的執行時間在35s處匯合,這個時間代表不包含GC time的真正query time
  2. 當堆空間變小時,三者的query time會因爲GC 耗時的原因相應增加
  3. 當堆空間不斷縮小時,Spark 1.1和Myria會拋出OOM異常,而Spark 2.0不會拋出異常因爲他能借助磁盤解決內存緊張的問題

 

Section 3: Elastic Memory Allocation(具體實現)

        Section 3.1展示了通過修改OpenJDK實現JVM動態改變memory limit,Section 3.2介紹了一種跨多個query動態分配內存的算法(memory manager),Section 3.3提出了一個預測heap state、GC開銷(GC耗時)以及GC收益(GC能回收多少內存)的模型,這個模型可以幫助memory manager在一定的時間點做出相應的決定。

 

Section 3.1:實現JVM堆的動態修改(memory limits)

        OpenJDK對內存的操作:

  1. 用戶確定堆最大空間,JVM向OS請求內存空間後根據內部策略將空間分爲不同的generation
  2. 程序運行時內存不足,JVM可能會觸發GC釋放內存,如果觸發GC後經過很長一段時間都沒能回收足夠的內存,JVM拋出OOM異常
  3. 堆最大空間(maximum heap size)在JVM的生命週期中保持不變,即使在拋出OOM異常時機器還有多餘的內存,maximum heap size也不能增加。同理,當內存實際使用率很低時,maximum heap size也不會減少

因爲OS能夠爲我們提供足夠的內存,因此我們可以先爲JVM分配足夠的邏輯地址空間,等到JVM實際運行時,真正的內存限制(物理內存地址)可以根據內存的需求動態變化。我們通過修改OpenJDK的源碼實現瞭如下操作:

  1. JVM在啓動時根據一個具體的最大堆空間數值(-Xmx)保留一塊連續的地址空間。通常我們會將這個值設置得很大,因爲每個generation的初始size limit值由JVM內部策略設定,這樣每個generation的size limit互不干擾
  2. 我們會將該虛擬機p年輕代和老年代動態變化的size limit表示爲ylimit(p)和olimit(p),memory manager會在運行時改變這些數值,我們將它們初始化爲一個較小的數字(),並且在運行時每個generation中的內存使用情況都不會超過ylimit(p)和olimit(p)

我們使用了一個以套接字爲基礎的api,通過這個api,我們可以和JVM進行交互,例如查看當前的heap state、調整memory limit或者觸發GC。我們不使用JVM內部策略而是使用我們的memory manager決定什麼時候調用以及調用哪一個調用GC,我們將GC回收的內存歸還給OS。如果在當前limit下沒有足夠的內存,我們會暫停JVM直到有足夠的內存(If more memory is needed but unavailable given the current limits, we let the JVM pause until more memory is available)。我們在OpenJDK 7u85的默認堆實現(ParallelScavengeHeap)上進行實現。

 

        Section 3.2:Dynamic Memory Allocation=algorithm=memory manager

該memory manager通過執行一些action調整JVM之間內存的內存分配。這些action包括觸發GC或者kill JVM等等,每個action都有一個value,內存分配的目標就是要使所有action的value之和最大。一個action包含三個因素:該action是否會kill JVM、該action是否會導致JVM暫停以及該action獲取內存的效率(可以是從OS獲取內存,也可以是從GC回收內存,例如.time/space,該值越小代表能花費更少的時間獲取更多的內存,表示獲取內存的效率越高)。

memory manager作出決定主要受兩個因素的影響:JVM heap state和action的value。該manager在時間點t收集運行時統計信息(heap state和value)做出決定,query執行一段時間△t,在時間點t+△t,manager再根據統計信息做出決定,query再執行一段時間△t,以此類推。也就是說,memory manager自適應地在每個時間點t做出決定,該決定持續一段時間△t,在時間點t+△t,manager再做出新的決定。在下文中,我們會從更加精確的問題介紹開始詳細地描述這個算法。

 

        Section 3.2.1:Problem Statement(問題介紹)

應用場景是單節點的,並且每個query僅有一個進程,每個JVM代表一個容器,容器裏運行一個query。我們將query的執行建模爲相應的JVM內存變化。在時間段[t,t+△t]中,因爲query正在執行,所以JVM內存的使用可能會發生相應的增長。我們可以執行以下action中的某一項來影響內存的利用率:

  1. 爲即將到來的memory growth分配相應的內存
  2. 調用GC(short term:會獲得額外的內存,longer term:釋放內存)
  3. 殺死JVM釋放所有內存(kill)
  4. 什麼也不做(如果JVM不能滿足相應的內存需求,JVM就會stall(which may stall a JVM if it cannot grow its memory utilization as needed))

假設single-node上內存總量爲M,有一系列JVM{p1,p2,p3......pN},每個JVM都爲年輕代和老年代分配了一些內存。在時間點t,我們需要爲這N個JVM分配內存M,需要達到的要求是,N個JVM內存之和不能超過M並且全局目標函數要最優。

某一個JVM分配到的內存取決於memory manager執行哪一項action。例如,當要執行YGC時,老年代需要爲promotion準備足夠的空間,因此,memory manager必須增加老年代的memory limit。我們將memory allocation執行action ai時分配給JVM pi的年輕代和老年代的內存最小值表示爲y cap(pi,ai),o cap(pi,ai)(少於這個值就不行,另外這些值是分配的內存總量,不是增量)

每個action都會對全局目標函數有一個value的貢獻,我們將JVM pi上執行action pi具有的value值表示爲value(pi,ai),全局目標函數如下:在分配給每個JVM內存總和小於M的前提下,最大化每個JVM上執行的action所帶來的value之和。(我們將在Section 3.2.3中詳細介紹value(pi,ai))

當我們爲每臺機器單獨做決定時,以上情形可以擴展到shared-nothing集羣的場景。

 

        Section 3.2.2: Runtime Metrics(運行時變量)

action的value和space requirements的計算需要一些運行時變量,有些變量可以從JVM直接得到,有些則需要memory manager進行估計:

Metrics reported by the JVM:

  1. y limit(p,t)、o limit(p,t):在時間點t,JVM p的年輕代和老年代的當前memory limit(這兩個值是memory manager在前一個時間點t-△t設置的,可以在時間點t直接得到)
  2. y used(p,t)、o used(p,t):在時間點t,JVM p年輕代和老年代已使用的內存

 

Metrics estimated by the manager:

  1. y(^) live(p,t) y(^) dead (p,t) , ô live (p,t) and ô dead (p,t):年輕代中live object、dead object的總量,老年代中live object和dead object的總量(有^的變量表示需要memory manager進行估計)
  2. g(^)rw(p,t):heap growth,在到達下一個時間點t+△t前,g(^)rw(p,t) =y(^) used (p,t +△ t )−y used (p,t)
  3. 爲了對GC的影響進行建模,memory manager需要知道GC能夠釋放多少內存以及GC需要花費多少時間。g(^)c y (y obj (p,t)) 和g(^)c o (o obj (p,t))分別表示young GC和old GC的估計時間,其中y obj (p,t)和o obj (p,t)表示年輕代和老年代中的所有對象

下圖詳細列出了上文中提到的metrics,因爲只有一個時間參數t,因此在後面的內容中我們在用到某個metric時會省略t,我們將在Section 3.3中詳細描述memory manager是如何估計那些帶有^的metric

 

        Section 3.2.3: Space of Possible Actions(詳細介紹有哪些action)

                對於一個JVM,有四種action可供選擇:

  1. 允許JVM向OS申請內存用於增長
  2. 調用GC回收空間以減少分配給該JVM的內存
  3. 如果既不grow也不能回收足夠的內存,就暫停JVM
  4. 殺死JVM釋放所有的內存

memory manager會在每個時間點t爲每個JVM執行action(t、t+△t、t+2*△t等等),對於某個JVM執行的action會有一個value,同時也會得到y cap(p,a)、o cap(p,a)(虛擬機p年輕代和老年代的最低內存需求,即新的limit)。我們把action a在JVM p上執行的時間表示爲time(p,a),同時把action a得到的(申請的或者回收的)可用內存表示爲space(p,a),那麼cost就可以定義爲得到指定大小的內存所需要的時間,也就是time(p,a)/space(p,a),memory manager會使用這個數值進行action的比較和選擇。action的詳細描述如下:

  1. GROW: 讓JVM grow使得query繼續執行,由於新增加的內存總是出現在年輕代,所以y cap(p,GROW) = y used(p)+ g(^)rw(p,t)。o cap(p,GROW)= y(^) live(p)+o used(p)(未來可能會觸發不同種類的GC,所以爲老年代預留promotion所需要的空間)。space=y cap(p)+o cap(p)-y limit(p)-o limit(p)(即新的limit-舊的limit),action的cost指的是請求並得到這些內存所需要的時間,取決於space的大小。值得一提的是,在通常情況下,GROW是首選的action,除非內存變得緊張,JVM纔會考慮執行GC或者pause直到能夠滿足GROW執行
  2. YGC: 觸發 young generation GC,包含promotion操作和回收年輕代中的dead objects,因此y cap(p) =y used(p),o cap(p)=y(^) live(p)+o used(p),回收的空間大小爲年輕代中的dead objects,花費的時間爲young collection的估計時間
  3. FGCp: full GC,包括YGC和old collection,因此y cap(p)和o cap(p)和YGC一樣,回收的空間包含年輕代和老年代中的dead objects,花費的時間爲young collection的估計時間+old collection的估計時間
  4. FGCc: full GC,和FGCp不同的地方在於,首先在整個heap執行一次GC,回收dead objects,執行完後年輕代中的多餘空間會被移動到老年代,然後根據老年代中的內存情況判斷是否執行YGC(?cap表示的是最低並不是limit)。因爲YGC不是必須執行的,所以y cap(p) = y used(p),o cap(p) = o used(p),因爲第一次GC的執行對象是整個heap,不僅僅是老年代,假設時間與live objects成正比,所以總時間等於young collection的估計時間+old colletion的估計時間*r(r是總的live objects除以老年代中的live objects),能夠回收的空間爲整個heap的dead objects
  5. NOOP: 什麼也不做,y cap和o cap與之前的memory limit保持一致,這樣會因爲JVM既不grow也不調用GC導致暫停
  6. KILL:kill JVM,在該JVM中執行的query會失敗

每個action的執行後的size limit以及space和time等屬性如下表所示:(os(m)表示向OS申請m大小的內存所需要的時間,跟不同的系統以及環境有關,可以通過執行一個calibration program得到(在Section 4.1中會詳細介紹))

FGCp(先執行YGC)是OpenJDK中的默認行爲,如果老年代的內存不足,promotion就會失敗,JVM會花費很多的時間將已經拷貝到老年代中的live objects拷貝回年輕代以保持年輕代的狀態和未執行promotion之前一樣,這樣會造成效率低下,尤其是內存稀缺時,promotion failure可能會更加難以避免。因此我們需要用到FGCc,他可以在不增加memory limit的前提下回收內存。FGCc首先在整個heap上執行一次GC回收dead objects,然後在保持整個heap limit不變的前提下將年輕代中的free space移動到老年代,如果此時老年代中的free space足以執行YGC,則執行YGC

               action的value由三個屬性組成,但是三個屬性中只有一個非零,如下所示:

                      

對於NOOP,value的NOOP屬性爲1,其他均爲0;對於KILL,value的KILL屬性爲1,其他均爲0;對於GROW,YGC,FGCp和FGCc,他們的NOOP和KILL屬性爲0,cost屬性(時間/空間,獲取單位空間的內存所花費的時間)不爲零,我們將在Section 3.2.4中詳細介紹memory manager是如何比較這三個屬性從而對action進行選擇的。

 

        Section 3.2.4: Memory Allocation Algorithm(內存分配算法)

內存分配算法在具有間隔的時間點上通過執行action爲衆多JVM分配內存,我們可以將這個算法看做0-1揹包問題,揹包的大小就是總的內存大小,揹包裏的item就是每個JVM執行的action,每個action具有一個value值和一個最小空間需求(y cap和o cap),我們的目標就是在總的空間需求不超過總內存的情況下,最大化value值之和。

0-1揹包的問題可以用動態規劃的方法來解,定義optN,M爲將M大小的內存分配給N個JVM{p1,p2...pN}最佳解的value值(當JVM pi正在執行GC,memory manager會跳過它,否則它會遍歷所有action選擇一個action使得value值最大)。動態規劃解法的狀態方程如下圖所示(兩個value之和是指他們的三個屬性相加):

下圖展示瞭如何比較兩個value a和b,如果a.KILL<b.KILL,則返回a>b,如果a.NOOP<b.NOOP,則返回a>b,如果a.cost<b.cost,則返回a>b。當以上三個條件均不滿足時,返回a<b。在比較value的三個屬性時,優先級順序分別是KILL>NOOP>cost。也就是說我們傾向於選擇KILL更小、NOOP更小以及cost更小的action。

動態規劃的實際操作在算法2 KNAPSACK中有體現,首先將opt 0,j初始化爲0,首先遍歷每個JVM,再循環裏遍歷K(實際上是遍歷內存M),在二重循環裏遍歷每個action,對每個opt i,j都進行上圖所示的opt i,j狀態轉換方程得到新的opt i,j,最後最大的value之和被放在opt N,K中,通過記錄每次更新時的action就可以得到最優的解決方案act。

動態規劃的時間複雜度爲O(N*M),其中N是JVM的總數,而M是總的內存大小。如果內存單元是細粒度的話,這將導致上述算法中K值很大,從而導致算法的執行時間增加,memory manager不能很快地做出內存分配的決定。因此,我們假定U是內存分配單元(分配給JVM的內存都是constant*U,如上述算法所示),U可以設置爲常量,也可以根據當前heap state進行動態變化,我們將在Section 4中演示不同的U對query執行性能的影響。

算法1 ALLOCATE的詳細內容如下圖所示,該算法在當前時間點爲JVM構成的集合P分配內存M,返回內存分配方案最優解。算法1的具體步驟如下:

  1. 找出當前時間點沒有執行GC的JVM: P-P INGC
  2. 因爲該算法給JVM分配的內存是U的增量,而在當前時間下JVM的memory limit往往不能被U整除,因此我們在算法1中先考慮action爲NOOP的JVM,不對它們做任何處理,同時我們計算出餘下的內存M’(減去正在執行GC和確定要執行NOOP的JVM的memory limit)和餘下的JVM P’(減去正在執行GC和已經確定執行NOOP的JVM數量),調用函數Knapsack得到最佳解。又因爲我們不知道哪些JVM應該執行action NOOP,所以我們考慮所有的情況,即把P-P INGC的冪集的每一個元素作爲執行action NOOP的集合 P NOOP,在遍歷所有情況後得到value值最大的解
  3. 在調用函數Knapsack之前,我們會先計算memory unit: U,進而計算出K = M/U,並將K傳入函數Knapsack
  4. 在算法2中,分配給JVM的內存y cap和o cap會通過函數align(size,U)(天花板除)進行調整,同時,如果要執行的action屬於GC,space(action)必須>=mingcsace(mingcsave是爲了避免GC只回收少量內存而定義的一個常量)
  5. 從函數Knapsack得到的結果與P NOOP 和P INGC 構成最終的分配方案,如果最優解只包含action NOOP,我們會選擇KILL一些JVM進行優化(我們的實際操作是KILL那些佔用內存最多的query所在的JVM,其他優化策略我們留作future work)

 

        Section 3.3: Estimating Runtime Values(估計運行時變量)

               這一部分主要介紹如何估計那些對內存分配決定至關重要的變量

 

        Section 3.3.1: Heap Growth

在當前時間點t爲JVM分配到下一個時間點t+△t的內存時需要估計heap growth。在這篇paper中,我們介紹一種簡單的方法:當前時間點t的heap growth = 過去b+1個時間段內存變化的最大值。在我們的實驗中,取b=3,我們將在Section 4中演示取b=3能達到較好的性能。

 

        Section 3.3.2: GC Time and Space Saving(GC開銷和回收空間)

GC開銷和回收的空間主要是由執行GC的區域裏live objects和dead objects的數量和大小決定的。要想獲得這項數據就得像GC一樣遍歷對象引用表,然而這樣的開銷是巨大的。也就是說,通過這樣的方式對GC開銷和GC回收空間進行預測已經失去了預測本身的意義。

爲了解決以上提到的問題,我們觀察到,JVM中live objects與dead objects的狀態是由query的各種操作(operators)使用的數據結構以及他們的更新模式決定的,所以該數據結構和其更新模式也可以決定GC開銷和回收的空間。我們預測GC開銷和GC回收空間的方式是通過監測query 中各種操作(operator)主要使用的數據結構,收集這些數據結構的統計信息作爲特徵值用於建模。儘管在大數據系統中有很多種operator,但他們使用的數據結構種類卻很少,例如hash table。我們不對operator進行修改,因爲他們的種類太多了,相反地,我們將數據結構和能夠獲得該數據結構統計信息的函數封裝成新的數據結構,這樣我們就能在query執行時得到每個數據結構的統計信息。考慮到在數據分析應用程序中,佔用內存比較多的operator(例如join和aggregate)最常使用的數據結構就是hash table。在這篇paper中,我們着重介紹的數據結構就是hash table。爲了得到整個query的預測,我們先爲一個hash table構建預測模型,最後將每個hash table的預測結果進行彙總得到最終的結果。我們的方法也可以擴展到其他的operator和數據結構中。

hash table存儲包含column的tuple,每個tuple包括由一些column定義的key以及剩餘的column定義的value。我們統計tuple和key的總量與自上次GC起的增量(因爲在執行GC之前,新增加的object都會出現在年輕代)。由於原始類型(如long,使用long[ ]數組進行存儲)和Java對象類型(如String,存引用)在內存中的存儲方式有所不同,因此我們將它們單獨處理。於是,所有的feature就如下所示:

  1. nt:hash table中tuple的總量
  2. ntd:上次GC之後hash table中tuple增量
  3. nk:hash table中的key總量
  4. nkd:上次GC之後hash table中key的增量
  5. num long:屬性爲long (原始類型)的column數
  6. num str:屬性爲String (Java對象類型)的column數
  7. sum str:屬性爲String的column的平均長度()

下表展示了一些具體的hash table統計信息:

在hash table中,獲取這些feature的開銷是很小的,我們可以通過建立一個機器學習模型來預測GC開銷和JVM堆中總的live objects與dead objects的大小(進而得到GC能夠回收的空間)。

建模需要training data,我們嘗試瞭如下幾種方法獲取training data並在建立模型後進行測試:

  1. 隨機觸發GC得到多組training data。測試結果:通過這種方式建立的模型在training data沒有覆蓋或者覆蓋較少的地方預測效果很差
  2. 使用粗粒度的多維grid獲取training data。測試結果:因爲這種方法可以使訓練集均勻地分佈在特徵空間,所以當測試點分佈在grid上時預測效果很好。但在其他地方,例如grid之間的空隙(grid cell)中,預測效果很差
  3. 使用細粒度的多維grid獲取training data,開銷太大,不予考慮

綜合上述方法,我們最終決定結合a和b。具體來講,在使用粗粒度的多維grid進行取樣的基礎上,我們在每個grid cell中隨機選取兩個點作爲training data。最終的training data由多維grid的取樣點和隨機生成點兩部分組成。爲了得到某個hash table的數據點,我們運行只有該hash table並且合成生成的數據集(便於調整feature)作爲輸入的query,這樣做可以在觸發GC時精確地控制每個feature值。得到數據點後,我們可以使用現成的方法構建regression模型。在我們的實現方法中,我們使用的是來自Weka的M5P模型,因爲從整體上看他的預測效果最精準。我們將在Section 4.2中評估該模型。

 

        Section 4: Evaluation(評估)

               在這一部分我們會評估memory manager和GC model

  1. 實驗環境:Amazon EC2上的r3.4xlarge實例、不支持虛擬內存
  2. 大數據系統:Myrial
  3. query:TPC-H query (Q1-Q6,Q8-Q12, and Q14-19),每個query在兩個數據庫上運行,比例係數分別爲1和2

 

        Section 4.1: Scheduling

我們將ElasticMem和Original(堆最大空間固定,每個JVM均分總內存量)進行比較,我們選用了8個query(四個內存密集型query在兩個數據庫上運行),每次內存分配算法的花費大致是0.15s。

我們將Algorithm 2中的mingcsave設置爲30MB。os(m)的值來自於calibration program,該程序使用mmap向os申請內存並通過變量賦值訪問該內存(在r3.4xlarge中,os(m) = 0.35s*m/1 GB)。我們將時間點之間的時間間隔interval設置爲0.5s,我們會在Section 4.1.3中比較不同interval對query性能帶來的影響。爲了防止頻繁的GC調用但回收內存過少導致query停滯不前的情況發生,我們會kill執行時間超過8 min的query,因爲通過觀察,在合理的內存條件下,8 min足以使得實驗中每個query執行完畢。

Original的兩個極端分別是串行執行和所有query並行執行。我們使用一個變量DOP(degree of parallelism)來表示Original的並行程度(同一時間query的執行數量)。爲了使比較更加公平,我們引入了ElasticMem的一個變種——Elastic-Resubmit,它可以在query執行完畢後串行提交一次killed query(執行失敗的query),讓這些query再執行一次。爲了防止livelocks(一直提交,一直被kill)的發生,我們只提交一次killed query,同時,我們將同時提交多個query的Resubmit留作future work。

memory unit:U的值可以是固定的,也可以是隨着時間變化的。在我們的測試中,U可以取100MB,500MB,1000MB(fixed size),1/8,1/12,1/16(variable size:當前時間點t空閒內存的1/8,1/12和1/16)。

 

Section 4.1.1: Scheduling Simultaneous Queries

simultaneous是指同時提交所有query,下表展示了隨着總內存的改變,query執行的時間以及成功執行的query數的變化。(對比了Elastic、Elastic-Resubmit以及各種併發度的Original)

在上表中,我們統一取U=1/12 of total free space,因爲根據實驗得到的結果,U=1/12能夠爲ElasticMem帶來最好的效果,我們將在下一個表格中展示不同U對query執行性能的影響。在上圖中我們可以得到以下幾個結論:

  1. 當內存充足時(>20GB),Elastic和Elastic-Resubmit跟三種Original相比,具有更少的執行時間和更多的query完成數
  2. 當內存緊張到只允許同一時間執行一個query時(<=15GB)
    1. memory = 15GB,Elastic-Resubmit跟Original DOP=1相比,具有更少的執行時間(兩者均完成了所有的query)
    2. memory = 10GB,Elastic-Resubmit跟Original DOP=1相比,它只失敗了一個query,執行時間也稍微多了一點。(據我們觀察,執行失敗的那個query是因爲U不夠細粒度(),並且執行時間更多的原因是Elastic努力將所有query都放在一起以避免他們串行執行,在內存稀缺的情況下這樣做反而會使得執行時間增加。爲了證明我們的觀點,我們統計了實驗中8個query的大型hash table的內存大小之和,約爲14GB。)
  3. 總體上看,相比較於Original,ElasticMem具有一定的優勢:它能夠自適應地調節併發度(隨着內存從稀缺到充足,Elastic-Resubmit都具有較好的表現),在儘可能降低query失敗的情況下提高系統的性能表現

在下表中,我們通過和Original DOP=8(相比於DOP=1更公平)進行比較,演示了ElasticMem的不同變種(不同的U)的性能表現(query執行時間,GC時間,內存利用率)。我們將query執行時間和GC執行時間的性能提升定義爲(x-y)/x,x是Original’s time,y是Elastic’s time。

從上表可以得到如下結論:

  1. 當內存不足時(<=15GB),使用變化的值作爲U(1/8,1/12,1/16)的ElasticMem在執行query時反而比Original DOP=8的更加耗時。這是因爲此時跟Original相比,ElasticMem在嘗試完成更多的query,所以他的query執行時間會更多(這在Figure 4中有所體現)
  2. 當內存充足時(>=20GB),無論U的值是多少,ElasticMem在query執行時間和GC耗時上都要比Original DOP=8表現更好,query執行時間提升了10%-30%,GC耗時提升了40%-80%。通過觀察,我們發現這是因爲Original觸發了回收空間太少的GC,尤其是在大規模的query的後期階段,Original無法把小規模query(這些query在這時已經執行完畢)佔用的空間分配給大規模query。然而ElasticMem能夠動態地調整query之間的內存分配,因此性能表現要優於Original
  3. 當內存達到70GB時,性能提升比開始有所減少,這是因爲在內存足夠充足時,GC時間佔總執行時間的比例減少了。爲了找到GC時間變化到0的過程中ElasticMem性能提升最大的點,我們在最頂部的子表格中展示了GC時間佔query時間比例的變化
  4. 底部的表格顯示了實際內存使用率(RSS)的變化,通過對比,我們可以發現ElasticMem可以更加充分地利用實際物理內存,從而節省GC time和query time,提升性能表現
  5. 我們發現U爲變化值的ElasticMem(U=1/8,1/12,1/16)性能表現大致相當,因此不用在U的調整上花費過多的時間

 

        Section 4.1.2: Scheduling Queries with Delays(模擬真實的集羣,引入delay)

爲了模擬真實的cluster,我們在提交8個query時引入delay=30s,也就是說每隔30s提交一次query。下表顯示了Elastic和Original的query執行時間和query完成數。

通過和Figure 4進行對比我們得到了如下結論:

  1. 整體上和Figure 4類似,Elastic-Resubmit無論是在內存稀缺還是充足的條件下,性能表現都要優於Original的三種變體
  2. 當內存=10GB時,Elastic-Resubmit的執行時間比Figure 4中的Elastic-Resubmit少,同時也比Figure 6中的Original DOP=1少。這是因爲在引入延遲的情況下,同一時間執行的query變少了,又因爲ElasicMem的內存分配具有靈活性,所以跟同一時間下執行的query數更多的情況相比,它能在更短的時間完成更多的query。由於Original對8個query採用的是平均分配內存的方式,因此引入delay後的性能表現和之前相同。

 

        Section 4.1.3: Timestep Interval

               我們通過改變△t(0.1s,0.5s,1s)對ElasticMem的性能表現進行觀察。當內存稀缺時,△t=0.5s能夠輕微地提升性能。在通常情況下,三者的性能表現大致相當。最後我們得出結論:當U爲變化的值時,△t的微小變化不會對ElasticMem的性能造成較大的影響,這也表明不用在△t的調整上花費過多的時間(由於篇幅有限,本篇paper省略了細節)。

 

        Section 4.2: GC Models(評估GC模型)

               在本節中我們會評估Section 3.3中提到的GC模型,評估環境如下:

  1. 訓練空間:12 million tuples、12 million keys、1-7 long columns、0-8 String columns、0-96 characters(sum str)
  2. training data:1080 grid points + 1082 random points
  3. testing data:7696 data points(在兩個數據庫上執行17個TPC-H query時隨機觸發GC得到)
  4. JVM配置:JVM GC 線程:1(-XX:ParallelGCThreads=1,因爲我們觀察到JVM常常不能給每個GC線程平均地分配任務)、不使用線程局部緩衝(-XX:-UseTLAB)、每次執行collection後將live objects移動到老年代的開頭而不是執行幾次collection後再這麼做(-XX:MarkSweepAlwaysCompactCount=1,這麼做可以降低GC開銷方差)
  5. 模型選擇:來自Weka的M5P模型(所有設置均爲默認),M5P模型是一個決策樹,他的所有葉子結點都是linear regression(線性迴歸)。
  6. 誤差計算:RAE(相對絕對誤差),用於表示預測誤差

下表顯示了model在training data上的交叉檢驗結果(經過了十次交叉驗證)以及在testing data上的測試結果。

從表中可以看出,在cross validation中,除了o_dead(老年代中的dead objects)之外,其餘屬性的預測都表現得很好(<=5%)。在testing data中,y_dead和o_dead的預測效果都不理想(>25%)。這是因爲dead objects的數量跟模型的feature(data structure的屬性)不是強相關的。因爲我們已經通過模型預測出了y_live和o_live的數量,因此我們可以通過JVM提供的變量y used和o used得到y_dead和o_dead的值(y_dead=y_used-y_live、o_dead=o_used-o_live),我們在Section 4.1中以這種方式爲基礎進行測試,測試的結果顯示這種方式可以正確的預測GC開銷和GC space,從而使memory manager能夠做出正確的內存分配決定。

 

        Section 5: Related Work(略)

             

這部分提供了一些其他人所做的相關工作,並將ElasticMem和他們進行了比較。

    Memory allocation within a single machine

    這部分簡述了一些single machine上的內存分配方式。這些人做的貢獻考慮到了通過優化global objective function來管理多個對象之間的內存分配,但他們只是在single machine上考慮這個問題,除此之外,他們還忽略了GC。即使有人考慮了GC,但他們沒有GC性能模型和內存分配算法。即使有的人提出了GC模型,他們的模型只適用於某個具體的場景,不像ElasticMem,可以適用於任何關係代數查詢

    Cluster-wide resource scheduling

    這部分簡述一些集羣上的資源調度方式,和這些算法相比,ElasticMem專注於基於Java的大數據系統上的數據分析程序,同時它具有動態調節內存限制的功能

    Adaptive GC tuning

    這部分簡述了幾種其他的GC更改方式

    Region-based memory management

    這部分簡述了另外一種減少GC開銷的方式:RMM,但他會帶來編譯複雜性和一定的安全問題

    Section 6: Conclusion and Future Work(結論和擴展)

在這篇paper中,我們介紹了一種shared-nothing集羣上數據分析應用程序的動態彈性內存管理方式。該方案主要包括動態改變JVM memory limit、預測內存使用情況和GC開銷的模型以及通過執行action爲衆多JVM分配內存的算法。我們在Myria上測試了我們的方案,通過與靜態內存分配方式進行比較,該方案在執行時間和query完成數(減少query執行失敗次數)上都表現更優。我們把使用除hash table以外的數據結構的方法以及在其他系統上實現此方案留作擴展內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章