昨天我們學會了用 Flink SQL 構建大寬表,一個 INSERT 語句就能 JOIN 所有相關表,看似完美解決了 OLAP 分析的需求。但在實際的生產環境中,你會發現一個關鍵問題:大寬表一旦 sink 到 JDBC,就無法再進行 streaming read 了!
Streaming Read:數據以「持續流動」的方式被讀取,新數據一到達就立刻被處理。
Streaming Read (流式讀取):
Kafka Topic → Flink → 持續監聽新數據 → 即時處理
JDBC Read (批次讀取):
Database → 讀取現有數據 → 處理結束
為什麼 JDBC 無法 Streaming Read?
-- JDBC 只能這樣讀(批次查詢)
SELECT * FROM order_wide_table WHERE order_date > '2024-01-01';
-- 問題:只能讀到「當下存在」的數據,無法持續監聽新數據
-- Kafka 可以這樣讀(流式消費)
CREATE TABLE order_stream (...) WITH ('connector' = 'kafka', ...);
-- 優勢:新數據一寫入 Kafka 就立刻被 Flink 消費處理
實際影響:一旦數據寫入 JDBC,就失去了「實時響應新數據」的能力,只能定期批次查詢。
其實 Day 22 的架構(大寬表 + OLAP)已經可以在資料庫裡直接 GROUP BY 做各種分析:
-- 在 OLAP 資料庫中直接查詢
SELECT city, DATE_TRUNC('hour', order_date), SUM(payment_amount)
FROM order_wide_table
GROUP BY city, DATE_TRUNC('hour', order_date);
但如果你想要:
那就需要在 Flink 層面進行更多計算。這就引出了現代數據架構的進階設計:Kafka 多層架構。
企業級數據架構遵循 Medallion Architecture(獎章架構)設計模式:
Modern Data Architecture:
Bronze Layer (Kafka Raw Data):
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ orders │ │ customers │ │ payments │ │ deliveries │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │ │ │
└─────────────────┼─────────────────────┼─────────────┘
│ │
▼ ▼
Silver Layer (Wide Table):
┌─────────────────────────────────────────────┐
│ Kafka: order_wide_table │
│ (Enriched Data) │
└─────────────────────────────────────────────┘
│
│ Multiple Consumers
▼
Gold Layer (Aggregated Data):
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌────────────┐
│ JDBC: city_sales│ │Kafka:hourly_topn│ │JDBC:payment_stats│ │Kafka:alerts│
└─────────────────┘ └─────────────────┘ └──────────────────┘ └────────────┘
Bronze Layer(銅牌層):
Silver Layer(銀牌層):
Gold Layer(金牌層):
重要提醒
本文所有程式碼皆為教學與概念演示用的 Flink SQL,可以幫助你理解並解決實際場景的問題,但這裡的程式碼需要根據實際環境調整,主要目的是用來講解多層架構設計的思路與核心概念。
Silver 層的關鍵設計:
-- 關鍵:Silver 層寫到 Kafka,不是 JDBC!
CREATE TABLE order_wide_table_silver (
...
) WITH (
'connector' = 'kafka',
'topic' = 'order_wide_table_silver',
'value.format' = 'json'
);
-- 大寬表 JOIN(昨天學會的)
INSERT INTO order_wide_table_silver
SELECT ...
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
LEFT JOIN payments p ON o.order_id = p.order_id
LEFT JOIN deliveries d ON o.order_id = d.order_id;
現在可以對 Silver 層進行 streaming 聚合:
-- Gold 層 1: 城市銷售排行榜
CREATE TABLE city_sales_ranking (
window_start TIMESTAMP(3),
window_end TIMESTAMP(3),
city STRING,
total_sales DECIMAL(10,2),
ranking BIGINT
) WITH (
'connector' = 'kafka',
'topic' = 'city_sales_ranking'
);
INSERT INTO city_sales_ranking
SELECT
window_start,
window_end,
city,
total_sales,
ROW_NUMBER() OVER (PARTITION BY window_start ORDER BY total_sales DESC) as ranking
FROM (
SELECT
TUMBLE_START(order_date, INTERVAL '1' HOUR) as window_start,
TUMBLE_END(order_date, INTERVAL '1' HOUR) as window_end,
customer_city as city,
SUM(payment_amount) as total_sales
FROM order_wide_table_silver -- 從 Silver 讀取!
GROUP BY
TUMBLE(order_date, INTERVAL '1' HOUR),
customer_city
)
WHERE ranking <= 10; -- TOP 10
-- Gold 層 2: 付款方式統計
CREATE TABLE payment_method_stats (
window_start TIMESTAMP(3),
payment_method STRING,
success_count BIGINT,
total_count BIGINT,
success_rate DECIMAL(5,2)
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://localhost:3306/analytics'
);
INSERT INTO payment_method_stats
SELECT
TUMBLE_START(paid_at, INTERVAL '5' MINUTE) as window_start,
payment_method,
SUM(CASE WHEN payment_status = 'success' THEN 1 ELSE 0 END) as success_count,
COUNT(*) as total_count,
CAST(SUM(CASE WHEN payment_status = 'success' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) AS DECIMAL(5,2)) as success_rate
FROM order_wide_table_silver -- 同一個 Silver 源可以多次使用
GROUP BY
TUMBLE(paid_at, INTERVAL '5' MINUTE),
payment_method;
單層架構 (Bronze → Gold):
多層架構 (Bronze → Silver → Gold):
新的分析需求來了?不用重新 JOIN 多張表,直接從 Silver 層讀取即可:
-- 新需求:產品銷量分析
INSERT INTO product_sales_analysis
SELECT
product_id,
SUM(quantity) as total_sold,
AVG(unit_price) as avg_price
FROM order_wide_table_silver -- 直接使用現有 Silver 層
GROUP BY product_id;
多層 Kafka 架構提供了強大的技術能力 - 從 Day 22 的基礎大寬表到 Day 23 的進階聚合,我們解決了「JDBC 無法 streaming read」的關鍵問題。Silver 層存在 Kafka 中保持流動性,讓同一份大寬表可以支撐多種實時分析需求。
實務上的選擇:目前業界的 Realtime Data 應用主要有兩種路線 - 大寬表 + OLAP 計算(Day 22 的方式)或是全部 Flink 計算(Day 23 的多層架構)。技術實現上,無論是 Flink SQL 的 JOIN、GROUP BY、還是窗口函數,程式碼都不是什麼難事。
但當這些美好的架構真正運行在生產環境時,你會遇到一些讓人頭痛的現實問題。
場景:業務邏輯變更需要重跑歷史數據
想像你已經穩定運行了半年的大寬表流處理,突然業務方說:「我們發現訂單金額計算有bug,VIP客戶的折扣邏輯錯了,需要修正並重新計算過去 3 個月的所有訂單數據。」
為什麼需要全量重跑?Stream JOIN 的狀態依賴
Stream JOIN 需要維護狀態。這是 Streaming 與 Batch 處理的根本差異:
Batch vs Streaming 重跑對比:
Batch 處理:
SELECT * FROM orders o JOIN payments p ON o.order_id = p.order_id
WHERE order_date >= '2024-01-01'
→ 一次查詢,立即得到所有結果
Streaming 處理:
需要按時間順序逐一重放每個事件:
Event 1: Order(123) → 建立狀態 {123: waiting}
Event 2: Payment(123) → 匹配狀態,輸出結果
Event 3: Order(456) → 建立狀態 {456: waiting}
...
(必須按原始順序,一筆一筆重建狀態)
挑戰:
• Kafka retention:歷史數據可能已經過期刪除 -> 需要想辦法重新 produce 歷史數據
• 狀態重建:需要從零開始重建所有 JOIN 狀態
• 資源消耗:重跑歷史數據會消耗大量計算資源
場景:Silver 層大寬表出現異常數據
客戶反映某筆訂單的金額計算有誤,但這筆數據已經經過了複雜的多表 JOIN:
排查困難:
• 流式數據:Kafka 中的數據無法直接 SQL 查詢
• 時間窗口:需要確定問題發生的確切時間範圍
• 多數據源:錯誤可能來自 orders、customers、payments、deliveries 任一環節
• 狀態追蹤:JOIN 過程中的中間狀態難以重現
實際痛點:
-- 傳統資料庫可以這樣查
SELECT * FROM order_wide_table WHERE order_id = '12345';
-- 但 Kafka 中的流式數據無法直接查詢
-- 你需要:
-- 1. 檢查 Kafka Consumer offset
-- 2. 手動追蹤數據在各個 Topic 中的流動
維運工程師的真實感受:
長期備份策略:
一個常見的解決方案是將中間表(如 Silver 層的大寬表)同時備份到可以長期保存的存儲系統:
架構升級:
Kafka Silver Layer → ┌─ 下游 Gold Layer (Kafka/JDBC)
└─ 長期存儲 (S3/HDFS/Data Warehouse)
↓
可直接 SQL 查詢歷史數據
優勢:
代價:
這種方案體現了實務上的經典權衡:用系統複雜度換取維運便利性。對於大型企業來說,這個代價往往是值得的。
這些維運挑戰往往比技術實現更讓人頭疼,也是企業級 Flink 部署必須面對的現實問題。
今天我們從多層 Kafka 架構的設計開始,深入探討了企業級流處理的現實挑戰。多層架構解決了「JDBC 無法 streaming read」的問題,讓 Silver 層保持在 Kafka 中的流動性,支撐多種實時分析需求。
但更重要的是,我們揭露了生產環境中的嚴峻現實:
從這一系列的學習中可以看出,狀態是 Streaming 領域一個極其重要的概念。無論是窗口聚合或者是 JOIN 處理,所有這些問題的核心都指向同一個關鍵:如何有效管理和優化流處理中的狀態。
我們在「歷史資料重放」部分看到了問題,但還沒有深入討論解決方案。事實上,狀態管理如此重要和複雜,值得獨立一個完整章節來深入討論!
明天我們將專門聚焦於 Flink 的狀態管理難題:
大狀態帶來的挑戰:
技術解決方案:
從問題分析到解決方案,讓我們徹底搞懂 Flink 狀態管理的方方面面!