1. 程式人生 > >Project Tungsten:讓Spark將硬件性能壓榨到極限(轉載)

Project Tungsten:讓Spark將硬件性能壓榨到極限(轉載)

不同的 taf nsa expr 自己的 機器學習 偉大的 重新 object

在之前的博文中,我們回顧和總結了2014年Spark在性能提升上所做的努力。本篇博文中,我們將為你介紹性能提升的下一階段——Tungsten。在2014年,我們目睹了Spark締造大規模排序的新世界紀錄,同時也看到了Spark整個引擎的大幅度提升——從Python到SQL再到機器學習。

Tungsten項目將是Spark自誕生以來內核級別的最大改動,以大幅度提升Spark應用程序的內存和CPU利用率為目標,旨在最大程度上壓榨新時代硬件性能。Project Tungsten包括了3個方面的努力:

  • Memory Management和Binary Processing:
    利用應用的語義(application semantics)來更明確地管理內存,同時消除JVM對象模型和垃圾回收開銷。
  • Cache-aware computation(緩存友好的計算):使用算法和數據結構來實現內存分級結構(memory hierarchy)。
  • 代碼生成(Code generation):使用代碼生成來利用新型編譯器和CPU。

之所以大幅度聚焦內存和CPU的利用,其主要原因就在於:對比IO和網絡通信,Spark在CPU和內存上遭遇的瓶頸日益增多。詳細信息可以查看最新的大數據負載性能研究(Ousterhout ),而我們在為Databricks Cloud用戶做優化調整時也得出了類似的結論。

為什麽CPU會成為新的瓶頸?這裏存在多個問題:首先,在硬件配置中,IO帶寬提升的非常明顯,比如10Gbps網絡和SSD存儲(或者做了條文化處理的HDD陣列)提供的高帶寬;從軟件的角度來看,通過Spark優化器基於業務對輸入數據進行剪枝,當下許多類型的工作負載已經不會再需要使用大量的IO;在Spark Shuffle子系統中,對比底層硬件系統提供的原始吞吐量,序列化和哈希(CPU相關)成為主要瓶頸。從種種跡象來看,對比IO,Spark當下更受限於CPU效率和內存壓力。

1. Memory Management和Binary Processing

在JVM上的應用程序通常依賴JVM的垃圾回收機制來管理內存。毫無疑問,JVM絕對是一個偉大的工程,為不同工作負載提供了一個通用的運行環境。然而,隨著Spark應用程序性能的不斷提升,JVM對象和GC開銷產生的影響將非常致命。

一直以來,Java對象產生的開銷都非常大。在UTF-8編碼上,簡單如“abcd”這樣的字符串也需要4個字節進行儲存。然而,到了JVM情況就更糟糕了。為了更加通用,它重新定制了自己的儲存機制——使用UTF-16方式編碼每個字符(2字節),與此同時,每個String對象還包含一個12字節的header,和一個8字節的哈希編碼,我們可以從 Java Object Layout工具的輸出上獲得一個更清晰的理解:

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)

毫無疑問,在JVM對象模型中,一個4字節的字符串需要48字節的空間來存儲!

JVM對象帶來的另一個問題是GC。從高等級上看,通常情況下GC會將對象劃分成兩種類型:第一種會有很高的allocation/deallocation(年輕代),另一種的狀態非常穩定(年老代)。通過利用年輕代對象的瞬時特性,垃圾收集器可以更有效率地對其進行管理。在GC可以可靠地估算對象的生命周期時,這種機制可以良好運行,但是如果只是基於一個很短的時間,這個機制很顯然會遭遇困境,比如對象忽然從年輕代進入到年老代。鑒於這種實現基於一個啟發和估計的原理,性能可以通過GC調優的一些“黑魔法”來實現,因此你可能需要給JVM更多的參數讓其弄清楚對象的生命周期。

然而,Spark追求的不僅僅是通用性。在計算上,Spark了解每個步驟的數據傳輸,以及每個作業和任務的範圍。因此,對比JVM垃圾收集器,Spark知悉內存塊生命周期的更多信息,從而在內存管理上擁有比JVM更具效率的可能。

為了扭轉對象開銷和無效率GC產生的影響,我們引入了一個顯式的內存管理器讓Spark操作可以直接針對二進制數據而不是Java對象。它基於sun.misc.Unsafe建立,由JVM提供,一個類似C的內存訪問功能(比如explicit allocation、deallocation和pointer arithmetics)。此外,Unsafe方法是內置的,這意味著,每個方法都將由JIT編譯成單一的機器指令。

在某些方面,Spark已經開始利用內存管理。2014年,Databricks引入了一個新的基於Netty的網絡傳輸機制,它使用一個類jemalloc的內存管理器來管理所有網絡緩沖。這個機制讓Spark shuffle得到了非常大的改善,也幫助了Spark創造了新的世界紀錄。

新內存管理的首次亮相將出現在Spark 1.4版本,它包含了一個由Spark管理,可以直接在內存中操作二進制數據的hashmap。對比標準的Java HashMap,該實現避免了很多中間環節開銷,並且對垃圾收集器透明。

技術分享圖片

當下,這個功能仍然處於開發階段,但是其展現的初始測試行能已然令人興奮。如上圖所示,我們在3個不同的途徑中對比了聚合計算的吞吐量——開發中的新模型、offheap模型、以及java.util.HashMap。新的hashmap可以支撐每秒超過100萬的聚合操作,大約是java.util.HashMap的兩倍。更重要的是,在沒有太多參數調優的情況下,隨著內存利用增加,這個模式基本上不存在性能的衰弱,而使用JVM默認模式最終已被GC壓垮。

在Spark 1.4中,這個hashmap可以為DataFracmes和SQL的聚合處理使用,而在1.5中,我們將為其他操作提供一個讓其利用這個特性的數據結構,比如sort和join。毫無疑問,它將應用到大量需要調優GC以獲得高性能的場景。

2. Cache-aware computation(緩存友好的計算)

在解釋Cache-aware computation之前,我們首先回顧一下“內存計算”,也是Spark廣為業內知曉的優勢。對於Spark來說,它可以更好地利用集群中的內存資源,提供了比基於磁盤解決方案更快的速度。然而,Spark同樣可以處理超過內存大小的數據,自動地外溢到磁盤,並執行額外的操作,比如排序和哈希。

類似的情況,Cache-aware computation通過使用 L1/ L2/L3 CPU緩存來提升速度,同樣也可以處理超過寄存器大小的數據。在給用戶Spark應用程序做性能分析時,我們發現大量的CPU時間因為等待從內存中讀取數據而浪費。在 Tungsten項目中,我們設計了更加緩存友好的算法和數據結構,從而讓Spark應用程序可以花費更少的時間等待CPU從內存中讀取數據,也給有用工作提供了更多的計算時間。

我們不妨看向對記錄排序的例子。一個標準的排序步驟需要為記錄儲存一組的指針,並使用quicksort 來互換指針直到所有記錄被排序。基於順序掃描的特性,排序通常能獲得一個不錯的緩存命中率。然而,排序一組指針的緩存命中率卻很低,因為每個比較運算都需要對兩個指針解引用,而這兩個指針對應的卻是內存中兩個隨機位置的數據。

技術分享圖片

那麽,我們該如何提高排序中的緩存本地性?其中一個方法就是通過指針順序地儲存每個記錄的sort key。舉個例子,如果sort key是一個64位的整型,那麽我們需要在指針陣列中使用128位(64位指針,64位sort key)來儲存每條記錄。這個途徑下,每個quicksort對比操作只需要線性的查找每對pointer-key,從而不會產生任何的隨機掃描。希望上述解釋可以讓你對我們提高緩存本地性的方法有一定的了解。

這樣一來,我們又如何將這些優化應用到Spark?大多數分布式數據處理都可以歸結為多個操作組成的一個小列表,比如聚合、排序和join。因此,通過提升這些操作的效率,我們可以從整體上提升Spark。我們已經為排序操作建立了一個新的版本,它比老版本的速度快5倍。這個新的sort將會被應用到sort-based shuffle、high cardinality aggregations和sort-merge join operator。在2015年底,所有Spark上的低等級算法都將升級為cache-aware,從而讓所有應用程序的效率都得到提高——從機器學習到SQL。

3. 代碼生成

大約在1年前,Spark引入代碼生成用於SQL和DataFrames裏的表達式求值(expression evaluation)。表達式求值的過程是在特定的記錄上計算一個表達式的值(比如age > 35 && age < 40)。當然,這裏是在運行時,而不是在一個緩慢的解釋器中為每個行做單步調試。對比解釋器,代碼生成去掉了原始數據類型的封裝,更重要的是,避免了昂貴的多態函數調度。

在之前的博文中,我們闡述了代碼生成可以加速(接近一個量級)多種TPC-DS查詢。當下,我們正在努力讓代碼生成可以應用到所有的內置表達式上。此外,我們計劃提升代碼生成的等級,從每次一條記錄表達式求值到向量化表達式求值,使用JIT來開發更好的作用於新型CPU的指令流水線,從而在同時處理多條記錄。

在通過表達式求值優化內部組件的CPU效率之外,我們還期望將代碼生成推到更廣泛的地方,其中一個就是shuffle過程中將數據從內存二進制格式轉換到wire-protocol。如之前所述,取代帶寬,shuffle通常會因數據系列化出現瓶頸。通過代碼生成,我們可以顯著地提升序列化吞吐量,從而反過來作用到shuffle網絡吞吐量的提升。

技術分享圖片

上面的圖片對比了單線程對800萬復雜行做shuffle的性能,分別使用的是Kryo和代碼生成,在速度上後者是前者的2倍以上。

Tungsten和未來的工作

在未來的幾個版本中,Tungsten將大幅度提升Spark的核心引擎。它首先將登陸Spark 1.4版本,包括了Dataframe API中聚合操作的內存管理,以及定制化序列化器。二進制內存管理的擴展和cache-aware數據結構將出現在Spark 1.5的部分項目(基於DataFrame模型)中。當然如果需要的話,這個提升也會應用到Spark RDD API。

對於Spark,Tungsten是一個長期的項目,因此也存在很多的可能性。值得關註的是,我們還將考察LLVM或者OpenCL,讓Spark應用程序可以利用新型CPU所提供的SSE/SIMD指令,以及GPU更好的並行性來提升機器學習和圖的計算。

Spark不變的目標就是提供一個單一的平臺,讓用戶可以從中獲得更好的分布式算法來匹配任何類型的數據處理任務。其中,性能一直是主要的目標之一,而Tungsten的目標就是讓Spark應用程序達到硬件性能的極限。更多詳情可以持續關註Databricks博客,以及6月舊金山的Spark Summit。

轉載自 http://www.csdn.net/article/2015-04-30/2824591-project-tungsten-bringing-spark-closer-to-bare-metal

英文原文參見 https://databricks.com/blog/2015/04/28/project-tungsten-bringing-spark-closer-to-bare-metal.html

Project Tungsten:讓Spark將硬件性能壓榨到極限(轉載)