在上一篇我們理解了 Flink 的設計理念:真正的流式處理。今天我們要深入 Flink 的內部世界,掌握那些在生產環境中必須理解的核心概念。
通過前面構建 SimpleStream 的經驗,我們已經理解了流處理的基本原理。現在讓我們看看 Flink 這個工業級系統是如何解決同樣的問題。
網路上已經有很多關於如何架設 Flink 的文章,這邊不多加著墨。本篇會先介紹 Flink 中重要且對應我們 SimpleStream 的組件與概念,後續幾天會分享筆者在「用 Flink SQL 開發商業邏輯」方面的實務經驗。
還記得我們在 SimpleStream 中構建的 SimpleStreamingEngine 嗎?它負責協調 Source、DataFrame 和 Sink 之間的工作。Flink 將這個概念擴展到分散式環境,形成了 JobManager 和 TaskManager 的架構。
想像 Flink 集群就像一個現代工廠:
┌─────────────────────┐
│ JobManager │ ◄── Factory Supervisor (Brain)
│ │
│ + Task Schedule │
│ + Resource Control │
│ + Checkpoint │
└─────────────────────┘
│
│ Manage
▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ TaskManager │ │ TaskManager │ │ TaskManager │
│ │ │ │ │ │
│ + Execute Tasks │ │ + Execute Tasks │ │ + Execute Tasks │
│ + State Management │ │ + State Management │ │ + State Management │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
JobManager(工廠總監)的職責:
TaskManager(生產線工人)的職責:
從 SimpleStreamingEngine 到分散式架構
在 SimpleStream 中,我們的 SimpleStreamingEngine 在單機上完成所有協調工作。Flink 將這個概念分散化:
Flink 的 DataStream API 與我們在 SimpleStream 中設計的 SimpleDataFrame 概念相似,都支援鏈式調用和聲明式的數據處理風格。
而 Flink SQL 更是與我們在 Day 18 探討的「SQL 到流式處理轉換」理念不謀而合 - 讓熟悉 SQL 的工程師能直接用 SQL 語法進行流處理,系統自動轉換為對應的流式算子。
在 SimpleStream 中,我們實作了基礎的 Source 和 Sink。Flink 將這個概念擴展到工業級:
External Systems Flink Cluster External Systems
┌─────────┐ ┌─────────────────────┐ ┌─────────┐
│ Kafka │--->│Source→Transform→Sink│--->│ Database│
│ Files │ │ │ │ Kafka │
│ APIs │ │ │ │ Files │
└─────────┘ └─────────────────────┘ └─────────┘
Source 的核心能力:
Sink 的核心能力:
雖然 Flink 標榜 True Streaming,但在實際生產環境中,Flink 也提供了 MiniBatch 參數來優化性能。這個概念其實在我們 SimpleStream 的設計中也有體現 - 可以選擇逐筆處理或是累積一定數量後批量處理。
為什麼需要 Micro Batch?
在高吞吐量場景下,逐筆處理可能造成:
Flink 的 MiniBatch 設定:
-- 啟用 MiniBatch 聚合
SET table.exec.mini-batch.enabled = true;
-- 設定批次大小(筆數)
SET table.exec.mini-batch.allow-latency = 1s;
-- 設定最大延遲時間
SET table.exec.mini-batch.size = 1000;
這讓 Flink 能在延遲與吞吐量之間找到最佳平衡點,這正是工業級系統需要的彈性。
想像一條高速公路突然遇到施工:
正常情況:
車流 ───▶ 車流 ───▶ 車流 ───▶ 出口
快速 快速 快速
遇到瓶頸:
車流 ───▶ 車流 ───▶ 堵車 ───▶ 施工區
快速 快速 慢慢 很慢
背壓傳播:
慢行 ───▶ 慢行 ───▶ 堵車 ───▶ 施工區
調節 調節 慢慢 很慢
Flink 的背壓機制:
為什麼背壓很重要?
這是 Flink 最精妙的設計之一
通過 Checkpoint 機制實現,基於 Chandy-Lamport 分散式快照算法:
詳細的 Checkpoint 流程:
1. 觸發階段:
JobManager 定期(例如每5秒)向所有 Source 節點發送 checkpoint 觸發指令
2. Barrier 注入:
Source 接收到指令後,在當前數據流中插入一個特殊的 checkpoint barrier
barrier 包含唯一的 checkpoint-id(如 checkpoint-1001)
3. 快照保存:
節點收到 barrier 時的行為:
- 算子狀態保存:將當前算子的所有狀態快照保存到 StateBackend(如 HDFS、S3)
- 偏移量記錄:記錄外部系統的消費位置(如 Kafka offset)
- 狀態元數據:記錄狀態的版本、大小、存儲位置等信息
- 將 barrier 繼續向下游傳播
4. 同步等待:
下游節點必須等待所有上游的 barrier 都到達後,才能保存自己的狀態
這確保了全局狀態的一致性
5. 完成確認:
當所有節點都完成快照保存,JobManager 收到確認後
此 checkpoint 被標記為成功,可以用於故障恢復
故障恢復過程:
- 檢測到故障 → 停止所有處理
- 從最近成功的 checkpoint 恢復所有節點狀態
- 從保存的 offset 位置重新消費數據
- 重新處理故障點之後的數據
為什麼這樣能保證 Exactly-once?
實際例子(電商訂單統計):
時刻 T1: Kafka offset=100
算子狀態: {customer_123: count=5, customer_456: count=3}
時刻 T2: Kafka offset=150 ← checkpoint 成功
算子狀態: {customer_123: count=8, customer_456: count=6, customer_789: count=2}
時刻 T3: Kafka offset=200
算子狀態: {customer_123: count=12, customer_456: count=8, customer_789: count=4}
時刻 T4: 故障發生
恢復後:
- 恢復 Kafka Consumer 的 offset=150
- 恢復算子狀態: {customer_123: count=8, customer_456: count=6, customer_789: count=2}
- 重新處理 offset 150-200 的數據,繼續累加統計
- 結果:每個客戶的訂單數量恰好被統計一次,無重複無遺漏
Flink 提供三種 StateBackend,對應不同的使用場景:
MemoryStateBackend:
┌─────────────┐
│ JVM Heap │ ← 狀態存儲在 TaskManager 記憶體
│ │
│ State Data │
└─────────────┘
適用:開發測試、小狀態場景
限制:受 JVM 記憶體大小限制
FsStateBackend:
┌─────────────┐ ┌─────────────┐
│ JVM Heap │--->│ File System │ ← CheckPoint 存到分散式檔案系統
│ │ │ (HDFS/S3) │
│ State Data │ │ │
└─────────────┘ └─────────────┘
適用:中等規模生產環境
特點:平衡性能和可靠性
RocksDBStateBackend:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ JVM Heap │--->│ RocksDB │--->│ File System │
│ │ │ (Local SSD) │ │ (HDFS/S3) │
│ Hot Data │ │ State Data │ │ CheckPoints │
└─────────────┘ └─────────────┘ └─────────────┘
適用:大規模生產環境(TB 級狀態)
特點:狀態大小不受記憶體限制
在實際業務中,我們經常需要關聯不同來源的數據:
Lookup Join:靜態數據關聯(無狀態)
Real-time Stream: User Click Events
│
▼
┌─────────────┐ Instant Query ┌─────────────┐
│ Click Event │ --------------> │ User Profile│
│user_id: 123 │ (Stateless) │ (Database) │
│item_id: 456 │ │ │
└─────────────┘ └─────────────┘
│ │
│ Immediate Response, No Waiting │
▼ ▼
┌──────────────────────────────────────────┐
│ Enriched Event: Click + User Profile │
│ user_id: 123, age: 25, city: "Taipei" │
│ item_id: 456 │
└──────────────────────────────────────────┘
特點:
+ 無需儲存流數據狀態
+ Instant query, no waiting delay
+ 記憶體消耗低
- 依賴外部系統可用性
- 查詢延遲影響整體性能
Streaming Join:動態數據關聯(有狀態)
Order Stream ┌─────────────┐
--------------->│ │
│ JOIN │───────────▶ Complete Order Info
Payment Stream │ │
--------------->│ │
└─────────────┘
特點:
+ 不依賴外部系統
- 需要大量狀態儲存
- 記憶體消耗高
Streaming Join 的狀態儲存機制:
Join 算子內部狀態
┌─────────────────────────────────┐
Order │ Left State (Order) │
Stream ----->│ ┌─────────────────────────┐ │
│ │ order_id: 123 │ │
│ │ amount: 100 │ │◄── Wait Payment
│ │ timestamp: 10:30 │ │
│ └─────────────────────────┘ │
│ │
Payment │ Right State (Payment) │
Stream ----->│ ┌─────────────────────────┐ │
│ │ order_id: 123 │ │◄── Wait Order
│ │ status: paid │ │
│ │ timestamp: 10:32 │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
│
When order_id matches
▼
┌─────────────────┐
│ Join Result │
│ order_id: 123 │
│ amount: 100 │
│ status: paid │
└─────────────────┘
除了 Join 操作外,Flink 還實現了豐富的算子來滿足各種業務需求:
這些算子都經過流式處理的特殊優化,能在連續數據流上提供與傳統 SQL 一致的語義。每個算子背後都有複雜的狀態管理和性能優化機制,但對開發者來說使用方式與標準 SQL 完全相同,大大降低了流處理開發的門檻。
Flink 的強大在於它的設計哲學:將複雜的分散式流處理包裝在簡潔的 API 之下。
當你在寫一行 SQL 或調用一個算子時,背後實際上是一套精密的分散式系統在運轉。JobManager 在協調全局,TaskManager 在並行處理,Checkpoint 在默默保障一致性。
這種設計讓開發者能專注業務邏輯,而不必糾結於分散式系統的技術細節。這就是工業級框架的價值。
理解了 Flink 的核心概念後,下一章我們將進入實務開發階段:如何用 Flink SQL 開發商業邏輯。
我們將探討:
從理論學習走向實際應用,讓我們看看如何用這些知識解決真實的業務問題。