因爲實時場景對可用性十分敏感,實時做業一般須要避免頻繁重啓,所以動態加載做業配置(變量)是實時計算裏十分常見的需求,好比一般復瑣事件處理 (CEP) 的規則或者在線機器學習的模型。儘管常見,實現起來卻並無那麼簡單,其中最難點在於如何確保節點狀態在變動期間的一致性。目前來講通常有兩種實現方式:apache
- 輪詢拉取方式,即做業算子定時檢測在外部系統的配置是否有變動,如有則同步配置。
- 控制流方式,即做業除了用於計算的一個或多個普通數據流之外,還有提供一個用於改變做業算子狀態的元數據流,也就是控制流。
輪詢拉取方式基於 pull 模式,通常實現是用戶在 Stateful 算子(好比 RichMap)裏實現後臺線程定時從外部系統同步變量。這種方式對於通常做業或許足夠,但存在兩個缺點分別限制了做業的實時性和準確性的進一步提升:首先,輪詢老是有必定的延遲,所以變量的變動不能第一時間生效;其次,這種方式依賴於節點本地時間來進行校準。若是在同一時間有的節點已經檢測到變動並更新狀態,而有的節點尚未檢測到或者還未更新,就會形成短期內的不一致。編程
控制流方式基於 push 模式,變動的檢測和節點更新的一致性都由計算框架負責,從用戶視角看只須要定義如何更新算子狀態並負責將控制事件丟入控制流,後續工做計算框架會自動處理。控制流不一樣於其餘普通數據流的地方在於控制流是以廣播形式流動的,不然在有 Keyby 或者 rebalance 等提升並行度分流的算子的狀況下就沒法將控制事件傳達給全部的算子。緩存
以目前最流行的兩個實時計算框架 Spark Streaming 和 Flink 來講,前者是以相似輪詢的方式來實現實時做業的更新,然後者則是基於控制流的方式。網絡
Spark Streaming Broadcast Variable
Spark Streaming 爲用戶提供了 Broadcast Varialbe,能夠用於節點算子狀態的初始化和後續更新。Broacast Variable 是一組只讀的變量,它在做業初始化時由 Spark Driver 生成並廣播到每一個 Executor 節點,隨後該節點的 Task 能夠複用同一份變量。app
Broadcast Variable 的設計初衷是爲了不大文件,好比 NLP 經常使用的分詞詞典,隨序列化後的做業對象一塊兒分發,形成重複分發的網絡資源浪費和啓動時間延長。這類文件的更新頻率是相對低的,扮演的角色相似於只讀緩存,經過設置 TTL 來定時更新,緩存過時以後 Executor 節點會從新向 Driver 請求最新的變量。框架
Broadcast Variable 並非從設計理念上就支持低延遲的做業狀態更新,所以用戶想出了很多 Hack 的方法,其中最爲常見的方式是:一方面在 Driver 實現後臺線程不斷更新 Broadcast Variavle,另外一方面在做業運行時經過顯式地刪除 Broadcast Variable 來迫使 Executor 從新從 Driver 拉取最新的 Broadcast Variable。這個過程會發生在兩個 micro batch 計算之間,以確保每一個 micro batch 計算過程當中狀態是一致的。機器學習
比起用戶在算子內訪問外部系統實現更新變量,這種方式的優勢在於一致性更有保證。由於 Broadcast Variable 是統一由 Driver 更新並推到 Executor 的,這就保證不一樣節點的更新時間是一致的。然而相對地,缺點是會給 Driver 帶來比較大的負擔,由於須要不斷分發全量的 Broadcast Variable (試想下一個巨大的 Map,每次只會更新少數 Entry,卻要整個 Map 從新分發)。在 Spark 2.0 版本之後,Broadcast Variable 的分發已經從 Driver 單點改成基於 BitTorrent 的 P2P 分發,這必定程度上緩解了隨着集羣規模提高 Driver 分發變量的壓力,但我我的對這種方式能支持到多大規模的部署仍是持懷疑態度。另一點是從新分發 Broadcast Variable 須要阻塞做業進行,這也會使做業的吞吐量和延遲受到比較大的影響。ide
Flink Broadcast State & Stream
Broadcast Stream 是 Flink 1.5.0 發佈的新特性,基於控制流的方式實現了實時做業的狀態更新。Broadcast Stream 的建立方式與普通數據流相同,例如從 Kafka Topic 讀取,特別之處在於它承載的是控制事件流,會以廣播形式將數據發給下游算子的每一個實例。Broadcast Stream 須要在做業拓撲的某個節點和普通數據流 (Main Stream) join 到一塊兒。post
該節點的算子須要同時處理普通數據流和控制流:一方面它須要讀取控制流以更新本地狀態 (Broadcast State),另一方面須要讀取 Main Stream 並根據 Broadcast State 來進行數據轉換。因爲每一個算子實例讀到的控制流都是相同的,它們生成的 Broadcast State 也是相同的,從而達到經過控制消息來更新全部算子實例的效果。學習
目前 Flink 的 Broadcast Stream 從效果上實現了控制流的做業狀態更新,不過在編程模型上有點和通常直覺不一樣。緣由主要在於 Flink 對控制流的處理方式和普通數據流保持了一致,最爲明顯的一點是控制流除了改變本地 State 還能夠產生 output,這很大程度上影響了 Broadcast Stream 的使用方式。Broadcast Stream 的使用方式與普通的 DataStream 差異比較大,即須要和 DataStream 鏈接成爲 BroadcastConnectedStream 後,再經過特殊的 BroadcastProcessFunction 來處理,而 BroadcastProcessFunction 目前只支持 相似於 RichCoFlatMap 效果的操做。RichCoFlatMap 能夠間接實現對 Main Stream 的 Map 轉換(返回一隻有一個元素的集合)和 Filter 轉換(返回空集合),但沒法實現 Window 類計算。這意味着若是用戶但願改變 Window 算子的狀態,那麼須要將狀態管理提早到上游的 BroadcastProcessFunction,而後再經過 BroadcastProcessFunction 的輸出來將影響下游 Window 算子的行爲。
總結
實時做業運行時動態加載變量能夠令大大提高實時做業的靈活性和適應更多應用場景,目前不管是 Flink 仍是 Spark Streaming 對動態加載變量的支持都不是特別完美。Spark Streaming 受限於 Micro Batch 的計算模型(雖然如今 2.3 版本引入 Continuous Streaming 來支持流式處理,但離成熟還須要必定時間),將做業變量做爲一致性和實時性要求相對低的節點本地緩存,並不支持低延遲地、低成本地更新做業變量。Flink 將變量更新視爲特殊的控制事件流,符合 Even Driven 的流式計算框架定位,目前在業界已有比較成熟的應用。不過美中不足的是編程模型的易用性上有提升空間:控制流目前只能用於和數據流的 join,這意味着下游節點沒法繼續訪問控制流或者須要把控制流數據插入到數據流中(這種方式並不優雅),從而下降了編程模型的靈活性。我的認爲最好的狀況是大部分的算子均可以被拓展爲具備 BroadcastOperator,就像 RichFunction 同樣,它們能夠接收一個數據流和一個至多個控制流,並維護對應的 BroadcastState,這樣控制流的接入成本將顯著降低。
參考文獻
1.FLIP-17 Side Inputs for DataStream API
2.Dynamically Configured Stream Processing: How BetterCloud Built an Alerting System with Apache Flink®
3.Using Control Streams to Manage Apache Flink Applications
4.StackOverFlow - ow can I update a broadcast variable in spark streaming?