Spark的效能調優

筆尖的痕發表於2016-03-10

下面這些關於Spark的效能調優項,有的是來自官方的,有的是來自別的的工程師,有的則是我自己總結的。

Data Serialization,預設使用的是Java Serialization,這個程式設計師最熟悉,但是效能、空間表現都比較差。還有一個選項是Kryo Serialization,更快,壓縮率也更高,但是並非支援任意類的序列化。

Memory Tuning,Java物件會佔用原始資料2~5倍甚至更多的空間。最好的檢測物件記憶體消耗的辦法就是建立RDD,然後放到cache裡面去,然後在UI 上面看storage的變化;當然也可以使用SizeEstimator來估算。使用-XX:+UseCompressedOops選項可以壓縮指標(8 位元組變成4位元組)。在呼叫collect等等API的時候也要小心——大塊資料往記憶體拷貝的時候心裡要清楚。

GC調優。列印GC資訊:-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps。預設60%的executor記憶體可以被用來作為RDD的快取,因此只有40%的記憶體可以被用來作為物件建立的空間,這一點可以通過設定spark.storage.memoryFraction改變。如果有很多小物件建立,但是這些物件在不完全GC的過程中就可以回收,那麼增大Eden區會有一定幫助。如果有任務從HDFS拷貝資料,記憶體消耗有一個簡單的估算公式——比如HDFS的block size是64MB,工作區內有4個task拷貝資料,而解壓縮一個block要增大3倍大小,那麼記憶體消耗就是:4*3*64MB。另外,工作中遇到過這樣的一個問題:GC預設情況下有一個限制,預設是GC時間不能超過2%的CPU時間,但是如果大量物件建立(在Spark裡很容易出現,程式碼模式就是一個RDD轉下一個RDD),就會導致大量的GC時間,從而出現OutOfMemoryError: GC overhead limit exceeded,可以通過設定-XX:-UseGCOverheadLimit關掉它。

Level of Parallelism。Spark根據要處理的檔案大小設定map task的數量(也可以通過SparkContext.textFile顯式指定),並且使用最大的parent RDD的分割槽數量來執行reduce操作。設定level of parallelism或者屬性spark.default.parallelism來改變並行級別,通常來說,每一個CPU核可以分配2~3個task

Reduce Task的記憶體使用。在某些情況下reduce task特別消耗記憶體,比如當shuffle出現的時候,比如sortByKey、groupByKey、reduceByKey和join等,要在記憶體裡面建立一個巨大的hash table。其中一個解決辦法是增大level of parallelism,這樣每個task的輸入規模就相應減小。

Broadcasting Large Variables。在task使用靜態大物件的時候,可以把它broadcast出去。Spark會列印序列化後的大小,通常來說如果它超過20KB就值得這麼做。有一種常見情形是,一個大表join一個小表,把小表broadcast後,大表的資料就不需要在各個node之間瘋跑,安安靜靜地呆在本地等小表broadcast過來就好了

Data Locality。資料和程式碼要放到一起才能處理,通常程式碼總比資料要小一些,因此把程式碼送到各處會更快。Data Locality是資料和處理的程式碼在屋裡空間上接近的程度:PROCESS_LOCAL(同一個JVM)、NODE_LOCAL(同一個node,比如資料在HDFS上,但是和程式碼在同一個node)、NO_PREF、RACK_LOCAL(不在同一個server,但在同一個機架)、ANY。當然優先順序從高到低,但是如果在空閒的executor上面沒有未處理資料了,那麼就有兩個選擇:(1)要麼等如今繁忙的CPU閒下來處理儘可能“本地”的資料,(1)要麼就不等直接啟動task去處理相對遠端的資料。預設當這種情況發生Spark會等一會兒(spark.locality),即策略(1),如果繁忙的CPU停不下來,就會執行策略(2)。

檔案儲存和讀取的優化。比如對於一些case而言,如果只需要某幾列,使用rcfile和parquet這樣的格式會大大減少檔案讀取成本。再有就是儲存檔案到S3上或者HDFS上,可以根據情況選擇更合適的格式,比如壓縮率更高的格式。

檔案分片。比如在S3上面就支援檔案以分片形式存放,字尾是partXX。使用coalesce方法來設定分成多少片,這個調整成並行級別或者其整數倍可以提高讀寫效能。但是太高太低都不好,太低了沒法充分利用S3並行讀寫的能力,太高了則是小檔案太多,預處理、合併、連線建立等等都是時間開銷啊,讀寫還容易超過throttle。

Spark的Speculation。通過設定spark.speculation等幾個相關選項,可以讓Spark在發現某些task執行特別慢的時候,可以在不等待完成的情況下被重新執行,最後相同的task只要有一個執行完了,那麼最快執行完的那個結果就會被採納。

減少Shuffle。其實Spark的計算往往很快,但是大量開銷都花在網路和IO上面,而shuffle就是一個典型。舉個例子,如果(k, v1) join (k, v2) => (k, v3),那麼,這種情況其實Spark是優化得非常好的,因為需要join的都在一個node的一個partition裡面,join很快完成,結果也是在同一個node(這一系列操作可以被放在同一個stage裡面)。但是如果資料結構被設計為(obj1) join (obj2) => (obj3),而其中的join條件為obj1.column1 == obj2.column1,這個時候往往就被迫shuffle了,因為不再有同一個key使得資料在同一個node上的強保證。在一定要shuffle的情況下,儘可能減少shuffle前的資料規模,比如這個避免groupByKey的例子

合理的partition。運算過程中資料量時大時小,選擇合適的partition數量關係重大,如果太多partition就導致有很多小任務和空任務產生;如果太少則導致運算資源沒法充分利用,必要時候可以使用repartition來調整,不過它也不是沒有代價的,其中一個最主要代價就是shuffle。再有一個常見問題是資料大小差異太大,這種情況主要是資料的partition的key其實取值並不均勻造成的(預設使用 HashPartitioner),需要改進這一點,比如重寫hash演算法。測試的時候想知道partition的數量可以呼叫 rdd.partitions().size()獲知。

其它一些內容。同事發現Spark1.0.1的速度居然比Spark1.1和1.2快很多,而Spark1.2則比前幾個版本要吃掉多得多的記憶體。

可供參考的文件:官方調優文件Tuning Spark,Spark配置的官方文件,Spark Programming Guide,JVMGC調優文件,JVM效能調優文件,How-to: Tune Your Apache Spark Jobs part-1 & part-2

相關文章