Flink流量控制與反壓機制完全總結

前言

筆者最近回顧自己對Flink技術棧細節的理解,發現對Flink的網絡棧、流控與反壓這一套機制存在比較大的盲區。雖然平時多次處理過作業反壓的問題,但是不完全理解背後的實現顯然說不過去。於是專門寫一篇總結,站在大佬們的肩膀上徹底搞清楚Flink是怎麼做流控與處理反壓的。

Flink網絡傳輸的數據流向

Flink網絡傳輸的數據流向如下圖所示。

Sender在發送數據時,首先寫入TaskManager內部的網絡緩存,利用Netty進行傳輸——將待發送的數據存入Netty的ChannelOutboundBuffer,再經由Socket的發送緩存發送出去。Receiver在接收數據時是反過來的,同樣要經過3層緩存,即Socket接收緩存→Netty ChannelInboundBuffer→TaskManager網絡緩存。要實現流量控制,就是在上面的流程上做文章。

Flink的反壓傳播

反壓(back pressure)就是流式系統中關於處理能力的動態反饋機制,並且是從下游到上游的反饋。下圖示出數據流在Flink TaskManager之間流動的邏輯。

可見,一旦因爲下游處理能力不足而出現反壓,反壓信號的傳播應該分爲兩個階段:一是從下游TaskManager的輸入端(InputGate)傳播到直接上游TaskManager的輸出端(ResultPartition);二是在TaskManager內部從輸出端傳播到輸入端。當然,我們要重點考慮的是跨TaskManager的反壓傳播,因爲它的鏈路比較長(參考上一節的數據流向圖),更有可能成爲瓶頸。

下面先來介紹舊版本中的流控和反壓機制。

Flink 1.5之前:基於TCP的流控和反壓

在1.5版本之前,Flink並沒有特別地去實現自己的流控機制,而是在傳輸層直接依靠TCP協議自身具備的滑動窗口機制(大學計算機網絡課程必講)。下面通過實例來複習TCP滑動窗口是如何實現流控的。

  1. 初始情況如下圖所示。Sender每單位時間發送3個包,發送窗口初始大小爲3;Receiver每單位時間接收1個包,接收窗口初始大小爲5(與接收緩存的大小相同)。
  1. Sender發送1~3三個包,Receiver接收到之後將它們放入緩存。
  1. Receiver消費一個包,接收窗口向前滑動一格,並告知Sender ACK=4(表示可以從第4個包開始發送),以及Window=3(表示接收窗口當前的空餘量爲3)。
  1. Sender接收到ACK消息後發送4~6三個包,Receiver接收到之後將它們放入緩存。
  1. Receiver消費一個包,接收窗口向前滑動一格,並告知Sender ACK=7(表示可以從第7個包開始發送),以及Window=1(表示接收窗口當前的空餘量爲1)。Sender接收到ACK消息之後,發現Receiver只能再接收1個包了,就將發送窗口的大小調整爲1併發送包7,達到了限流的目的。

接着這個流程分析下去,可以得知Sender最終會無法發送數據(因爲Receiver報告Window=0),直到Receiver消費掉緩存中的數據才能繼續發送。同時Sender還會定時向Receiver發送ZeroWindowProbe探測消息,保證Receiver能夠及時將消費能力報告給Sender。

接下來用實例介紹反壓流程。

  1. 如圖所示,Sender發送速度與Receiver接收速度的比是2:1,起初是可以正常發送與接收的。
  1. 一段時間過後,Receiver端InputChannel本身的緩存被耗盡,因此會向本地緩存池LocalBufferPool申請新的緩存。
  1. 一段時間過後,LocalBufferPool的可用額度會被耗盡,因此會向網絡緩存池NetworkBufferPool申請新的緩存。
  1. 隨着數據不斷積壓,NetworkBufferPool的額度也會被耗盡,此時沒有空間再接收新的數據,Netty的auto read會被關閉,不再從Socket緩存讀取數據。
  1. Socket緩存耗盡後,Receiver報告Window=0(參見上文的滑動窗口),Sender的Socket就會停止發送數據。
  1. Sender端的Socket緩存積壓,導致Netty無法再發送數據。
  1. 待發送的數據都積壓在Sender的ChannelOutboundBuffer中,當數據量超過Netty的high watermark之後,Channel被置爲不可寫,ResultSubPartition也就不再向Netty寫數據。
  1. Sender端的ResultSubPartition緩存滿了之後,就會像Receiver端的InputChannel一樣,不斷地向LocalBufferPool和NetworkBufferPool申請新的緩存,直到緩存全部耗盡,RecordWriter不能再寫數據。

這樣,我們就實現了反壓向上遊TaskManager的傳遞。

Flink 1.5之後:基於Credit的流控和反壓

基於TCP的流控和反壓方案有兩大缺點:

  • 只要TaskManager執行的一個Task觸發反壓,該TaskManager與上游TaskManager的Socket就不能再傳輸數據,從而影響到所有其他正常的Task,以及Checkpoint Barrier的流動,可能造成作業雪崩;
  • 反壓的傳播鏈路太長,且需要耗盡所有網絡緩存之後纔能有效觸發,延遲比較大。

Flink 1.5+版本爲了解決這兩個問題,引入了基於Credit的流控和反壓機制。它本質上是將TCP的流控機制從傳輸層提升到了應用層——即ResultPartition和InputGate的層級,從而避免在傳輸層造成阻塞。具體來講:

  • Sender端的ResultSubPartition會統計累積的消息量(以緩存個數計),以backlog size的形式通知到Receiver端的InputChannel;
  • Receiver端InputChannel會計算有多少空間能夠接收消息(同樣以緩存個數計),以credit的形式通知到Sender端的ResultSubPartition。

也就是說,Sender和Receiver通過互相告知對方自己的處理能力的方式來精準地進行流控(注意backlog size和credit也是要通過傳輸層的,不是直接交換的)。接下來仍然通過實例來說明基於Credit的流控和反壓流程。

  1. 仍然是Sender發送速度與Receiver接收速度的比是2:1的情景。Sender端的ResultSubPartition積壓了2個緩存的數據,因此會將該批次要發送的數據與backlog size = 2一同發往Receiver。
    Receiver收到當前批數據和backlog size之後,會計算InputChannel是否有足夠的緩存來接收下一批數據,如果不夠,則會去LocalBufferPool/NetworkBufferPool申請緩存,並將credit = 3通知到上游的ResultSubPartition,表示自己能夠接收3個緩存的消息。
  1. 隨着Receiver端的數據不斷積壓,網絡緩存最終被耗盡,因此會反饋給上游credit = 0(相當於TCP滑動窗口中的window = 0),Sender端ResultPartition到Netty的鏈路會被阻斷。按照上一節所述的流程,Sender端的網絡緩存會被更快地耗盡,RecordWriter不能再寫數據,從而達到反壓的效果。

由上可知,反壓信號在TaskManager之間不需要再通過傳輸層隨着數據向上反饋,大大降低了反壓的延遲。並且也不會因爲一個Task反壓而阻塞整個Socket鏈路,能夠相當精確地在Task粒度控制流量,不僅輕量級,而且高效。

The End

筆者之前也寫過Spark Streaming基於PID的流控和反壓機制(傳送門),橫向對比一下,不得不感嘆技術是永無止境的。

民那晚安晚安。

Reference

flink-china/flink-training-course/Flink網絡流控及反壓剖析

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