Spark性能調優系列二:Spark流計算重要性能參數測試與分析

在本系列的前一篇文章《Spark性能調優系列一:Spark的作業模型》中介紹了Spark作業的基礎模型之後,我們要在本文討論與Spark流計算性能相關的幾個重要參數,這些參數對作業執行的併發性和資源分配有着很大的影響,我們會析這些參數以怎樣的方式發揮作用,同時本文會討論一系列的測試用例,這些用例通過設定不同的參數和數值的組合來輔助我們驗證和理解這些參數配置的實際效果。本文原文鏈接: http://blog.csdn.net/bluishglc/article/details/80754855 轉載請註明出處。

參數介紹

spark.scheduler.mode

默認情況下,Spark對於Job的排期策略是FIFO,也就是spark.scheduler.mode的默認值是FIFO,這一策略的含義是:先提交的作業會先被執行,但這也不是絕對的,如果當前執行的Job並沒有佔用到集羣的全部資源(還有空閒的executors或cpu cores)則Spark會讓後續的作業立即執行,這顯然是“明智”的。當然,反方向上的極端是當一個Job很“重”需要耗用大量資源長時間才能執行結束時,後續的Job都會Delay!

從Spark 0.8開始,引入了一種新的作業排期策略:“FAIR”,顧名思義,就是讓所有的Job能獲得相對“均等”的機會來執行。具體的作法就是:將所有job的task按一種“round robin”(輪詢調度)的方式執行。注意,這裏的執行粒度“下放”到了Task,並且是跨job的,這樣就變成在Job之下按更細粒度的單位:Task進行輪詢式的執行,宏觀上起到了Job並行的效果。

應該說spark.scheduler.mode是一個面向job級別的配置項,但是又不是這麼簡單,當它是FIFO時,我們可以認爲它的“作用”粒度是Job,當它是FAIR時,爲了真正能使得各個作業獲得均等的執行機會,實際上的作業調度已經細化到了Task級別,在Spark的源代碼org.apache.spark.scheduler.Pool#getSortedTaskSetQueue中我們可以看到,在同一個pool中,所有作業的Task都會依據配置的spark.scheduler.mode來對tasks統一進行排序,然後依次提交給Spark Core執行,所以說,實際的控制粒度是在task層面上的,但是對於這一點的理解也不要過,從實際並行作業的event timeline上看,這種並行並不是在兩個作業中頻繁地交替執行Tasks(這樣的做話顯然是代價巨大的), 而是在一個較長的作業中間“嵌套”進去一到兩個相對短作業一直到它們執行結束再切回到長作業繼續執行。

  override def getSortedTaskSetQueue: ArrayBuffer[TaskSetManager] = {
    var sortedTaskSetQueue = new ArrayBuffer[TaskSetManager]
    val sortedSchedulableQueue =
      schedulableQueue.asScala.toSeq.sortWith(taskSetSchedulingAlgorithm.comparator)
    for (schedulable <- sortedSchedulableQueue) {
      sortedTaskSetQueue ++= schedulable.getSortedTaskSetQueue
    }
    sortedTaskSetQueue
  }

spark.streaming.concurrentJobs

concurrentJobs要配合FAIR模式一起工作,同時你還要清楚你的集羣是否有足夠的資源來支撐更多併發作業,如果沒有適當的資源的話,加大concurrentJobs的數值是不是會有顯著的並行度和吞吐量的提升的。這裏還是要再叮囑一下:即使是concurrentJobs=1,如果集羣有空閒的計算資源,Spark也同樣會激活新的作業去並行執行的。

scheduler線程池

FAIR模式給每一個作業提供了均等的執行機會,但是這未必能解決這樣一類問題:假定在一個Spark Streaming應用裏有一個很重的作業,比如有兩三百個Task,同時還有幾個很小的作業,可能只有能幾個Task,按FAIR的輪詢調度,每個作業都有均等的機會執行各自的Task, 這樣形成的結果是在一個相對固定的時間週期內,長Job作業與衆多短作業執行完畢的數量是一樣,比例都是1:1:1…., 那麼,如果我們的需求是讓這些短作業執行的頻率加快,以更加實時的速率來處理數據,那麼就勢必要給到這些短作業更多的資源來執行Task, 那麼這時就需要爲這些作業引入獨立的執行線程池,並配置相應的資源佔用權重來解決了,具體的做法就是在代碼中加入

// Assuming sc is your SparkContext variable
sc.setLocalProperty("spark.scheduler.pool", "pool1")

這樣,在當前線程(即調用RDD.save, count, collect的線程)中提交的作業都會使用這個指定的pool運行作業。

在沒有引入任何上述代碼時,所有的作業實際上是在公用一個root的線程池,這樣整個集羣的計算資源都分配給了這一個root線程池,然後,如果我們制定了spark.scheduler.mode=FAIR,那含義就是:所有的作業會有均等的機會輪流佔用全部的資源執行任務。那麼就會出現我們前面提到的那個問題。而解決方法就是:既然pool是對計算資源的劃分,那麼我們就可以爲不同的作業引入多個獨立的pool, 然後給這些pool分配相應的權重讓它們來按比例來分配整體的計算資源,然後在pool內部再FAIR模式讓其對應的那些Job以均等機會獲取到制定配置的資源去執行!這聽上去有一點抽象,我們使用會在後面的測試用例中詳細的說明。

測試用例

本文全部測試用例涉及到4個作業:persist_cpu_usage,persist_mem_free,evaluate_cpu_usage,evaluate_mem_free,其中前兩個是短作業,只有一個Task,沒有任何shuffle動作,後兩個是長作業,都含有shuffle操作,我們是在一個單節點集羣上執行測試用例的,計算資源有限,爲了拉大長短作業的執行時間差,我們特意將spark.sql.shuffle.partitions設置成了100,這不是從性能最優來考慮的,如果從性能考慮,我們會把大大降低shuffle.partitions的數值。

Test Case Group 1: FIFO vs. FAIR

Test Case 1-1: FIFO

  • 配置
spark.num.executors=1
spark.executor.cores=1
spark.streaming.concurrentJobs=1
spark.scheduler.mode=FIFO
spark.sql.shuffle.partitions=100
所有Job使用default pool (spark.scheduler.pool=default)
  • 結論

看上去所有的Job是在併發執行的,但是Job頁面在顯示上是有些歧義的,它顯示的進度條是從作業提交到執行結束的整個時間跨度,在本例上,前四個作業的提交動作是一個很輕量的動作,在一開始的很短的時間內變相繼完成了,所以看上去幾乎是在同時開始並且在“並行”執行,但是實際的情況是:作業提交後,是按0,1,2,3順序執行的,因爲我們限定了非常有限的資源,Job的排期使用的又是FIFO,所以,這此Job的實際上按先後順序依此執行的,也就是說越後面的Job在提交之後的等待時間越多!這一點我們後面會從Task頁面得到驗證。

Stage 0和Stage 1分屬兩個獨立的作業,且都只有一個Task,第二個Job的Task緊緊跟隨第一個Job的Task執行完成之後開始執行,這說明集羣Task的並行度(slot)是1,並且作業的排期是FIFO模式,所以所有的Job都是先進先出的串行執行。

  • Job
    這裏寫圖片描述

  • Stage 0

這裏寫圖片描述

  • Stage 1

這裏寫圖片描述

Test Case 1-2: FAIR

  • 配置

···
spark.num.executors=1
spark.executor.cores=1
spark.streaming.concurrentJobs=1
spark.scheduler.mode=FAIR
spark.sql.shuffle.partitions=100
···

  • 結論

FAIR模式下,作業視圖和前面的FIFO區別不大,在提交時,都可以很快完成,造成各個作業是在“並行”的假象,但實際上的並行度是不能依賴作業視圖來判斷的(在計算資源充足的情況下另當別論,那時所有提交的作業往往可以立即被分配資源執行,這種情況下可以認爲作業視圖中藍色的進度條就是做業的實際執行時間)因爲我們的測試中資源都是嚴格限定的,這會導致很多作業從提交到能夠得到資源開始執行之間有大量的等待時間,所以不能把這些藍色進度條當作實際的Job執行時間。

但是在FAIR模式下,確實發生了一些不同,我們繼續看:Stage 6 & 4是對FAIR模式的一個很好的詮釋,從Stage 6的截圖上可以看到在02:12:24到02:12:56之間有32秒的空白,而這段時間正好是Stage 4在執行,兩個Stage隸屬兩個不同的Job,產生這一結果的原因是在FAIR模式下,不同的Job會獲得同等的執行機會,也就是在所有已提交的作業中,Spark使用輪詢調度的方式從各個作業中依此選取Task執行。但是本例在展示跨Job的Task輪詢調度上顯得不是很典型,這是還是因爲我們在這個示例中的限定了很有限的計算資源導致的,後續的示例中會有更好的演示示例。

  • Job
    這裏寫圖片描述

  • Stage 6
    這裏寫圖片描述

  • Stage 4

這裏寫圖片描述

Test Case Group 2: FIFO 1 Slot vs. FIFO 4 Slots

Test Case 2-1: FIFO 1 Slot

這個Case就是 Test Case 1-1: FIFO

Test Case 2-2: FIFO 4 Slots

  • 配置
spark.num.executors=1
spark.executor.cores=4
spark.streaming.concurrentJobs=1
spark.scheduler.mode=FIFO
spark.sql.shuffle.partitions=100
  • 結論

在cores從1提長到4之後,作業層面一個宏觀的改變是作業的併發度得到了提升!後續Task層面的執行歷史能更準確地體現這一點。這裏要着重解釋的是雖然spark.scheduler.mode=FIFO,但是這並不意味着不存在作業級別的併發,Spark文檔中對FIFO的介紹中也特別強調道:如果當前提交的作業並沒有佔據集羣的全部資源,則後續的作業會跟進提交併執行!這在我們這個測試用例中得到了很好的體現。我們可以看到在08:51:55到08:52:10這段時間,Job 0(Stage 0)和Job 1(Stage 1)這兩個作業是嚴格意義上並行的!並且這類情況在後續頻繁發生,這就是對FIFO模式在資源充裕時允許並行多個作業的展示。

另外一個要補充的是,在擴充爲4核之後,像Stage 3這樣比較重的計算,並行程度得到提升,執行時間大幅度縮短,對比前面所有1核的測試用例我們可以發現,用於只有一個可以的core, 每個stage,特別是有100個task的重的stage, 它們的task都是串行的,在同一個時間點上,只有一個在running的task,而本例中core提升爲4之後,同樣的stage, 任意時刻總是有4個running的task.

  • Job

這裏寫圖片描述

  • Stage 0
    這裏寫圖片描述

  • Stage 1

這裏寫圖片描述

  • Stage 2

這裏寫圖片描述

  • Stage 3

這裏寫圖片描述

Test Case Group 3: FIFO 4 Slots vs. FAIR 4 Slots

Test Case 3-1: FIFO 4 Slots

這個Case就是 Test Case 2-2: FIFO 4 Slots

Test Case 3-2:FAIR 4 Slots

  • 配置
spark.num.executors=1
spark.executor.cores=4
spark.streaming.concurrentJobs=1
spark.scheduler.mode=FAIR
spark.sql.shuffle.partitions=100
  • 結論

首先,對比, 本用例改爲FAIR模式後最大的一個變化就是作業的整體吞吐量由原來的91個Job提升到了119個Job(concurrentJobs並沒有變,依然是默認值1), 這有力地證明了FAIR模式可以提升作業的並行度,當然,更加顯著的效果要配合提升concurrentJobs,我們會在下一個測試用例中瞭解到。

從Job頁面以及前三個Stage上看,本測試和對比測試《Test Case 3-1: FIFO 4 Slots》是很類似的,對於FIFO我們前面已有解釋,但是對於FAIR模式我們也要認清一點:即使concurrentJobs=1,Spark也會遵從與FIFO同樣的原則:如果集羣當前尚有空閒資源,則處於等待狀態中的作業一定會被激活。例如在本例中,我們就可以從Job截圖上看到,雖然concurrentJobs=1,但是依然有兩個Active的Job。

當然,FAIR模式作業運行的一個最基本的“特徵”就是一個需要話費長時間完成的長作業會在中途被“打斷”,Spark留出資源給到其他的作業去執行,例如本例stage 3和stage 6關係,同樣的情況也發生在《Test Case 1-2: FAIR》

  • Job
    這裏寫圖片描述

  • Stage 0

這裏寫圖片描述

  • Stage 1

這裏寫圖片描述

  • Stage 2

這裏寫圖片描述

  • Stage 3

這裏寫圖片描述

  • Stage 6
    這裏寫圖片描述

Test Case Group 4: FAIR 4 Slots, concurrentJobs = 1 vs. FAIR 4 Slots, concurrentJobs = 4

Test Case 4-1: FAIR 4 Slots, concurrentJobs = 1

該測試用例就是《Test Case 3-2:FAIR 4 Slots》

Test Case 4-2: FAIR 4 Slots, concurrentJobs = 4

  • 配置
spark.num.executors=1
spark.executor.cores=4
spark.streaming.concurrentJobs=4
spark.scheduler.mode=FAIR
spark.sql.shuffle.partitions=100
  • 結論

concurrentJobs變爲4之後,我們可以在作業視圖中觀察到有4個Active的作業,顧名思義,concurrentJobs參數的作用就是Spark Streaming允許並行作業的數量,具體到代碼層面上就是用來執行作業的線程數。但是,從運行結果上看,可能讓人出乎意料:concurrentJobs變爲4之後,作業的整個吞吐量沒有得到明顯的提升!這說明作業執行的瓶頸已不再是併發度了,原因是我們前面所有concurrentJobs=1的測試用例中,配置上是限定了一個併發作業,但是在鑑於部分作業只需要一個core,所以集羣中某些時刻總是有空餘資源,這時候Spark會啓動新的作業,從而讓硬件資源充分利用。即使我們在這個測試中人爲的把作業併發數改爲了4,也不會在整體的資源利用率上有所提升了!

補充說明:

  1. 不管是FIFO還FAIR(concurrentJobs=1),某一時刻都是可能存在兩上以上的Active Job的!!這是因爲只要還有空閒資源,Spark就會激活等待中的作業!它們的Task就會進入到Pool的Task隊列裏!

    • Job

這裏寫圖片描述

  • Stage 0
    這裏寫圖片描述

  • Stage 1

這裏寫圖片描述

  • Stage 2
    這裏寫圖片描述

  • Stage 6

這裏寫圖片描述

  • Stage 4

這裏寫圖片描述

Test Case Group 5: FAIR 4 Slots, concurrentJobs = 4, Root Pool vs. FAIR 4 Slots, concurrentJobs = 4, Independent Pools

Test Case 5-1: FAIR 4 Slots, concurrentJobs = 4, Root Pool

該測試用例就是《Test Case 4-2: FAIR 4 Slots, concurrentJobs = 4》

Test Case 5-2: FAIR 4 Slots, concurrentJobs = 4, Independent Pools

  • 配置
spark.num.executors=1
spark.executor.cores=4
spark.streaming.concurrentJobs=4
spark.scheduler.mode=FAIR
spark.sql.shuffle.partitions=100
//在4個流上都使用了獨立的pool:
sparkSession.sparkContext.setLocalProperty("spark.scheduler.pool", s"pool_persist_$metric")
sparkSession.sparkContext.setLocalProperty("spark.scheduler.pool", s"pool_evaluate_$metric")

4個獨立pool的配置:

<?xml version="1.0"?>
<allocations>
    <pool name="pool_persist_cpu_usage">
        <schedulingMode>FAIR</schedulingMode>
        <weight>1</weight>
        <minShare>0</minShare>
    </pool>
    <pool name="pool_persist_mem_free">
        <schedulingMode>FAIR</schedulingMode>
        <weight>1</weight>
        <minShare>0</minShare>
    </pool>
    <pool name="pool_evaluate_cpu_usage">
        <schedulingMode>FAIR</schedulingMode>
        <weight>1</weight>
        <minShare>0</minShare>
    </pool>
    <pool name="pool_evaluate_mem_free">
        <schedulingMode>FAIR</schedulingMode>
        <weight>1</weight>
        <minShare>0</minShare>
    </pool>
</allocations>
  • 結論

這是變化最劇烈的一個測試用例,也就是意味着spark.scheduler.pool是一個效用非常大的配置項!我們先看一下Job頁面,最大的變化是完成的作業數激增到935個!在之前的所有用例裏, 四個作業(包含兩個短作業:persist_cpu_usage和persist_mem_free以及兩個長作業: evaluate_cpu_usage和evaluate_mem_free),無論長短,都是保持1:1:1:1的比例,而在本測試中兩個短作業persist_cpu_usage和persist_mem_free的數量明顯增多,遠遠超過了兩個長作業數量的N倍!那麼造成這一現象的原因是什麼呢?關鍵點就是pool的weight屬性!我們來看一下Spark官方文檔對pool的weight屬性的介紹:

weight: This controls the pool’s share of the cluster relative to other pools. By default, all pools have a weight of 1. If you give a specific pool a weight of 2, for example, it will get 2x more resources as other active pools. Setting a high weight such as 1000 also makes it possible to implement priority between pools—in essence, the weight-1000 pool will always get to launch tasks first whenever it has jobs active.

不難看出:對於多個pool, 它們的weight決定了它們所能分得的硬件資源的“份額”!我們來對比地理解一下這個差別:在前面所有使用FAIR模式的測試用例中,都沒有特別地配置pool, 它們都使用同一個默認的root pool, 則在同一個pool內,應用FAIR模式,四個作業得到的是均等的執行機會,因此4個作業完成的比例是1:1:1:1,而在本測試用例中,我們爲四個作業配備了獨立的pool, 而pool與pool之間是按佔有資源的份額劃分的,這就變成四個作業獲得的執行資源是均等的,這一變化的實質效果就是提升了短作業佔有資源的份額!因爲之前4個作業是按均等的執行機會分配的,對於那些短作業,它們獲得了資源之後,只需要很短的時間就完成了工作,所以宏觀上它們實際佔用資源的份額並不高,而現在的模式變成了,分配給它們均等的資源,則實質上短作業獲得了更多的資源份額,因此完成作業數量就成倍地激增了!

但是這一定是好的嗎?這要看我們到底想如何協調長短作業之間的資源競爭關係!因爲總的資源就這麼多了,分配給了短作業更多的資源就意味了長作業獲得的資源別消減了,這可以從長作業完成的數量上得到驗證,在本測試用例中,一共完成了28個長作業(evaluate_cpu_usage和evaluate_mem_free),而在前一個對比測試中,完成的長作業數量是122/2=61個,所以總結下來最重點的結論是:pool是從整體計算資源上按份額(weight)進行切分的,在一個pool內部,如果是FAIR模式,則作業是按均等的執行機會來劃分的。

  • Job
    這裏寫圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章