寫在前面
Akidau對Streaming System提出了五個基本概念,其中EventTime/ProcessTime和Window在《Streaming 101》已經介紹過了。如果不熟悉EventTime和ProcessTime的區別,或者不熟悉無界數據的Window劃分,請停止閱讀本文,從Streaming 101開始。
除了上面兩個,Streaming System還有三個重要的概念,這篇Streaming 102主要分析的就是剩下的這三個:
- Trigger 觸發器 何時窗口結果被持久存儲到外部;做個比喻,觸發器類似相機的快門,它定義了什麼時候讓計算結果留下快照。
- Watermark 水印 針對事件時間的輸入完整性概念;
- Accumulation 累加器 對同一個窗口的多組計算結果,它們是如何累加的;遲到的數據如何影響之前的結果;
此外,爲了描述數據和操作,我們需要借用Beam中的PCollections和PTransforms的概念,這些概念都是抽象的,引入它們僅用來解釋概念,和流式系統的具體實現無關。
- PCollections 數據集、並行轉換操作的執行對象
- PTransforms 對數據集的操作,例如分組group/聚集aggragate等
一、批式: What and Where
從簡單的例子開始,有一個數據集UserScores
,它包含三個字段:姓名Name
,隊伍Team
,分數Score
,此外還包括事件時間EventTime
、處理時間ProcTime
。全部數據見下表:
姓名 | 隊伍 | 得分 | 事件時間 | 處理時間 |
---|---|---|---|---|
Julie | TeamX | 5 | 12:00:26 | 12:05:19 |
Frank | TeamX | 9 | 12:01:26 | 12:08:19 |
Ed | TeamX | 7 | 12:02:26 | 12:05:39 |
Julie | TeamX | 8 | 12:03:06 | 12:07:06 |
Amy | TeamX | 3 | 12:03:39 | 12:06:13 |
Fred | TeamX | 4 | 12:04:19 | 12:06:39 |
Naomi | TeamX | 3 | 12:06:39 | 12:07:19 |
Becky | TeamX | 8 | 12:07:26 | 12:08:39 |
Naomi | TeamX | 1 | 12:07:46 | 12:09:00 |
二、流式: When and How
1. When Trigger
這一節講什麼是Trigger。“在處理時間的什麼時刻,處理結果被物化?”, Trigger實際上回答了這個問題。在處理結果物化的時機選擇上有非常多的可能,如果做一個分類,可以分爲基於事件時間的Trigger、基於處理時間的Trigger、基於數據的Trigger,例如:
- AfterWatermark 一類基於事件時間的trigger,當watermark超過了window的最後範圍,AfterWatermark trigger會釋放窗口的內容。Watermark是全局進度的度量,表示着在一個特定時刻輸入數據的完整性。事件時間由數據攜帶的時間戳決定。
// Create a bill at the end of the month.
AfterWatermark.pastEndOfWindow()
// During the month, get near real-time estimates.
.withEarlyFirings(
AfterProcessingTime
.pastFirstElementInPane()
.plusDuration(Duration.standardMinutes(1))
// Fire on any late data so the bill can be corrected.
.withLateFirings(AfterPane.elementCountAtLeast(1))
-
AfterProcessingTime 一類基於處理時間的trigger,例如AfterProcessingTime.pastFirstElementInPane在收到數據的特定處理時間後會釋放窗口的內容。處理時間由系統時鐘決定。
-
AfterPane 一類基於數據的trigger,例如AfterPane.elementCountAtLeast(),這個trigger在當前pane收集到至少N個元素後釋放窗口的內容。這種trigger可以讓window釋放早期結果,當你只使用一個全局窗口做流式計算的時候特別有用。需要額外注意,如果你指定了.elementCountAtLeast(50)但是隻收到了32個元素,那沒有辦法觸發執行,這32個元素就永遠閒着了。
-
複合trigger 上個例子中不足以觸發數據量trigger的32個元素,如果這部分數據很重要,你可以組合trigger,指定複合trigger,例如“每收到50條數據觸發一次,或者每1秒觸發一次”。
上面提到的分類參考自Beam API,在Streaming 102原文中提到的分類方式比上述更晦澀一些,按照Repeated update trigger和Completeness trigger進行分類。簡單說,特定時間或者特定數據條數觸發的trigger,叫做repeated update trigger;而特定window的數據在watermark範圍內完整後觸發的trigger,叫做completeness trigger。這也就引出了When的第二個重要概念,即watermark。
2. When Watermark
回到上一節開頭的問題 “在處理時間的什麼時刻,處理結果被物化?” 你會回答是在trigger定義的時刻,結果被系統物化。回答正確,但並不夠完整。Watermark是在事件時間領域,針對輸入完整性的臨時表示。回顧Streaming 101的這張圖,圖中描述了事件時間和處理時間的gap,即時間傾斜問題。
常見的Window
-
Fixed Time Windows 固定時間窗口
-
Sliding Time Windows 滑動時間窗口
-
Per-Session Windows 會話窗口
-
Single Global Window 單個全局窗口
-
Calendar-based Windows 基於日曆的窗口
有一些transforms, 例如GroupByKey和Combine,根據key把元素分組,相同key的元素屬於相同的組。
- 完美水印 理想情況的水印,但無法達到,你永遠不知道數據在什麼時候到齊
- 啓發式水印 實際能做到的水印,通常根據經驗人爲指定
兩類水印的計算過程如下圖,左邊是完美水印,右邊的是啓發式水印:
左邊的數據"9"是一條遲到數據,它的事件時間是[12:01, 12:02),但是在[12:08,12:09)到達流式處理系統,
在完美水印情況下,遲到的數據“9”也會被統計到,每個窗口的水印見圖4的左半部分。
在啓發式水印情況下,遲到的數據“9”會被劃分在窗口之外。
對左圖來說,[12:02, 12:04), [12:04, 12:06), [12:06, 12:08) 這三個窗口的物化時刻(見圖3,藍色表示已物化)太慢了,因爲這三個窗口的數據已經在12:08之前到齊。對右圖來說,[12:00,12:02)這個窗口的物化時刻(見圖1,藍色表示已物化)太快了,因爲有一條遲到的數據“9”,太快的物化導致[12:00, 12:02)這個窗口的統計結果是5,而不是正確的5+9=14。
有些情況太慢了,有些情況又太快了。 永遠不存在完美的水印。只通過啓發式水印來物化輸出不足以得到完全準確的結果。
▲ 圖 1: 處理時間12:06
▲ 圖 2: 處理時間12:08
▲ 圖 3: 處理時間12:10
▲ 圖 4: 不再修改的水印
3. When:Composite Triggers
前面提到了Repeated update trigger和Completeness trigger,在很多情況下,只用兩者中的一種是不夠的,需要組合使用。在Beam中提供了一種標準watermark trigger的擴展,支持在水印的任意一側更新trigger。在Beam中,這些擴展稱作early/on-time/late trigger。Beam包括了下述複合trigger:
- AfterWatermark.pastEndOfWindow 在窗口左側更新trigger選擇.withEarlyFirings;在窗口右側更新trigger選擇.withLateFirings;
- Repeatedly.forever 每次trigger條件達到,就引發一個窗口釋放結果;
- AfterEach.inOrder 把多個trigger組合到一個序列中,每次序列中的trigger釋放一個窗口,trigger條件切換到下一個trigger;
- AfterFirst 接收多個trigger,當任意一個trigger條件達成時,窗口釋放結果;
- AfterAll 接收多個trigger,當全部trigger條件達成時,窗口釋放結果;
- orFinally 在任何trigger後面添加,觸發一次釋放結果,然後再也不釋放;
在Beam的代碼中,上述trigger的樣例代碼如下
PCollection<KV<Team, Integer>> totals = input
.apply(Window.into(FixedWindows.of(TWO_MINUTES))
.triggering(AfterWatermark()
.withEarlyFirings(AlignedDelay(ONE_MINUTE))
.withLateFirings(AfterCount(1))))
.apply(Sum.integersPerKey());
使用複合trigger之後,處理過程變成圖5-圖8所示:
▲ 圖 5: 處理時間12:06,兩個窗口存在數據;Early Firings,即使window數據沒完整也觸發一次計算,避免“太慢”的問題;
▲ 圖 6:
▲ 圖 7:
▲ 圖 8: Late Firings, 當啓發式watermark太快時,一旦出現窗口外的數據,立刻觸發一次修正
- Early Trigger 在watermark(圖8中,左圖綠色虛線,右圖綠色實線)到達window的結尾之前,重複觸發,表示雖然window還沒到最後時刻,但數據要觸發一次計算,窗口內的數據叫Early Pane。Early Firings,即使window數據沒完整也觸發一次計算,一定程度避免了“太慢”的問題;
- On-Time Trigger 在watermark剛好到達window的結尾處時,觸發一次,窗口內的數據叫做On-Time Pane。這種情況很少見。
- Late Trigger 在watermark; Late Firings, 當啓發式watermark太快時,一旦出現窗口外的數據,立刻觸發一次修正;一定程度解決了“太快”的問題。
無論是那種watermark, 隨着時間推移,window的範圍都得到了一定程度的修正,同時平衡了數據的及時性和準確性,這就是引入複合Trigger的價值。
4. When: Allowed Lateness
什麼是Allowed Lateness呢?我們直接看圖說話(建議放大到全屏查看)。
我們知道無界數據按事件時間被劃分成一個個窗口(圖中橫座標的區間),在達到Trigger條件時觸發計算操作。當處理時間到達一定階段後 ,比較早的窗口就再也不會看到數據,每個窗口處理時間的上限叫做watermark(圖中綠色實線)。當watermark通過窗口終點後(圖中綠色實線穿過橫座標的窗口區間),我們已經可以關閉窗口的狀態,清空這部分存儲空間了。在啓發式watermark中,如果我們讓窗口的右邊界允許再向右擴展,留一部分時間等待遲到數據,那這個向右繼續擴展的窗口邊界範圍就叫做allowed lateness或者lateness horizon。 顯然,lateness horizon是在事件時間的範疇定義的。允許的遲到時間越長,窗口狀態數據的保留時間越長,對流式系統的存儲空間要求也就越高。也就是說,lateness horizon使用更多的窗口狀態存儲空間來換取流式統計結果的準確性。
例如圖9中的左邊,[12:00, 12:02)這個窗口被允許1分鐘的lateness horizon, 保留到12:03; [12:02, 12:04)這個窗口被允許1分鐘的lateness horizon, 保留到12:05;
▲ 圖 9: 允許遲到的時間
▲ 圖 10: 允許遲到的時間
Lateness horizon用Beam API withAllowedLateness
來描述,如下述代碼所示:
PCollection<KV<Team, Integer>> totals = input
.apply(Window.into(FixedWindows.of(TWO_MINUTES))
.triggering(
AfterWatermark()
.withEarlyFirings(AlignedDelay(ONE_MINUTE))
.withLateFirings(AfterCount(1)))
.withAllowedLateness(ONE_MINUTE))
.apply(Sum.integersPerKey());
5. How: Accumulation
如果一個窗口的多次Trigger產生了多個Pane,那麼這個窗口的最終結果如何在多個pane的結果中產生?這就是Accumulation要定義的內容。有三種Accumulation模式:Discarding、Accumulating、Accumulating & Retracting,它們的處理過程見下表:
丟棄 | 累積 | 累積、撤回 | |
---|---|---|---|
Pane 1: inputs=[3] | 3 | 3 | 3 |
Pane 2: inputs=[8,1] | 9 | 12 | 12, -3 |
Final Pane | 9 | 12 | 12 |
Sum of all Panes | 12 | 15 | 12 |
- Discarding 丟棄 當每個窗口釋放的是某種中間形式的數據,下游系統(而不是流式系統)會真正執行計算邏輯,這種情況適合丟棄上次的結果;舉個例子,計算窗口內的數字之和,上次的結果count=3,新進入的數據是9,在丟棄模式下,最終結果是9;
- Accumulating 累積 計算窗口內的數字之和,上次的結果count=3,新進入的數據是9,在累積模式下,最終結果是3+9=12;
- Accumulating and retracting 累積、撤回 繼續上面的例子,當結果更新爲12後,同時發出一個撤回操作-3,表示之前的3是個已經失效的結果,需要撤回;當下遊消費者對數據按另外一個維度做重新分組時,之前的結果可能被分配到新的組,因此只覆蓋舊結果還不夠,需要“撤回操作”刪除舊值;當動態劃分窗口時,新數據的到來會引起相關窗口的重新劃分。
上述累加模式的存儲成本和計算成本是遞增的。
參考
[1] Apache Beam官方文檔
[2] Streaming 102
[3] Streaming Systems