Spark 作業資源調度

北風網spark學習筆記

靜態資源分配原理

spark提供了許多功能用來在集羣中同時調度多個作業。首先,回想一下,每個spark作業都會運行自己獨立的一批executor進程,此時集羣管理器會爲我們提供同時調度多個作業的功能。第二,在每個spark作業內部,多個job也可以並行執行,比如說spark-shell就是一個spark application,但是隨着我們輸入scala rdd action類代碼,就會觸發多個job,多個job是可以並行執行的。爲這種情況,spark也提供了不同的調度器來在一個application內部調度多個job。

先來看一下多個作業的同時調度

靜態資源分配

  • 當一個spark application運行在集羣中時,會獲取一批獨立的executor進程專門爲自己服務,比如運行task和存儲數據。如果多個用戶同時在使用一個集羣,並且同時提交多個作業,那麼根據cluster manager的不同,有幾種不同的方式來管理作業間的資源分配。
  • 最簡單的一種方式,是所有cluster manager都提供的,也就是靜態資源分配。在這種方式下,每個作業都會被給予一個它能使用的最大資源量的限額,並且可以在運行期間持有這些資源。這是spark standalone集羣和YARN集羣使用的默認方式。
  • Standalone集羣: 默認情況下,提交到standalone集羣上的多個作業,會通過FIFO的方式來運行,每個作業都會嘗試獲取所有的資源。可以限制每個作業能夠使用的cpu core的最大數量(spark.cores.max),或者設置每個作業的默認cpu core使用量(spark.deploy.defaultCores)。最後,除了控制cpu core之外,每個作業的spark.executor.memory也用來控制它的最大內存的使用。
  • YARN: --num-executors屬性用來配置作業可以在集羣中分配到多少個executor,--executor-memory和--executor-cores可以控制每個executor能夠使用的資源。
  • 要注意的是,沒有一種cluster manager可以提供多個作業間的內存共享功能。如果你想要通過這種方式來在多個作業間共享數據,我們建議就運行一個spark作業,但是可以接收網絡請求,並對相同RDD的進行計算操作。在未來的版本中,內存存儲系統,比如Tachyon會提供其他的方式來共享RDD數據。

動態資源分配原理

動態資源分配原理

  • spark 1.2開始,引入了一種根據作業負載動態分配集羣資源給你的多個作業的功能。這意味着你的作業在申請到了資源之後,可以在使用完之後將資源還給cluster manager,而且可以在之後有需要的時候再次申請這些資源。這個功能對於多個作業在集羣中共享資源是非常有用的。如果部分資源被分配給了一個作業,然後出現了空閒,那麼可以還給cluster manager的資源池中,並且被其他作業使用。在spark中,動態資源分配在executor粒度上被實現,可以通過spark.dynamicAllocation.enabled來啓用。

資源分配策略

  • 一個較高的角度來說,當executor不再被使用的時候,spark就應該釋放這些executor,並且在需要的時候再次獲取這些executor。因爲沒有一個絕對的方法去預測一個未來可能會運行一個task的executor應該被移除掉,或者一個新的executor應該別加入,我們需要一系列的探索式算法來決定什麼應該移除和申請executor。

申請策略

  • 一個啓用了動態資源分配的spark作業會在它有pending住的task等待被調度時,申請額外的executor。這個條件必要地暗示了,已經存在的executor是不足以同時運行所有的task的,這些task已經提交了,但是沒有完成。
  • driver會輪詢式地申請executor。當在一定時間內(spark.dynamicAllocation.schedulerBacklogTimeout)有pending的task時,就會觸發真正的executor申請,然後每隔一定時間後(spark.dynamicAllocation.sustainedSchedulerBacklogTimeout),如果又有pending的task了,則再次觸發申請操作。此外,每一輪申請到的executor數量都會比上一輪要增加。舉例來說,一個作業需要增加一個executor在第一輪申請時,那麼在後續的一輪中會申請2個、4個、8個executor。
  • 每輪增加executor數量的原因主要有兩方面。第一,一個作業應該在開始謹慎地申請以防它只需要一點點executor就足夠了。第二,作業應該會隨着時間的推移逐漸增加它的資源使用量,以防突然大量executor被增加進來。

移除策略

  • 移除一個executor的策略比較簡單。一個spark作業會在它的executor出現了空閒超過一定時間後(spark.dynamicAllocation.executorIdleTimeout),被移除掉。要注意,在大多數環境下,這個條件都是跟申請條件互斥的,因爲如果有task被pending住的話,executor是不該是空閒的。

executor如何優雅地被釋放掉

  • 在使用動態分配之前,executor無論是發生了故障失敗,還是關聯的application退出了,都還是存在的。在所有場景中,executor關聯的所有狀態都不再被需要,並且可以被安全地拋棄。使用動態分配之後,executor移除之後,作業還是存在的。如果作業嘗試獲取executor寫的中間狀態數據,就需要去重新計算哪些數據。因此,spark需要一種機制來優雅地卸載executor,在移除它之前要保護它的狀態。
  • 解決方案就是使用一個外部的shuffle服務來保存每個executor的中間寫狀態,這也是spark 1.2引入的特性。這個服務是一個長時間運行的進程,集羣的每個節點上都會運行一個,位你的spark作業和executor服務。如果服務被啓用了,那麼spark executor會在shuffle write和read時,將數據寫入該服務,並從該服務獲取數據。這意味着所有executor寫的shuffle數據都可以在executor聲明週期之外繼續使用。
  • 除了寫shuffle文件,executor也會在內存或磁盤中持久化數據。當一個executor被移除掉時,所有緩存的數據都會消失。目前還沒有有效的方案。在未來的版本中,緩存的數據可能會通過堆外存儲來進行保存,就像external shuffle service保存shuffle write文件一樣。

standalone模式下使用動態資源分配

./sbin/start-shuffle-service.sh


spark-shell --master spark://192.168.75.101:7077 \
--jars /usr/local/hive/lib/mysql-connector-java-5.1.17.jar \
--conf spark.shuffle.service.enabled=true \
--conf spark.dynamicAllocation.enabled=true \
--conf spark.shuffle.service.port=7337 
  1. 啓動external shuffle service
  2. 啓動spark-shell,啓用動態資源分配
  3. 過60s,發現打印日誌,說executor被removed,executor進程也沒了
  4. 然後動手寫一個wordcount程序,最後提交job的時候,會動態申請一個新的executor,出來一個新的executor進程
  5. 然後整個作業執行完畢,證明external shuffle service+動態資源分配,流程可以走通
  6. 再等60s,executor又被釋放掉

yarn模式下使用動態資源分配

先停止之前爲standalone集羣啓動的shuffle service

./sbin/stop-shuffle-service.sh

配置

動態資源分配功能使用的所有配置,都是以spark.dynamicAllocation作爲前綴的。要啓用這個功能,你的作業必須將spark.dynamicAllocation.enabled設置爲true。其他相關的配置之後會詳細說明。

此外,你的作業必須有一個外部shuffle服務(external shuffle service)。這個服務的目的是去保存executor的shuffle write文件,從而讓executor可以被安全地移除。要啓用這個服務,可以將spark.shuffle.service.enabled設置爲true。在YARN中,這個外部shuffle service是由org.apache.spark.yarn.network.YarnShuffleService實現的,在每個NodeManager中都會運行。要啓用這個服務,需要使用以下步驟:

  1. 使用預編譯好的spark版本。

  2. 定位到spark-<version>-yarn-shuffle.jar。這個應該在$SPARK_HOME/lib目錄下。

  3. 將上面的jar加入到所有NodeManager的classpath/usr/local/hadoop/share/hadoop/yarn/lib/

  4. yarn-site.xml中,將yarn.nodemanager.aux-services設置爲spark_shuffle,將yarn.nodemanager.aux-services.spark_shuffle.class設置爲org.apache.spark.network.yarn.YarnShuffleService

    <property>
      <name>yarn.nodemanager.aux-services</name>
      <value>spark_shuffle</value>
    </property>
    <property>
      <name>yarn.nodemanager.aux-services.spark_shuffle.class</name>
      <value>org.apache.spark.network.yarn.YarnShuffleService</value>
    </property>
    <property>
      <name>yarn.log-aggregation-enable</name>
      <value>true</value>
    </property>
    
  5. 重啓所有NodeManager

spark-shell --master yarn-client \
--jars /usr/local/hive/lib/mysql-connector-java-5.1.17.jar \
--conf spark.shuffle.service.enabled=true \
--conf spark.dynamicAllocation.enabled=true \
--conf spark.shuffle.service.port=7337
  1. 首先配置好yarn的shuffle service,然後重啓集羣
  2. 接着呢,啓動spark shell,並啓用動態資源分配,但是這裏跟standalone不一樣,上來不會立刻申請executor
  3. 接着執行wordcount,會嘗試動態申請executor,並且申請到後,執行job,在spark web ui上,有兩個executor
  4. 過了一會兒,60s過後,executor由於空閒,所以自動被釋放掉了,在看spark web ui,沒有executor了

多個job資源調度原理

  • 在一個spark作業內部,多個並行的job是可以同時運行的。對於job,就是一個spark action操作觸發的計算單元。spark的調度器是完全線程安全的,而且支持一個spark application來服務多個網絡請求,以及併發執行多個job。
  • 默認情況下,spark的調度會使用FIFO的方式來調度多個job。每個job都會被劃分爲多個stage,而且第一個job會對所有可用的資源獲取優先使用權,並且讓它的stage的task去運行,然後第二個job再獲取資源的使用權,以此類推。如果隊列頭部的job不需要使用整個集羣資源,之後的job可以立即運行,但是如果隊列頭部的job使用了集羣幾乎所有的資源,那麼之後的job的運行會被推遲。
  • 從spark 0.8開始,我們是可以在多個job之間配置公平的調度器的。在公平的資源共享策略下,spark會將多個job的task使用一種輪詢的方式來分配資源和執行,所以所有的job都有一個基本公平的機會去使用集羣的資源。這就意味着,即使運行時間很長的job先提交併在運行了,之後提交的運行時間較短的job,也同樣可以立即獲取到資源並且運行,而不會等待運行時間很長的job結束之後才能獲取到資源。這種模式對於多個併發的job是最好的一種調度方式。

Fair Scheduler使用詳解

  • 要啓用Fair Scheduler,只要簡單地將spark.scheduler.mode屬性設置爲FAIR即可
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.scheduler.mode", "FAIR")
val sc = new SparkContext(conf)

或者

--conf spark.scheduler.mode=FAIR
  • fair scheduler也支持將job分成多個組並放入多個池中,以及爲每個池設置不同的調度優先級。這個feature對於將重要的和不重要的job隔離運行的情況非常有用,可以爲重要的job分配一個池,並給予更高的優先級; 爲不重要的job分配另一個池,並給予較低的優先級。
  • 默認情況下,新提交的job會進入一個默認池,但是job的池是可以通過spark.scheduler.pool屬性來設置的。
  • 如果spark application是作爲一個服務啓動的,SparkContext 7*24小時長時間存在,然後服務每次接收到一個請求,就用一個子線程去服務它:
    1. 在子線程內部,去執行一系列的RDD算子以及代碼來觸發job的執行
    2. 在子線程內部,可以調用SparkContext.setLocalProperty("spark.scheduler.pool", "pool1")
  • 在設置這個屬性之後,所有在這個線程中提交的job都會進入這個池中。同樣也可以通過將該屬性設置爲null來清空池子。

池的默認行爲

  • 默認情況下,每個池子都會對集羣資源有相同的優先使用權,但是在每個池內,job會使用FIFO的模式來執行。舉例來說,如果要爲每個用戶創建一個池,這就意味着每個用戶都會獲得集羣的公平使用權,但是每個用戶自己的job會按照順序來執行。

配置池的屬性

  • 可以通過配置文件來修改池的屬性。每個池都支持以下三個屬性:
  1. schedulingMode: 可以是FIFO或FAIR,來控制池中的jobs是否要排隊,或者是共享池中的資源
  2. weight: 控制每個池子對集羣資源使用的權重。默認情況下,所有池子的權重都是1.如果指定了一個池子的權重爲2。舉例來說,它就會獲取其他池子兩倍的資源使用權。設置一個很高的權重值,比如1000,也會很有影響,基本上該池子的task會在其他所有池子的task之前運行。
  3. minShare: 除了權重之外,每個池子還能被給予一個最小的資源使用量。
  • 池子的配置是通過xml文件來配置的,在spark/conffairscheduler.xml中配置
  • 然後去設置這個文件的路徑,conf.set("spark.scheduler.allocation.file", "/path/to/file")

文件內容大致如下所示

<?xml version="1.0"?>
<allocations>
  <pool name="production">
    <schedulingMode>FAIR</schedulingMode>
    <weight>1</weight>
    <minShare>2</minShare>
  </pool>
  <pool name="test">
    <schedulingMode>FIFO</schedulingMode>
    <weight>2</weight>
    <minShare>3</minShare>
  </pool>
</allocations>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章