Sitemap
Ryan Fu

Ryan Fu

探究一切事物的本質

Chapter 10. Stream Processing Engine: The Birth of Stream Processing

26 min readApr 27, 2026

--

《給學弟妹的矽谷 AI / Software 指南》是一個矽谷工程師自發的眾籌寫作計劃,由源來適你社群推動。這將是一個在台灣軟體學生圈內很有影響力的文章系列。已有 >10 篇準備發布,目標 100 篇並出書,歡迎 tag 強者大神來寫!

Press enter or click to view image in full size

上一章開始,我們正式進入了 Stream 的世界。

我曾這樣介紹:

進入 Data Lake 時代後,可透過 streaming 的方式即時傳輸資料。不再等待深夜批次處理,而是讓資料持續流動,即時同步到目標系統。若採用這種方式,Kafka 就成為不可或缺的工具 — — 它為即時資料傳輸搭起橋樑。

Kafka 解決的是一個關鍵問題:資料如何持續流動。但資料只是流動,本身並沒有意義。

Store 的下游,必須接上 Processing Engine,才有意思。Kafka 作為一個 Stream Store,它的下游可以有兩種典型使用方式:

  • 每小時讀取一批資料,交由 Batch Job 分析;
  • 或交由 Stream Processing Engine,即時處理。

前者仍然停留在「資料累積後再處理」的邏輯;後者則嘗試處理「正在發生的事情」。這兩者看似只是延遲差異,實則代表兩種完全不同的計算哲學。

在本書鋪陳的結構中:

  • 在 Data Source 端,我已詳細介紹了 Database;
  • 在 Data Destination 端,我介紹了 Data Warehouse、Data Lake,並初步窺探了 Lakehouse;
  • 在中間件層,我介紹了 Kafka,這個承載資料流動的橋樑。

熟悉我鋪排的讀者應該不難猜到 — — 既然我們已經理解了資料如何產生、如何儲存、如何流動,那麼接下來自然要問:

如何計算正在流動的資料?

本章,我們將正式進入 Kafka 下游的核心 — — Stream Processing Engine。

我們會從 2010 年 Yahoo 推出的 S4 開始。那是一個仍然粗糙、卻首次將 streaming 作為一級公民的系統原型。緊接著是 Twitter 的 Storm,它將 streaming 帶入 production,證明這種模式可以在現實世界運作。

在這一章,讀者可以用一種相對輕鬆的心情來閱讀。S4 與 Storm 開啟了一條新的方向,是過渡階段的探索,而非最終答案。這條路最終會引領到集大成的系統 — — Flink,因此下一章更適合在精神飽滿時細細閱讀。

到這裡讀者或許有疑問:既然 S4 與 Storm 都不是最終解,那為何還需要花篇幅理解它們?

一個技術,從來都不是憑空出現的。它背後總有一些人,在某個時間點、某個環境裡,面對具體的問題與限制,做出了一些選擇。也許是某個工程師在深夜 debug 系統時發現的瓶頸,也許是一家公司在規模成長後無法再承受既有架構的壓力。

當我們試著把這些「人、事、時、地、物」拼湊起來時,S4、Storm,甚至後來的 Flink,就不再只是幾個系統名稱,而是同一條演化路徑上的不同節點。S4 回應了 batch 無法即時處理的問題;Storm 則進一步回應了可靠傳遞的需求;而 Flink,則是在三個懸而未解的問題之上,試圖給出更完整的答案。

也正是在這樣的演進脈絡中,我們才開始真正理解,Kafka 與 Streaming Processing Engine 各自扮演的角色 — — Kafka 讓資料流動起來;Streaming Processing Engine 則讓這條河五顏六色。

之所以五顏六色,不是因為工業污染,而是因為 Stream Processing Engine 能在資料流動的瞬間,重新塑形每一筆資料。它可以即時聚合、即時過濾、即時轉換,讓資料在尚未沉澱為靜態儲存之前,就已經呈現出新的樣貌。

在這個世界裡,資料在流動之中持續被理解、被修正、被更新。河流不再單調,而是一條會隨時間改變顏色的動態結構。

S4(2010)

Introduction

2010 年,Yahoo 推出了 S4(Simple Scalable Streaming System)。在 MapReduce 仍然主導資料處理思維的時代,S4 嘗試回答一個不同的問題:如果資料不是靜態地躺在儲存系統裡,而是持續流入系統,我們是否能以長期運行的方式,即時處理這些資料,而不再等待批次作業?

S4 是最早公開的分散式 Stream Processing Engine 之一。它的核心思想很簡單卻具有前瞻性:資料以事件(event)的形式持續流動,而系統中的處理單元則長期存在,隨著事件到來即時更新內部狀態並產生新的事件。

在這個設計中,資料不再是等待被讀取的檔案,而是觸發計算的動態訊號。

Definition

Event

在 S4 中,系統的基本資料單位稱為 event。官方將其定義為不可變(immutable)的資料記錄。Event 在系統中流動,並可能包含一個 key,是整個擴展模型的核心:系統透過對 key 進行雜湊(hash),決定該 event 應被送往哪個節點。

因此,key-based routing 是 S4 水平擴展能力的基礎。

Processing Element (PE)

S4 的核心抽象是 Processing Element(PE)。PE 是一個可維持內部狀態的處理單元。它長期存在,持續接收 event,更新自身狀態,並可能產生新的 event。與批次系統不同,PE 並不是一次性執行,而是常駐於系統中的狀態機。

Per-Key Stateful Computation

在 S4 的模型中,每一個 key 對應一個 PE instance。相同 key 的 event 會被路由到同一個 PE,使得該 PE 可以維護與該 key 相關的內部狀態。

這種設計等同於一種 per-key stateful computation。每個 key 都擁有自己的狀態空間,形成一個小型的獨立狀態機。這種 per-key 分割機制,使得系統既可擴展,又能維持狀態一致性(在單一節點範圍內)。

Stream

Event 在 PE 之間以 stream 的形式傳遞。官方將 stream 定義為 event 的序列。然而,S4 並未建立嚴格的時間語意模型。Event 可以攜帶時間資訊,但系統本身並未制度化 event time、watermark 或 window 等概念。

這意味著時間相關的邏輯必須由應用程式自行實作。

Application Graph

一個 S4 應用由多個 Processing Element(PE)所組成,這些 PE 透過 stream 相互連接,形成一個持續運行的處理圖。與後來更形式化的拓撲模型相比,S4 的應用圖相對簡單,但其核心思想已十分清晰:計算不再是一次性的批次執行,而是一個長期存在的狀態轉移過程。

從結構上來看,這樣的處理圖在實務上常被畫成 DAG(Directed Acyclic Graph),資料沿著節點之間的邊持續流動。然而,將 S4 僅僅理解為一個 DAG,容易忽略它更本質的設計。

S4 的官方模型是一種 event-driven graph。每個 PE 接收 event、更新內部狀態,並產生新的 event,再透過 key-based routing 傳遞給下一個 PE。這種運作方式,使得整個系統更接近 Actor model 與 message passing 的組合,而非一個靜態的計算圖。

因此,S4 的重點在於兩個核心機制:

  1. 以 key 為中心的 state 管理,讓每個 PE 都能持續記住並更新自己的狀態。
  2. 同時透過 event 在系統中流動,讓計算一步一步往前推進。

Use Case: Top K Word Count in S4

Press enter or click to view image in full size

這張 Application Graph 展示了一個典型的 S4 應用:在整個系統中持續計算出現次數最多的前 K 個單詞。這不是一次性的批次運算,而是一個隨資料流動而不斷更新的狀態問題。資料持續進來,計數持續改變,Top K 也隨時間演化。整個流程被組織為一個由 Processing Element(PE)構成的有向圖。

在圖中,我們看到 PE1 到 PE8 這樣的標記,它們只是用來表示在分散式環境中被複製與部署的多個 PE instance。讀者真正必須關心的是每一層 PE 的角色與它們之間的連接關係。

例如,最上層的 QuoteSplitterPE 負責接收原始句子,將其拆分成單詞;中間一層的 WordCountPE 依據 key(單詞)維護各自的計數;再往下的 SortPE 則對局部資料進行排序;最後由 MergePE 將多個局部結果整合為全域的 Top K。這些邏輯上的角色,才是應用圖的核心。

至於像 PE2、PE3、PE4 這樣的節點,代表的是同一種類型的 PE 被平行部署多個 instance。這種 fan-out(平行度)通常是由開發者在設定中決定的,用來分散負載。在 S4 的模型中,這個數量並不會隨系統自動調整,而是需要在部署時事先配置,因此某種程度上是「寫死」的。系統會根據 event 的 key 做 hash,將資料路由到對應的 PE instance,確保相同 key 的資料會被送到同一個節點。

如果從實務角度來看,使用 S4 開發一個應用,大致可以分為三個步驟。

第一步,定義資料的流動方式,也就是設計 event 的結構與 key。key 的選擇會直接影響狀態如何被切分與分散。

第二步,要在各個 Processing Element 裡定義兩件事情:一是「我會接收哪些 event」,二是「我處理完之後會發出哪些 event」。

乍看之下,當開發者在定義「某種 event 應該被哪個 PE 接收」時,似乎已經在描述一張計算圖。從這個角度來看,將 S4 的應用畫成 DAG,並沒有錯。但關鍵差異在於:這張圖並不是一個被明確宣告、也不是一個會被編譯(compile)與最佳化的拓撲(topology)。

在 S4 中,並不存在一個集中式的 compile 階段,去驗證整個應用是否構成一個嚴格的 DAG。開發者也不需要先定義完整的圖結構。相反地,系統只關心 event 如何在不同的 PE 之間流動。至於這些 event 最終會串成什麼樣的結構,是在系統運行過程中,由 event 的流動與 key-based routing 自然形成的。

換句話說,圖是執行的結果,而不是設計的起點。這個結構通常可以被觀察為一個 DAG,但它並不是由系統在執行前所明確建構與驗證的。

第三步,設定系統的平行度與部署方式,包括每種 PE 要啟動多少個 instance,以及它們如何分布在不同節點上。

整體而言,開發者需要思考的並不是「資料在哪裡被處理」,而是「資料應該如何流動,以及每一個節點應該做什麼」。至於資料如何被分發、節點如何被調度,則交由系統來負責。

Summary of S4

S4 在 2014 年從 Apache 的孵化計畫中正式退場(retired)。至此,讓我們來檢視它的歷史地位。

當大數據的價值開始被企業真正重視,MapReduce 那種定期、批次式的處理模式逐漸顯得遲緩。業務需求開始要求更低延遲的回應,資料不再只是用來做歷史分析,而是要反映「正在發生的事情」。在這樣的背景下,流式計算首次走上舞台中央。

S4 正是這個轉折點的產物。Yahoo 嘗試用它來實現低延遲的即時資料處理,例如廣告與搜尋資料的即時回饋,以及線上 A/B 測試。S4 的設計理念與 MapReduce 在精神上有某種延續:開發者只需實作 Processing Element 的業務邏輯,而無需關心分散式系統如何運行。資料分發、節點調度與基本容錯機制由框架負責。

在架構上,S4 將業務邏輯與底層的網路協定、資料路由與負載分散機制拆分開來,形成可插拔(pluggable)的系統設計。整體流程被建構為一個有向圖,每個節點是一個 Processing Element,每條邊是一條事件流。每個 PE 被部署在某個處理節點上,負責執行業務邏輯並維護記憶體中的狀態。計算結果可以定期發布到外部儲存系統,使其對外可用。

然而,S4 的設計也顯得相當原始。它採用完全對稱、去中心化的分散式架構,試圖避免單點故障,但也因此缺乏靈活的動態擴展能力。當流量激增時,系統更傾向於採取服務降級,而非動態擴容。

在容錯層面,S4 僅處理節點層級的失效。當節點故障時,系統可以在其他硬體上重新啟動對應的處理元素,但已存在於記憶體中的狀態可能已經遺失。對於節點之間的資料傳輸,系統也未提供端到端的傳遞保證。

換句話說,S4 成功證明了一件事:流式計算可以被建構成一個可擴展的分散式系統。但它尚未回答更深層的問題:當狀態與資料同時流動時,如何在故障發生之後仍然確保計算結果的正確性?

S4 的種種限制,都成為 Storm 和之後系統的改進方向。

Storm(2011)

Introduction

如果說 S4 證明了流式計算是可能的,那麼 Storm 則試圖讓它變得可靠。

2011 年,Twitter 公開了 Storm。當時 Twitter 面臨的問題十分具體:使用者產生的訊息流量龐大,熱門話題、推薦系統、廣告排序等應用都需要在極低延遲下持續更新結果。單純依賴批次系統已無法滿足這樣的需求。

Storm 的出現,並不是要重新定義計算模型,而是要解決一個更迫切的問題 — — 如何在分散式環境下,可靠地處理無界資料流。

Definition

Storm 由以下幾個核心組件構成。

Spout

Spout 是資料來源。它負責從外部系統讀取或接收資料,並將資料轉化為 Tuple 持續發送進系統。Spout 本身不負責計算邏輯,而是扮演輸入端角色。與 S4 將所有節點統一為 Processing Element 不同,Storm 明確將資料來源抽象為獨立組件。在概念上,Spout 可以對應到 S4 中的無鍵 PE,但在模型層面,它被清楚區分為「產生資料」而非「處理資料」的單元。

Tuple

Tuple 是 Storm 中最小的資料單位。官方定義為一個帶有命名欄位的值列表。開發者在宣告時為欄位命名,但在實際傳輸過程中,系統只需傳輸欄位的位置與值。這種設計使得資料結構定義與傳輸過程分離。在概念上,Tuple 對應於 S4 的 Event。

Stream

Stream 是無界的 Tuple 序列。資料在 Topology 中透過 Stream 傳遞,並由系統分散到多個節點並行處理。Stream 並不代表一個有限集合,而是一個持續存在的資料流。

Bolt

Bolt 是執行計算邏輯的節點。它可以接收一個或多個輸入 Stream,並產生一個或多個輸出 Stream。所有業務邏輯 — — 例如轉換、過濾、聚合或將結果寫入外部系統 — — 都在 Bolt 中實作。

乍看之下,Bolt 與 S4 的 Processing Element(PE)非常相似。然而,兩者在抽象層面存在關鍵差異。

S4 在模型中明確定義 per-key Processing Element。每個 key 對應一個 PE instance,狀態與 key 綁定。而在 Storm 中,Bolt 並不天然具備 per-key 語意。若需要將相同 key 的 Tuple 發送至同一個 Bolt 任務,必須透過 grouping 機制(例如 fields grouping)來實現。換言之,per-key 行為在 Storm 中是一種配置結果,而不是模型內建抽象。

Get Ryan Fu’s stories in your inbox

Join Medium for free to get updates from this writer.

此外,PE 在 S4 中被視為 stateful 的核心單元;而 Bolt 是否維護狀態完全取決於開發者實作。Storm 並未在模型層制度化狀態管理。

Topology

Topology 是 Storm 中最關鍵的抽象。與 S4 不同,這張圖不再是執行過程中逐漸形成的結構,而是由開發者在系統啟動前明確定義。

開發者需要清楚描述:

  • 有哪些 Spout 與 Bolt
  • 它們之間如何連接
  • 資料應該如何在不同節點之間流動(透過 grouping)

在這個過程中,Topology 會在提交(submit)到叢集前經過基本的驗證,確保整體結構與設定是合法且可執行的。換句話說,這張圖不只是設計概念,而是一個會被系統接受並執行的計算描述。

在大多數情況下,Topology 被設計為一個有向無環圖(DAG)。這樣的限制,使資料能夠沿著單一方向流動,也讓系統在追蹤資料處理狀態與進行錯誤恢復時,能維持相對簡單的模型。這一點與 Storm 的 ack 機制與 tuple tree 設計密切相關,後續會再詳細說明。

不過,這並不代表 Storm 完全無法表達具有回饋(feedback)的計算。透過較高階的 API(例如 Trident)或結合外部系統,開發者仍然可以實現帶有「環」的邏輯流程。但在核心模型中,Storm 仍然傾向將計算限制在 DAG 之內,以換取更可控的可靠性與系統行為。

想進一步思考的讀者或許會問:什麼樣的情況,會需要一個帶有 feedback 的計算圖?

答案通常出現在那些「結果會影響下一步計算」的場景。例如圖演算法中反覆更新節點權重、機器學習中持續調整模型參數,或是即時推薦系統中,根據使用者最新行為不斷修正排序結果。這類問題的共通點在於:計算並不是單向推進的,而是需要將上一輪的輸出重新納入下一輪的輸入,形成一種持續迭代的過程。

簡而言之,DAG 所描述的是一種「資料一路往前流動」的世界;而一旦引入 feedback,計算就不再只是流動,而是開始「回頭修正自己」。這種模型在表達能力上更為強大,但同時也會大幅增加系統在狀態管理與錯誤恢復上的複雜度。

而 Word Count 這個用例的圖是一個典型的 DAG。

Use Case: Word Count

Press enter or click to view image in full size

Storm 官方文件中最常見的示例,是對 Tweet 進行即時單詞統計。我們可以從這個例子,理解 Storm 的可靠傳遞機制。

整個 Topology 的起點是一個 TweetSpout。它從外部資料流中讀取 Tweet,將每一條 Tweet 作為一個 Tuple 發送進系統。這個最初由 Spout 發出的 Tuple,被視為整棵計算樹的根。

接著,Tuple 被隨機分派到 ParseTweetBolt。這個 Bolt 將 Tweet 解析為單詞,並為每個單詞產生新的 Tuple,發送給下游的 WordCountBolt。不同的單詞會透過 fields grouping 被送往不同的 WordCountBolt 任務,因此整個計算過程呈現分散式並行。

在這個過程中,任何一個下游 Tuple 若未成功處理,都意味著整體處理失敗。Storm 的設計並不是局部補救,而是從 Spout 重新發送整條根 Tuple。這正是 Storm 所謂的 tuple tree 模型:所有由 Spout 發出的 Tuple,與其衍生出的所有子 Tuple,共同構成一棵邏輯上的樹。

為了追蹤這棵樹是否處理完成,Storm 引入了 Acker 機制。

The XOR-Based Ack Mechanism

在 Storm 中,每一個由 Spout 發出的 Tuple,都會被分配一個 64 位元的 message-id。我們可以想像系統中有一個 Spout_1,它從外部資料來源讀取一筆資料,例如一條 Tweet,並將其封裝成一個 Tuple 發送出去。假設這筆資料的 message-id 是 M。

當 Spout_1 發送這個 Tuple 的同時,系統會通知 Acker 開始追蹤這筆資料。此時,Acker 會建立一筆紀錄:

M → checksum = M

這裡可以把 checksum 想成一個「尚未完成工作的總和」。一開始只有這一筆資料,因此 checksum 就等於 M。本質上,Acker 的任務就是持續更新這個值,直到所有相關工作都完成為止。

接下來,這個 Tuple 進入第一個 Bolt,例如 ParseTweetBolt_1。這個 Bolt 將一句話拆解成多個單詞,並產生多個新的 Tuple,例如 [“the”], [“fox”], [“jumps”] 等。這些新 Tuple 都會帶著同一個 root message-id M。

當 ParseTweetBolt_1 發送這些新 Tuple 時,它會通知 Acker:「我產生了新的工作」。同時,當它完成對原始 Tuple 的處理時,也會再通知 Acker:「我已經處理完了這一段」。

後續的 Bolt,例如 WordCountBolt_1、WordCountBolt_2,也會做同樣的事情:
每當它們

  • 發送新的 Tuple(代表新增工作)
  • 或完成一個 Tuple 的處理(代表完成工作)

都會通知 Acker。

Acker 並不儲存整棵 Tuple tree,也不記錄每個節點之間的關係。它只做一件事情:用 XOR(異或)運算,把這些「新增工作」與「完成工作」累積到 checksum 上。

這個機制之所以成立,關鍵在於 XOR 的一個性質:

A XOR A = 0

也就是說,一個工作被「加入」一次、再被「完成」一次,對 checksum 的影響會互相抵消。因此,你可以把 checksum 理解為系統中「還沒完成的工作」的總體表示。只要還有任何一個衍生出的 Tuple 尚未完成處理,checksum 就不會是 0。一旦整棵 Tuple tree 上的所有 Tuple 都完成處理,所有的 XOR 最終會抵消為 0。此時,Acker 會通知 Spout_1:

message-id = M 的這筆資料,已經被完整處理。

這樣的設計還有一個重要的工程優勢。對於每一筆 root message-id,Acker 並不需要儲存整棵計算樹。它只需要記錄兩個 8 bytes 的數值,就可以代表整棵分散式計算流程的狀態:

  • message-id(8 bytes)
  • checksum(8 bytes)

換句話說,Acker 是用一個非常小的固定大小狀態,來追蹤一個可能非常龐大、跨多個節點的計算過程。

最後,Storm 會為每一筆 root Tuple 設定一個 timeout(預設約數十秒)。如果在這段時間內,checksum 一直沒有回到 0,代表仍然有某些工作沒有完成 — — 可能是某個 Bolt 處理失敗,或資料在系統中遺失。在這種情況下,Storm 會判定這筆資料處理失敗,並通知 Spout_1 重新發送這筆資料,讓整個流程重新執行一次。

整體而言,這個機制的本質可以用一句話概括:Storm 用一個簡單的 checksum,來代表整棵計算樹「還沒完成的部分」,當它變成 0,就代表整筆資料處理完成。

這是一種極為工程化、同時也相當優雅的設計:不用保存複雜的資料結構,只透過位元運算,就能追蹤整個分散式系統中的計算進度。

Delivery Guarantees

然而,這套 Ack 機制所提供的保證是 at-least-once。

若任何一個下游 Tuple 未成功處理,Storm 會重新發送整條根 Tuple。這意味著其他已成功處理的分支也可能被重複執行。以 Top K 的例子來說,若某個單詞處理失敗,整句 Tweet 可能被重新統計,造成重複計數。

若關閉 ack 機制,Storm 便成為 at-most-once 系統。上游發送後不再追蹤是否成功處理,可能導致資料遺失。

What are Still Missing in Kafka + Storm

如果我們將 Kafka 與 Storm 組合在一起,一個負責資料的持續流動,一個負責資料的即時處理,乍看之下,似乎已經具備了一個完整的流式資料處理系統。然而,當這樣的組合真正進入業務場景時,很快就會遇到一個根本性的問題:資料是否被正確地處理。

所謂正確,指的是精確一次、狀態與時間這三個面向。

Exactly Once

Kafka 確保資料能夠被持續傳遞,而 Storm 負責即時處理這些資料。Storm 透過 ack 機制提供了 at-least-once 的處理保證,確保從 Kafka 讀取的資料最終會被處理。然而,這並不保證資料只被處理一次。一旦發生失敗與重試,整條 Tuple chain 可能被重新執行,導致計算結果被重複更新。

這正是 Exactly Once 困難之處。它不僅要求資料不遺失,還要求整個計算過程在面對失敗時,對外呈現的結果等價於只執行一次。這意味著系統必須同時處理訊息重送、狀態更新與外部副作用的一致性,而這在分散式環境中並沒有簡單解法。

這個問題在計費相關的應用中尤為關鍵。例如廣告點擊計費、雲端服務用量計費或金融交易處理,每一筆事件都直接對應金額的增減。一旦某筆資料被重複處理,就可能造成重複扣款或錯誤計費。相較之下,資料遺失在這類場景中也同樣不可接受,系統必須同時避免兩者發生。這也使得 exactly-once 不再只是理論上的理想,而是實務上不可妥協的需求。

Persistent State Management

這個問題在 Kafka + Storm 的組合中會更加明顯。

Kafka 負責保存資料流,讓資料可以被重放(replay);Storm 則負責即時處理這些資料。然而,處理過程中的狀態,卻只存在於 Storm 的計算節點之中,通常保存在記憶體裡。

這與 MapReduce 的世界形成鮮明對比。MapReduce 建立在 batch 模式之上,輸入資料固定,計算有明確邊界。當任務失敗時,只需重新執行整個 job,即可得到一致結果,因為狀態本質上是可重建的。

但在 Kafka + Storm 的架構下,資料是無界的,計算是長期存在的,狀態持續演化。一旦某個 Bolt 所在的節點失效,該節點上的狀態可能直接遺失。即使 Kafka 能夠重新提供資料,系統也無法準確恢復先前的計算狀態。

因此,僅僅依靠 Kafka 的資料重放與 Storm 的訊息重送是不夠的。我們需要將計算節點所依賴的狀態資訊持久化下來,使系統在故障後能夠恢復到一致狀態,而不是重新開始。這不僅是容錯的需求,同時也是支援線上擴容與狀態遷移的基礎。

換句話說,在 Kafka + Storm 的架構中,state 並沒有被視為一等公民。

Time Window

然而,即使 Kafka 解決了資料流動,Storm 解決了資料處理與可靠傳遞,仍然存在一個更根本的問題:

在一條無界的資料流中,我們如何界定「一次計算應該涵蓋的範圍」?

Kafka 持續提供資料流,而 Storm 持續處理資料,但兩者都沒有回答這個問題。

在 batch 系統中,這個問題並不明顯,因為整個資料集本身就隱含了一個時間邊界。但在 Kafka + Storm 的 streaming 架構中,資料沒有開始,也沒有結束。任何聚合操作,都必須先定義其觀察範圍。

這正是時間窗口(time window)的概念。時間窗口將無界的資料流切分為一段一段有界的區間,使得統計與聚合具有明確語意。

進一步地,我們還必須決定時間的定義方式:是以資料產生的時間(event time)為準,還是以系統接收到資料的時間(processing time)為準。

event time 通常用於對「真實世界行為」敏感的業務場景,例如廣告點擊、交易紀錄或使用者行為分析。在這些情境中,資料可能延遲到達,但仍需依照實際發生時間進行統計。

processing time 則適用於對即時反應要求較高、但允許一定誤差的場景,例如即時監控、系統告警或 dashboard 顯示。這類應用更關心的是「系統現在看到什麼」,而不是「事件真正發生在什麼時候」。資料一旦被系統接收到,就立即參與計算,而不會等待可能延遲到達的歷史資料來修正結果。

例如,在監控系統中,我們希望即時知道目前每秒的請求數是否過高。如果某些資料因為網路延遲晚了幾秒才到達,通常不會回頭修正已經顯示的監控數據,而是直接忽略這些延遲事件。這種情況下,使用 processing time 能夠提供更低延遲、也更簡單的實作。

不同的窗口定義,如固定窗口(tumbling window)、滑動窗口(sliding window)或會話窗口(session window),各自對應不同的業務需求。

tumbling window 將時間切分為不重疊的區間,例如每五分鐘統計一次請求數,常用於報表與計費場景。sliding window 允許窗口之間重疊,例如每分鐘更新過去五分鐘的統計結果,適用於即時趨勢分析與監控系統。session window 則根據事件之間的間隔動態決定邊界,例如將一段時間內連續的使用者行為視為同一個 session,常用於使用者行為分析。

在 S4 與 Storm 時代,這三種窗口已經以各種形式存在於工程實作中,只是尚未被統一命名,也缺乏明確的模型。直到 Google 在 2015 年提出 Dataflow 模型,這些分散的實務經驗才首次被系統化整理,並被納入一個統一的計算框架之中。Dataflow 明確定義了 window 作為切分無界資料流的核心抽象,並將不同類型的 window 視為計算語意的一部分,而不再只是應用程式中的實作細節。

綜合來看,即使我們擁有 Kafka 與 Storm,一個能夠傳遞資料,一個能夠處理資料,我們仍然缺少三個關鍵要素:Exactly Once 的處理語意、狀態的持久化管理,以及時間窗口的內建模型。

Summary

從 at-least-once 到 exactly-once,是 streaming 領域的關鍵里程碑。而通往這個里程碑的路徑,在 storage 與 processing 兩個層面上分別展開。

在 Stream Storage 的層面,這條路徑由 Early Kafka 演進到 Modern Kafka,逐步建立起可靠的資料持久化與重放機制。

而在 Stream Processing Engine 的層面,這個里程碑則由 Flink 跨越。Flink 不僅補上了 Storm 所缺乏的 exactly-once 語意,還進一步將 state 視為一等公民,建立了完整的 Persistent State Management,並將 Time Window 納入計算模型之中。

換句話說,Kafka 解決了「資料如何可靠地流動」,而 Flink 則開始回答「資料應該如何被正確地計算」。

因此,在下一章,我們自然要問:當我們同時面對精確一次、狀態與時間這三個核心挑戰時,一個完整的 Stream Processing Engine,究竟是如何被建構出來的?

(To be continued …)

Appendix

  1. [極客時間] 大数据经典论文解读 25 | 从S4到Storm(一):当分布式遇上实时计算
  2. [極客時間] 大数据经典论文解读 26 | 从S4到Storm(二):位运算是个好东西
  3. [極客時間] 大数据经典论文解读 29 | Dataflow(一):正确性、容错和时间窗口
  4. Streaming 101: The world beyond batch: https://www.oreilly.com/radar/the-world-beyond-batch-streaming-101/
  5. Streaming 102: The world beyond batch: https://www.oreilly.com/radar/the-world-beyond-batch-streaming-102/
  6. Storm @ Twitter(Nathan Marz, 2014):https://classpages.cselabs.umn.edu/Spring-2018/csci8980/Papers/StreamProcessing/Storm.pdf
  7. Apache Storm 官方文件:Guaranteeing message processing: https://storm.apache.org/releases/2.6.0/Guaranteeing-message-processing.html

--

--

Ryan Fu
Ryan Fu

Written by Ryan Fu

探究一切事物的本質