流處理系統需要能優雅地處理反壓(backpressure)問題。反壓通常產生於這樣的場景:短時負載高峰導致系統接收數據的速率遠高於它處理數據的速率。許多日常問題都會導致反壓,例如,垃圾回收停頓可能會導致流入的數據快速堆積,或者遇到大促或秒殺活動導致流量陡增。反壓如果不能得到正確的處理,可能會導致資源耗盡甚至系統崩潰。
目前主流的流處理系統 Storm/JStorm/Spark Streaming/Flink 都已經提供了反壓機制,不過其實現各不相同。
Storm 是通過監控 Bolt 中的接收隊列負載情況,如果超過高水位值就會將反壓信息寫到 Zookeeper ,Zookeeper 上的 watch 會通知該拓撲的所有 Worker 都進入反壓狀態,最后 Spout 停止發送 tuple。
JStorm 認為直接停止 Spout 的發送太過暴力,存在大量問題。當下游出現阻塞時,上游停止發送,下游消除阻塞后,上游又開閘放水,過了一會兒,下游又阻塞,上游又限流,如此反復,整個數據流會一直處在一個顛簸狀態。所以 JStorm 是通過逐級降速來進行反壓的,效果會較 Storm 更為穩定,但算法也更復雜。另外 JStorm 沒有引入 Zookeeper 而是通過 TopologyMaster 來協調拓撲進入反壓狀態,這降低了 Zookeeper 的負載。
那么 Flink 是怎么處理反壓的呢?答案非常簡單:Flink 沒有使用任何復雜的機制來解決反壓問題,因為根本不需要那樣的方案!它利用自身作為純數據流引擎的優勢來優雅地響應反壓問題。下面我們會深入分析 Flink 是如何在 Task 之間傳輸數據的,以及數據流如何實現自然降速的。
Flink 在運行時主要由 operators 和 streams 兩大組件構成。每個 operator 會消費中間態的流,並在流上進行轉換,然后生成新的流。對於 Flink 的網絡機制一種形象的類比是,Flink 使用了高效有界的分布式阻塞隊列,就像 Java 通用的阻塞隊列(BlockingQueue)一樣。還記得經典的線程間通信案例:生產者消費者模型嗎?使用 BlockingQueue 的話,一個較慢的接受者會降低發送者的發送速率,因為一旦隊列滿了(有界隊列)發送者會被阻塞。Flink 解決反壓的方案就是這種感覺。
在 Flink 中,這些分布式阻塞隊列就是這些邏輯流,而隊列容量是通過緩沖池(LocalBufferPool
)來實現的。每個被生產和被消費的流都會被分配一個緩沖池。緩沖池管理着一組緩沖(Buffer
),緩沖在被消費后可以被回收循環利用。這很好理解:你從池子中拿走一個緩沖,填上數據,在數據消費完之后,又把緩沖還給池子,之后你可以再次使用它。
更多信息詳見:http://wuchong.me/blog/2016/04/26/flink-internals-how-to-handle-backpressure/