Spark Streaming的PIDRateEstimator與backpressure

devos發表於2018-08-30

PIDRateEstimator是Spark Streaming用來實現backpressure的關鍵元件。

看了一些部落格文章,感覺對它的解釋都沒有說到要點,還是自己來研究一下比較好。

首先,需要搞清楚的一個問題是Spark Streaming的backpressure是想讓系統達到怎麼樣的一種狀態。這個問題不明確,PIDRateEstimator的作用就搞不清楚。

backpressure的目標

首先,backpressure這套機制是系統(由應用程式和物理資源組成的整體)的內在性質對Spark Streaming的吞吐量的限制,而並非是某種優化。可以認為,在固定的資源下(CPU、記憶體、IO),Spark Streaming程式存在吞吐量的上限。

放在非micro-batch的情況下考慮,這意味著存在一個最大處理速度,RateEstimator認為這個速度的單位為records/second (不過實際上,每條訊息的處理所耗的時間可能差別很大,所以這個速度的單位用records/second實際上是可能並不合適,可能是一種過度的簡化)

放在Spark Streaming的micro-batch的情況下,由於排程器每隔batch duration的時間間隔生成一個micro-batch,這個吞吐率的上限意味著每個batch總的訊息數量存在上限。如果給每個batch分配率的訊息總數超過這個上限,每秒處理訊息條數是不變的,只會使得batch的處理時間延長,這樣對於系統沒有什麼好處,反而由於每個batch太大而可能導致OOM。

當達到這個最大處理速度時,表現就是batch duration等於batch的計算階段所花的時間,也就是batch duration == batch processing time。

那麼backpressure的目標,就是使得系統達到上邊這個狀態(這個並非完全對,下面的分析會給出具體的狀態)。它不會使得系統的累積未處理的資料減少,也不會使得系統的吞吐率提高(在不引起OOM,以及不計算GC的開銷的情況下,當processing time > batch duration時,系統的吞吐量已經達到最高)。而只是使得系統的實際吞吐量穩定在最大吞吐量(除非你手動設定的rate的最大值小於最大吞吐量)

 

PIDRateEstimator

首先,要明確PID控制器的作用。

引用一篇blog的說法:

PID控制器是一個在工業控制應用中常見的反饋迴路部件。

這個控制器把收集到的資料和一個參考值進行比較,然後把這個差別用於計算新的輸入值,

這個新的輸入值的目的是可以讓系統的資料達到或者保持在參考值。

PID控制器可以根據歷史資料和差別的出現率來調整輸入值,使系統更加準確而穩定。

重點在於它的目的是調整輸入,比而使得系統的某個我們關注的目標指標到目標值。

PID的控制輸出的公式為

這裡u(t)為PID的輸出。

SP是setpoint, 就是參考值

PV是 process variable, 也就是測量值。

A PID controller continuously calculates an error value e(t) as the difference between a desired setpoint (SP) and a measured process variable (PV) and applies a correction based on proportionalintegral, and derivative terms (denoted PI, and D respectively), hence the name.

計算邏輯

首先,看下RateEstimator的compute方法的定義

複製程式碼
private[streaming] trait RateEstimator extends Serializable {

  /**
   * Computes the number of records the stream attached to this `RateEstimator`
   * should ingest per second, given an update on the size and completion
   * times of the latest batch.
   *
   * @param time The timestamp of the current batch interval that just finished
   * @param elements The number of records that were processed in this batch
   * @param processingDelay The time in ms that took for the job to complete
   * @param schedulingDelay The time in ms that the job spent in the scheduling queue
   */
  def compute(
      time: Long,
      elements: Long,
      processingDelay: Long,
      schedulingDelay: Long): Option[Double]
}
複製程式碼

看下引數的含義

  • time: 從它的來源看,它來源於BatchInfo的processingEndTime, 準確含義是 “Clock time of when the last job of this batch finished processing”,也就是這個batch處理結束的時間
  • elements: 這個batch處理的訊息條數
  • processingDelay: 這個job在實際計算階段花的時間(不算排程延遲)
  • schedulingDelay:這個job花在排程佇列裡的時間

PIDRateEstimator是獲取當前這個結束的batch的資料,然後估計下一個batch的rate(注意,下一個batch並不一定跟當前結束的batch是連續兩個batch,可能會有積壓未處理的batch)。

PIDRateEstimator對於PID控制器裡的"error"這個值是這麼計算的:

複製程式碼

// in seconds, should be close to batchDuration
val delaySinceUpdate = (time - latestTime).toDouble / 1000

// in elements/second
val processingRate = numElements.toDouble / processingDelay * 1000

// In our system `error` is the difference between the desired rate and the measured rate
// based on the latest batch information. We consider the desired rate to be latest rate,
// which is what this estimator calculated for the previous batch.
// in elements/second
val error = latestRate - processingRate

val historicalError = schedulingDelay.toDouble * processingRate / batchIntervalMillis

// in elements/(second ^ 2)
val dError = (error - latestError) / delaySinceUpdate

val newRate = (latestRate - proportional * error -
integral * historicalError -
derivative * dError).max(minRate)
  
複製程式碼

這裡的latestRate是指PID控制器為上一個batch,也就是當前結束的batch,在生成這個batch的時候估計的處理速度。

所以上邊程式碼中,latestRate就是參考值, processingRate就是測量值。

這裡為什麼如此計算我還是沒搞清楚,因為latestRate是一個變化的值,不知道這樣在數學上會對後邊的積分、微分項的含義造成什麼影響。

error何時為0

可以推匯出來當batchDuration = processingDelay時候,這裡的error為零。

推導過程為:

latestRate實際上等於numElements / batchDuration,因為numElements是上次生成job時根據這個latestRate(也就是當時的estimated rate)算出來的。

那麼 error = (numElements / batchDuaration) - (numElements/processingDelay)             這裡的processingDelay就是processing time

所以,當processingDelay等於batchDuration時候,error為零。

但是error為零時,PID的輸出不一定為零,因為需要考慮到歷史誤差和誤差的變化。這裡剛結束的batch可能並非生成後就立即被執行,而是在排程佇列裡排了一會隊,所以還是需要考慮schedulingDelay,它反應了歷史誤差。

那麼什麼時候達到穩定狀態呢?

 當PID輸出為0時,newRate就等於latestRate,此時系統達到了穩定狀態,error為零,historicalError和dError都為0。

這意味著:

  • 沒有schedulingDelay,意味著job等待被排程的時間為0. 如果沒有累積的未執行的job,那麼schedulingDelay大致等於0.
  • error為零,意味著batchDuration等於processingDelay
  • dError為零,在error等於0時,意味著上一次計算的error也為零。

這就是整個RateEstimator,也就是backpressure想要系統達到的狀態。

 

這裡可以定性地分析一下達到穩定狀態的過程:

  • 如果batch分配的訊息少於最高吞吐量,就會有processingRate  > latestRate, 從而使得error為負,如果忽略積分和微分項的影響,就會使得newRate = latestRate - propotional * rate,從而使得newRate增大,因此下一個batch處理的訊息會變多。
  • 如果batch分配的訊息大於最高吞吐量,就會有processingRate < latestRate,從而使得error為正,如果此前已經有job被積累,那麼historicalError也為正,考慮到dError的係數預設為0,所以此時newRate  = latestRate - proportional * error -integral * historicalError  使得newRate變小,從而使得下一個batch處理的訊息變少,當newRate == latestRate時,有 -proportional * error == integral * historicalError,即error為一個負值,也即processingRate > latestRate,也就是說會使得給每個batch分配的訊息小於它的最大處理量。此時,由於processingDelay小於batchDuration,會使得歷史上累積的job有機會得到處理,從而逐漸減少在等待的job數量。

 

可以看出來這個PIDRateEstimator並非是普遍最優的,因為它的假設是系統的動態特定不隨時間變化,但是實際上如果沒有很有效的資源隔離,系統對於Spark Streaming程度來講,其資源是隨時間變化的,而且在某些時間可能發生劇烈的變化。此時,此時RateEstimator應該做出更劇烈的變化來應對,比如通過動態調整各個部分的係數。

如果使用者對自己的系統有深的瞭解,比如當資源和負載是週期性變化時,那就可以定製更合適的RateEstimator,比如考慮到每天同比的流量變化來調整estimatedRate。

相關文章