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 proportional, integral, and derivative terms (denoted P, I, 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。