『 Spark 』10. spark 應用程序性能優化|12 個優化方法

原文:http://litaotao.github.io/boost-Spark-application-performance

本系列是綜合了自己在學習spark過程中的理解記錄 + 對參考文章中的一些理解 + 個人實踐spark過程中的一些心得而來。寫這樣一個系列僅僅是爲了梳理個人學習spark的筆記記錄,所以一切以能夠理解爲主,沒有必要的細節就不會記錄了,而且文中有時候會出現英文原版文檔,只要不影響理解,都不翻譯了。若想深入瞭解,最好閱讀參考文章和官方文檔。

其次,本系列是基於目前最新的 spark 1.6.0 系列開始的,spark 目前的更新速度很快,記錄一下版本好還是必要的。

最後,如果各位覺得內容有誤,歡迎留言備註,所有留言 24 小時內必定回覆,非常感謝。

Tips: 如果插圖看起來不明顯,可以:1. 放大網頁;2. 新標籤中打開圖片,查看原圖哦。

1. 優化? Why? How? When? What?

“spark 應用程序也需要優化?”,很多人可能會有這個疑問,“不是已經有代碼生成器,執行優化器,pipeline 什麼的了的嗎?”。是的,spark 的確是有一些列強大的內置工具,讓你的代碼在執行時更快。但是,如果一切都依賴於工具,框架來做的話,我想那隻能說明兩個問題:1. 你對這個框架僅僅是知其然,而非知其所以然;2. 看來你也只是照葫蘆畫瓢而已,沒了你,別人也可以輕輕鬆鬆的寫這樣一個 spark 應用程序,so you are replaceable;

在做 spark 應用程序的優化的時候,從下面幾個點出發就夠了:

  • 爲什麼:因爲你的資源有限,因爲你的應用上生產環境了會有很多不穩定的因素,在上生產前做好優化和測試是唯一一個降低不穩定因素影響的辦法;
  • 怎麼做:web ui + log 是做優化的倚天劍和屠龍刀,能掌握好這兩點就可以了;
  • 何時做:應用開發成熟時,滿足業務要求時,就可以根據需求和時間安排開始做了;
  • 做什麼:一般來說,spark 應用程序 80% 的優化,都是集中在三個地方:內存,磁盤io,網絡io。再細點說,就是 driver,executor 的內存,shuffle 的設置,文件系統的配置,集羣的搭建,集羣和文件系統的搭建[e.g 儘量讓文件系統和集羣都在一個局域網內,網絡更快;如果可以,可以讓 driver 和 集羣也在一個局域網內,因爲有時候需要從 worker 返回數據到 driver]
  • 備註:千萬不要一心想着優化都從程序本身入手,雖然大多數時候都是程序自己的原因,但在入手檢查程序之前最好先確認所有的 worker 機器情況都正常哦。比如說機器負載,網絡情況。

下面這張圖來自 databricks 的一個分享 Tuning and Debugging Apache Spark ,很有意思,說得非常對啊,哈哈。

OK,下面我們來看看一些常見的優化方法。

2. repartition and coalesce

原文:

Spark provides the `repartition()` function, which shuffles the data 
across the network to create a new set of partitions. Keep in mind 
that repartitioning your data is a fairly expensive operation. Spark 
also has an optimized version of `repartition()` called `coalesce()` 
that allows avoiding data movement, but only if you are decreasing 
the number of RDD partitions. To know whether you can safely call 
coalesce(), you can check the size of the RDD using `rdd.partitions.size()` 
in Java/Scala and `rdd.getNumPartitions()` in Python and make sure 
that you are coalescing it to fewer partitions than it currently has.

總結:當要對 rdd 進行重新分片時,如果目標片區數量小於當前片區數量,那麼用coalesce ,不要用 repartition 。關於 partition 的更多優化細節,參考chapter 4 of Learning Spark

3. Passing Functions to Spark

In Python, we have three options for passing functions into Spark.

  • lambda expressions
word = rdd.filter(lambda s: "error" in s)
  • top-level functions
import my_personal_lib

word = rdd.filter(my_personal_lib.containsError)
  • locally defined functions
def containsError(s):
    return "error" in s
word = rdd.filter(containsError)

One issue to watch out for when passing functions is inadvertently serializing the object containing the function. When you pass a function that is the member of an object, or contains references to fields in an object (e.g., self.field), Spark sends the entire object to worker nodes, which can be much larger than the bit of information you need. Sometimes this can also cause your program to fail, if your class contains objects that Python can’t figure out how to pickle.

### wrong way

class SearchFunctions(object):
  def __init__(self, query):
      self.query = query
  def isMatch(self, s):
      return self.query in s
  def getMatchesFunctionReference(self, rdd):
      # Problem: references all of "self" in "self.isMatch"
      return rdd.filter(self.isMatch)
  def getMatchesMemberReference(self, rdd):
      # Problem: references all of "self" in "self.query"
      return rdd.filter(lambda x: self.query in x)

### the right way

class WordFunctions(object):
  ...
  def getMatchesNoReference(self, rdd):
      # Safe: extract only the field we need into a local variable
      query = self.query
      return rdd.filter(lambda x: query in x)

4. worker 的資源分配:cpu, memroy, executors

這個話題比較深,而且在不同的部署模式也不一樣 [standalone, yarn, mesos],這裏給不了什麼建議。唯一的一個宗旨是,不要一昧考慮把所有資源都獨立給到 spark 來用,要考慮到機器本身的一些進程,spark 依賴的一些進程,網絡情況,任務情況 [計算密集,IO密集,long-live task]等。

這裏只能推薦一些 video,slide 和 blog,具體情況具體分析,以後我遇到資源調優的時候再把實際案例發出來。

5. shuffle block size limitation

No Spark shuffle block can be greater than 2 GB — spark shuffle 裏的 block size 不能大於 2g 。

Spark 使用一個叫 ByteBuffer 的數據結構來作爲 shuffle 數據的緩存,但這個ByteBuffer 默認分配的內存是 2g,所以一旦 shuffle 的數據超過 2g 的時候,shuflle 過程會出錯。影響 shuffle 數據大小的因素有以下常見的幾個:

  • partition 的數量,partition 越多,分佈到每個 partition 上的數據越少,越不容易導致 shuffle 數據過大;
  • 數據分佈不均勻,一般是 groupByKey 後,存在某幾個 key 包含的數據過大,導致該 key 所在的 partition 上數據過大,有可能觸發後期 shuflle block 大於 2g;

一般解決這類辦法都是增加 partition 的數量, Top 5 Mistakes When Writing Spark Applications 這裏說可以預計讓每個 partition 上的數據爲 128MB 左右,僅供參考,還是需要具體場景具體分析,這裏只把原理講清楚就行了,並沒有一個完美的規範。

  • sc.textfile 時指定一個比較大的 partition number
  • spark.sql.shuffle.partitions
  • rdd.repartition
  • rdd.coalesce

TIPS :

在 partition 小於 2000 和大於 2000 的兩種場景下,Spark 使用不同的數據結構來在 shuffle 時記錄相關信息,在 partition 大於 2000 時,會有另一種更高效 [壓縮] 的數據結構來存儲信息。所以如果你的 partition 沒到 2000,但是很接近 2000,可以放心的把 partition 設置爲 2000 以上。

def apply(loc: BlockManagerId, uncompressedSizes: Array[Long]): MapStatus = {
    if (uncompressedSizes.length > 2000) {
      HighlyCompressedMapStatus(loc, uncompressedSizes)
    } else {
      new CompressedMapStatus(loc, uncompressedSizes)
    }
  }

6. level of parallel - partition

先來看看一個 stage 裏所有 task 運行的一些性能指標,其中的一些說明:

  • Scheduler Delay : spark 分配 task 所花費的時間
  • Executor Computing Time : executor 執行 task 所花費的時間
  • Getting Result Time : 獲取 task 執行結果所花費的時間
  • Result Serialization Time : task 執行結果序列化時間
  • Task Deserialization Time : task 反序列化時間
  • Shuffle Write Time : shuffle 寫數據時間
  • Shuffle Read Time : shuffle 讀數據所花費時間

而這裏要說的 level of parallel ,其實大多數情況下都是指 partition 的數量,partition 數量的變化會影響上面幾個指標的變動。我們調優的時候,很多時候都會看上面的指標變化情況。當 partition 變化的時候,上面幾個指標變動情況如下:

  • partition 過小[容易引入 data skew 問題]
    • Scheduler Delay : 無明顯變化
    • Executor Computing Time : 不穩定,有大有小,但平均下來比較大
    • Getting Result Time : 不穩定,有大有小,但平均下來比較大
    • Result Serialization Time : 不穩定,有大有小,但平均下來比較大
    • Task Deserialization Time : 不穩定,有大有小,但平均下來比較大
    • Shuffle Write Time : 不穩定,有大有小,但平均下來比較大
    • Shuffle Read Time : 不穩定,有大有小,但平均下來比較大
  • partition 過大
    • Scheduler Delay : 無明顯變化
    • Executor Computing Time : 比較穩定,平均下來比較小
    • Getting Result Time : 比較穩定,平均下來比較小
    • Result Serialization Time : 比較穩定,平均下來比較小
    • Task Deserialization Time : 比較穩定,平均下來比較小
    • Shuffle Write Time : 比較穩定,平均下來比較小
    • Shuffle Read Time : 比較穩定,平均下來比較小

那應該怎麼設置 partition 的數量呢?這裏同樣也沒有專門的公式和規範,一般都在嘗試幾次後有一個比較優化的結果。但宗旨是:儘量不要導致 data skew 問題,儘量讓每一個 task 執行的時間在一段變化不大的區間之內。

7. data skew

大多數時候,我們希望的分佈式計算帶來的好處應該是像下圖這樣的效果:

但是,有時候,卻是下面這種效果,這就是所謂的 data skew。即數據沒有被 大致均勻 的分佈到集羣中,這樣對一個 task 來說,整個 task 的執行時間取決於第一個數據塊被處理的時間。在很多分佈式系統中,data skew 都是一個很大的問題,比如說分佈式緩存,假設有 10 臺緩存機器,但有 50% 的數據都落到其中一臺機器上,那麼當這臺機器 down 掉之後,整個緩存的數據就會丟掉一般,緩存命中率至少 [肯定大於] 降低 50%。這也是很多分佈式緩存中要引入一致性哈希,要引入 虛擬節點 vnode 的原因。

一致性哈希原理圖:

回到正題,在 spark 中如何解決 data skew 的問題?首先明確這個問題的發生場景和根源:一般來說,都是 (key, value) 型數據中,key 的分佈不均勻,這種場景比較常見的方法是把 key 進行 salt 處理 [不知道 salt 中文應該怎麼說],比如說原來有 2 個 key (key1, key2),並且 key1 對應的數據集很大,而 key2 對應的數據集相對較小,可以把 key 擴張成多個 key (key1-1, key1-2, …, key1-n, key2-1, key2-2, …, key2-m) ,並且保證 key1-* 對應的數據都是原始 key1 對應的數據集上劃分而來的, key2-*上對應的數據都是原始的 key2 對應的數據集上劃分而來。這樣之後,我們有 m+n個 key,而且每個 key 對應的數據集都相對較小,並行度增加,每個並行程序處理的數據集大小差別不大,可以大大提速並行處理效率。在這兩個個分享裏都有提到這種方法:

8. avoid cartesian operation

rdd.cartesian 操作很耗時,特別是當數據集很大的時候,cartesian 的數量級都是平方級增長的,既耗時也耗空間。

>>> rdd = sc.parallelize([1, 2])
>>> sorted(rdd.cartesian(rdd).collect())
[(1, 1), (1, 2), (2, 1), (2, 2)]

9. avoid shuffle when possible

spark 中的 shuffle 默認是把上一個 stage 的數據寫到 disk 上,然後下一個 stage 再從 disk 上讀取數據。這裏的磁盤 IO 會對性能造成很大的影響,特別是數據量大的時候。

10. use reduceByKey instead of GroupByKey when possible

11. use treeReduce instead of reduce when possible

12. use Kryo serializer

spark 應用程序中,在對 RDD 進行 shuffle 和 cache 時,數據都是需要被序列化纔可以存儲的,此時除了 IO 外,數據序列化也可能是應用程序的瓶頸。這裏推薦使用 kryo 序列庫,在數據序列化時能保證較高的序列化效率。

sc_conf = SparkConf()
sc_conf.set("spark.serializer", "org.apache.spark.serializer.KryoSeria
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章