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

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

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

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

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

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

1. Memory Management和Binary Processinggithub

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

一直以來,Java對象產生的開銷都很是大。在UTF-8編碼上,簡單如「abcd」這樣的字符串也須要4個字節進行儲存。然而,到了JVM狀況就更糟糕了。爲了更加通用,它從新定製了本身的儲存機制——使用UTF-16方式編碼每一個字符(2字節),與此同時,每一個String對象還包含一個12字節的header,和一個8字節的哈希編碼,咱們能夠從 Java Object Layout工具的輸出上得到一個更清晰的理解:sql

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字節的空間來存儲!express

JVM對象帶來的另外一個問題是GC。從高等級上看,一般狀況下GC會將對象劃分紅兩種類型:第一種會有很高的allocation/deallocation(年輕代),另外一種的狀態很是穩定(年老代)。經過利用年輕代對象的瞬時特性,垃圾收集器能夠更有效率地對其進行管理。在GC能夠可靠地估算對象的生命週期時,這種機制能夠良好運行,可是若是隻是基於一個很短的時間,這個機制很顯然會遭遇困境,好比對象突然從年輕代進入到年老代。鑑於這種實現基於一個啓發和估計的原理,性能能夠經過GC調優的一些「黑魔法」來實現,所以你可能須要給JVM更多的參數讓其弄清楚對象的生命週期。apache

然而,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

相關文章
相關標籤/搜索