Project Tungsten:讓Spark將硬件性能壓榨到極限(轉(zhuǎn)載)
在之前的博文中,我們回顧和總結(jié)了2014年Spark在性能提升上所做的努力。本篇博文中,我們將為你介紹性能提升的下一階段——Tungsten。在2014年,我們目睹了Spark締造大規(guī)模排序的新世界紀(jì)錄,同時(shí)也看到了Spark整個(gè)引擎的大幅度提升——從Python到SQL再到機(jī)器學(xué)習(xí)。
Tungsten項(xiàng)目將是Spark自誕生以來(lái)內(nèi)核級(jí)別的最大改動(dòng),以大幅度提升Spark應(yīng)用程序的內(nèi)存和CPU利用率為目標(biāo),旨在最大程度上壓榨新時(shí)代硬件性能。Project Tungsten包括了3個(gè)方面的努力:
- Memory Management和Binary Processing:利用應(yīng)用的語(yǔ)義(application semantics)來(lái)更明確地管理內(nèi)存,同時(shí)消除JVM對(duì)象模型和垃圾回收開(kāi)銷。
- Cache-aware computation(緩存友好的計(jì)算):使用算法和數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)內(nèi)存分級(jí)結(jié)構(gòu)(memory hierarchy)。
- 代碼生成(Code generation):使用代碼生成來(lái)利用新型編譯器和CPU。
之所以大幅度聚焦內(nèi)存和CPU的利用,其主要原因就在于:對(duì)比IO和網(wǎng)絡(luò)通信,Spark在CPU和內(nèi)存上遭遇的瓶頸日益增多。詳細(xì)信息可以查看最新的大數(shù)據(jù)負(fù)載性能研究(Ousterhout ),而我們?cè)跒镈atabricks Cloud用戶做優(yōu)化調(diào)整時(shí)也得出了類似的結(jié)論。
為什么CPU會(huì)成為新的瓶頸?這里存在多個(gè)問(wèn)題:首先,在硬件配置中,IO帶寬提升的非常明顯,比如10Gbps網(wǎng)絡(luò)和SSD存儲(chǔ)(或者做了條文化處理的HDD陣列)提供的高帶寬;從軟件的角度來(lái)看,通過(guò)Spark優(yōu)化器基于業(yè)務(wù)對(duì)輸入數(shù)據(jù)進(jìn)行剪枝,當(dāng)下許多類型的工作負(fù)載已經(jīng)不會(huì)再需要使用大量的IO;在Spark Shuffle子系統(tǒng)中,對(duì)比底層硬件系統(tǒng)提供的原始吞吐量,序列化和哈希(CPU相關(guān))成為主要瓶頸。從種種跡象來(lái)看,對(duì)比IO,Spark當(dāng)下更受限于CPU效率和內(nèi)存壓力。
1. Memory Management和Binary Processing
在JVM上的應(yīng)用程序通常依賴JVM的垃圾回收機(jī)制來(lái)管理內(nèi)存。毫無(wú)疑問(wèn),JVM絕對(duì)是一個(gè)偉大的工程,為不同工作負(fù)載提供了一個(gè)通用的運(yùn)行環(huán)境。然而,隨著Spark應(yīng)用程序性能的不斷提升,JVM對(duì)象和GC開(kāi)銷產(chǎn)生的影響將非常致命。
一直以來(lái),Java對(duì)象產(chǎn)生的開(kāi)銷都非常大。在UTF-8編碼上,簡(jiǎn)單如“abcd”這樣的字符串也需要4個(gè)字節(jié)進(jìn)行儲(chǔ)存。然而,到了JVM情況就更糟糕了。為了更加通用,它重新定制了自己的儲(chǔ)存機(jī)制——使用UTF-16方式編碼每個(gè)字符(2字節(jié)),與此同時(shí),每個(gè)String對(duì)象還包含一個(gè)12字節(jié)的header,和一個(gè)8字節(jié)的哈希編碼,我們可以從 Java Object Layout工具的輸出上獲得一個(gè)更清晰的理解:
1 java.lang.String object internals: 2 OFFSET SIZE TYPE DESCRIPTION VALUE 3 0 4 (object header) ... 4 4 4 (object header) ... 5 8 4 (object header) ... 6 12 4 char[] String.value [] 7 16 4 int String.hash 0 8 20 4 int String.hash32 0 9 Instance size: 24 bytes (reported by Instrumentation API)
毫無(wú)疑問(wèn),在JVM對(duì)象模型中,一個(gè)4字節(jié)的字符串需要48字節(jié)的空間來(lái)存儲(chǔ)!
JVM對(duì)象帶來(lái)的另一個(gè)問(wèn)題是GC。從高等級(jí)上看,通常情況下GC會(huì)將對(duì)象劃分成兩種類型:第一種會(huì)有很高的allocation/deallocation(年輕代),另一種的狀態(tài)非常穩(wěn)定(年老代)。通過(guò)利用年輕代對(duì)象的瞬時(shí)特性,垃圾收集器可以更有效率地對(duì)其進(jìn)行管理。在GC可以可靠地估算對(duì)象的生命周期時(shí),這種機(jī)制可以良好運(yùn)行,但是如果只是基于一個(gè)很短的時(shí)間,這個(gè)機(jī)制很顯然會(huì)遭遇困境,比如對(duì)象忽然從年輕代進(jìn)入到年老代。鑒于這種實(shí)現(xiàn)基于一個(gè)啟發(fā)和估計(jì)的原理,性能可以通過(guò)GC調(diào)優(yōu)的一些“黑魔法”來(lái)實(shí)現(xiàn),因此你可能需要給JVM更多的參數(shù)讓其弄清楚對(duì)象的生命周期。
然而,Spark追求的不僅僅是通用性。在計(jì)算上,Spark了解每個(gè)步驟的數(shù)據(jù)傳輸,以及每個(gè)作業(yè)和任務(wù)的范圍。因此,對(duì)比JVM垃圾收集器,Spark知悉內(nèi)存塊生命周期的更多信息,從而在內(nèi)存管理上擁有比JVM更具效率的可能。
為了扭轉(zhuǎn)對(duì)象開(kāi)銷和無(wú)效率GC產(chǎn)生的影響,我們引入了一個(gè)顯式的內(nèi)存管理器讓Spark操作可以直接針對(duì)二進(jìn)制數(shù)據(jù)而不是Java對(duì)象。它基于sun.misc.Unsafe建立,由JVM提供,一個(gè)類似C的內(nèi)存訪問(wèn)功能(比如explicit allocation、deallocation和pointer arithmetics)。此外,Unsafe方法是內(nèi)置的,這意味著,每個(gè)方法都將由JIT編譯成單一的機(jī)器指令。
在某些方面,Spark已經(jīng)開(kāi)始利用內(nèi)存管理。2014年,Databricks引入了一個(gè)新的基于Netty的網(wǎng)絡(luò)傳輸機(jī)制,它使用一個(gè)類jemalloc的內(nèi)存管理器來(lái)管理所有網(wǎng)絡(luò)緩沖。這個(gè)機(jī)制讓Spark shuffle得到了非常大的改善,也幫助了Spark創(chuàng)造了新的世界紀(jì)錄。
新內(nèi)存管理的首次亮相將出現(xiàn)在Spark 1.4版本,它包含了一個(gè)由Spark管理,可以直接在內(nèi)存中操作二進(jìn)制數(shù)據(jù)的hashmap。對(duì)比標(biāo)準(zhǔn)的Java HashMap,該實(shí)現(xiàn)避免了很多中間環(huán)節(jié)開(kāi)銷,并且對(duì)垃圾收集器透明。

當(dāng)下,這個(gè)功能仍然處于開(kāi)發(fā)階段,但是其展現(xiàn)的初始測(cè)試行能已然令人興奮。如上圖所示,我們?cè)?個(gè)不同的途徑中對(duì)比了聚合計(jì)算的吞吐量——開(kāi)發(fā)中的新模型、offheap模型、以及java.util.HashMap。新的hashmap可以支撐每秒超過(guò)100萬(wàn)的聚合操作,大約是java.util.HashMap的兩倍。更重要的是,在沒(méi)有太多參數(shù)調(diào)優(yōu)的情況下,隨著內(nèi)存利用增加,這個(gè)模式基本上不存在性能的衰弱,而使用JVM默認(rèn)模式最終已被GC壓垮。
在Spark 1.4中,這個(gè)hashmap可以為DataFracmes和SQL的聚合處理使用,而在1.5中,我們將為其他操作提供一個(gè)讓其利用這個(gè)特性的數(shù)據(jù)結(jié)構(gòu),比如sort和join。毫無(wú)疑問(wèn),它將應(yīng)用到大量需要調(diào)優(yōu)GC以獲得高性能的場(chǎng)景。
2. Cache-aware computation(緩存友好的計(jì)算)
在解釋Cache-aware computation之前,我們首先回顧一下“內(nèi)存計(jì)算”,也是Spark廣為業(yè)內(nèi)知曉的優(yōu)勢(shì)。對(duì)于Spark來(lái)說(shuō),它可以更好地利用集群中的內(nèi)存資源,提供了比基于磁盤(pán)解決方案更快的速度。然而,Spark同樣可以處理超過(guò)內(nèi)存大小的數(shù)據(jù),自動(dòng)地外溢到磁盤(pán),并執(zhí)行額外的操作,比如排序和哈希。
類似的情況,Cache-aware computation通過(guò)使用 L1/ L2/L3 CPU緩存來(lái)提升速度,同樣也可以處理超過(guò)寄存器大小的數(shù)據(jù)。在給用戶Spark應(yīng)用程序做性能分析時(shí),我們發(fā)現(xiàn)大量的CPU時(shí)間因?yàn)榈却龔膬?nèi)存中讀取數(shù)據(jù)而浪費(fèi)。在 Tungsten項(xiàng)目中,我們?cè)O(shè)計(jì)了更加緩存友好的算法和數(shù)據(jù)結(jié)構(gòu),從而讓Spark應(yīng)用程序可以花費(fèi)更少的時(shí)間等待CPU從內(nèi)存中讀取數(shù)據(jù),也給有用工作提供了更多的計(jì)算時(shí)間。
我們不妨看向?qū)τ涗浥判虻睦?。一個(gè)標(biāo)準(zhǔn)的排序步驟需要為記錄儲(chǔ)存一組的指針,并使用quicksort 來(lái)互換指針直到所有記錄被排序?;陧樞驋呙璧奶匦?,排序通常能獲得一個(gè)不錯(cuò)的緩存命中率。然而,排序一組指針的緩存命中率卻很低,因?yàn)槊總€(gè)比較運(yùn)算都需要對(duì)兩個(gè)指針解引用,而這兩個(gè)指針對(duì)應(yīng)的卻是內(nèi)存中兩個(gè)隨機(jī)位置的數(shù)據(jù)。

那么,我們?cè)撊绾翁岣吲判蛑械木彺姹镜匦??其中一個(gè)方法就是通過(guò)指針順序地儲(chǔ)存每個(gè)記錄的sort key。舉個(gè)例子,如果sort key是一個(gè)64位的整型,那么我們需要在指針陣列中使用128位(64位指針,64位sort key)來(lái)儲(chǔ)存每條記錄。這個(gè)途徑下,每個(gè)quicksort對(duì)比操作只需要線性的查找每對(duì)pointer-key,從而不會(huì)產(chǎn)生任何的隨機(jī)掃描。希望上述解釋可以讓你對(duì)我們提高緩存本地性的方法有一定的了解。
這樣一來(lái),我們又如何將這些優(yōu)化應(yīng)用到Spark?大多數(shù)分布式數(shù)據(jù)處理都可以歸結(jié)為多個(gè)操作組成的一個(gè)小列表,比如聚合、排序和join。因此,通過(guò)提升這些操作的效率,我們可以從整體上提升Spark。我們已經(jīng)為排序操作建立了一個(gè)新的版本,它比老版本的速度快5倍。這個(gè)新的sort將會(huì)被應(yīng)用到sort-based shuffle、high cardinality aggregations和sort-merge join operator。在2015年底,所有Spark上的低等級(jí)算法都將升級(jí)為cache-aware,從而讓所有應(yīng)用程序的效率都得到提高——從機(jī)器學(xué)習(xí)到SQL。
3. 代碼生成
大約在1年前,Spark引入代碼生成用于SQL和DataFrames里的表達(dá)式求值(expression evaluation)。表達(dá)式求值的過(guò)程是在特定的記錄上計(jì)算一個(gè)表達(dá)式的值(比如age > 35 && age < 40)。當(dāng)然,這里是在運(yùn)行時(shí),而不是在一個(gè)緩慢的解釋器中為每個(gè)行做單步調(diào)試。對(duì)比解釋器,代碼生成去掉了原始數(shù)據(jù)類型的封裝,更重要的是,避免了昂貴的多態(tài)函數(shù)調(diào)度。
在之前的博文中,我們闡述了代碼生成可以加速(接近一個(gè)量級(jí))多種TPC-DS查詢。當(dāng)下,我們正在努力讓代碼生成可以應(yīng)用到所有的內(nèi)置表達(dá)式上。此外,我們計(jì)劃提升代碼生成的等級(jí),從每次一條記錄表達(dá)式求值到向量化表達(dá)式求值,使用JIT來(lái)開(kāi)發(fā)更好的作用于新型CPU的指令流水線,從而在同時(shí)處理多條記錄。
在通過(guò)表達(dá)式求值優(yōu)化內(nèi)部組件的CPU效率之外,我們還期望將代碼生成推到更廣泛的地方,其中一個(gè)就是shuffle過(guò)程中將數(shù)據(jù)從內(nèi)存二進(jìn)制格式轉(zhuǎn)換到wire-protocol。如之前所述,取代帶寬,shuffle通常會(huì)因數(shù)據(jù)系列化出現(xiàn)瓶頸。通過(guò)代碼生成,我們可以顯著地提升序列化吞吐量,從而反過(guò)來(lái)作用到shuffle網(wǎng)絡(luò)吞吐量的提升。

上面的圖片對(duì)比了單線程對(duì)800萬(wàn)復(fù)雜行做shuffle的性能,分別使用的是Kryo和代碼生成,在速度上后者是前者的2倍以上。
Tungsten和未來(lái)的工作
在未來(lái)的幾個(gè)版本中,Tungsten將大幅度提升Spark的核心引擎。它首先將登陸Spark 1.4版本,包括了Dataframe API中聚合操作的內(nèi)存管理,以及定制化序列化器。二進(jìn)制內(nèi)存管理的擴(kuò)展和cache-aware數(shù)據(jù)結(jié)構(gòu)將出現(xiàn)在Spark 1.5的部分項(xiàng)目(基于DataFrame模型)中。當(dāng)然如果需要的話,這個(gè)提升也會(huì)應(yīng)用到Spark RDD API。
對(duì)于Spark,Tungsten是一個(gè)長(zhǎng)期的項(xiàng)目,因此也存在很多的可能性。值得關(guān)注的是,我們還將考察LLVM或者OpenCL,讓Spark應(yīng)用程序可以利用新型CPU所提供的SSE/SIMD指令,以及GPU更好的并行性來(lái)提升機(jī)器學(xué)習(xí)和圖的計(jì)算。
Spark不變的目標(biāo)就是提供一個(gè)單一的平臺(tái),讓用戶可以從中獲得更好的分布式算法來(lái)匹配任何類型的數(shù)據(jù)處理任務(wù)。其中,性能一直是主要的目標(biāo)之一,而Tungsten的目標(biāo)就是讓Spark應(yīng)用程序達(dá)到硬件性能的極限。更多詳情可以持續(xù)關(guān)注Databricks博客,以及6月舊金山的Spark Summit。
轉(zhuǎn)載自 http://www.csdn.net/article/2015-04-30/2824591-project-tungsten-bringing-spark-closer-to-bare-metal
英文原文參見(jiàn) https://databricks.com/blog/2015/04/28/project-tungsten-bringing-spark-closer-to-bare-metal.html

浙公網(wǎng)安備 33010602011771號(hào)