数据工程实战课程 · 流处理阶段
窗口将无界流切分为有界切片进行分析,水位线处理乱序事件和迟到数据。
# 窗口计算引擎
import time, random
from datetime import datetime, timedelta
from collections import defaultdict
class Event:
def __init__(self, key, value, event_time, process_time=None):
self.key = key; self.value = value; self.event_time = event_time; self.process_time = process_time or datetime.now()
class WindowManager:
def __init__(self):
self.windows = defaultdict(list)
def tumbling(self, events, window_size_sec):
windows = defaultdict(list)
for e in events:
wid = int(e.event_time.timestamp() / window_size_sec)
windows[wid].append(e)
return {wid: {'events':len(evts),'sum':round(sum(e.value for e in evts),2),'avg':round(sum(e.value for e in evts)/len(evts),2)} for wid, evts in windows.items()}
def sliding(self, events, window_size_sec, slide_sec):
windows = defaultdict(list)
for e in events:
start = int(e.event_time.timestamp() / slide_sec)
for wid in range(start, start + window_size_sec // slide_sec + 1):
windows[wid * slide_sec].append(e)
return {wid: {'count':len(evts),'avg':round(sum(e.value for e in evts)/len(evts),2)} for wid, evts in sorted(windows.items())[:5]}
class Watermark:
def __init__(self, allowed_lateness_sec=5):
self.allowed_lateness = allowed_lateness_sec; self.current_watermark = None
def update(self, event):
wm = event.event_time - timedelta(seconds=self.allowed_lateness)
if self.current_watermark is None or wm > self.current_watermark:
self.current_watermark = wm
def is_late(self, event):
return event.event_time < self.current_watermark if self.current_watermark else False
# 生成测试事件(含乱序)
events = []
base = datetime(2024, 1, 1, 0, 0, 0)
for i in range(100):
# 80%按序, 20%乱序
if random.random() < 0.8:
et = base + timedelta(seconds=i)
else:
et = base + timedelta(seconds=max(0, i - random.randint(1, 10)))
events.append(Event(f"sensor_{random.randint(1,5)}", round(random.uniform(10,50),2), et))
# 按处理时间排序(模拟乱序到达)
random.shuffle(events)
wm = WindowManager()
watermark = Watermark(5)
late_count = 0
for e in events:
watermark.update(e)
if watermark.is_late(e): late_count += 1
tumbling = wm.tumbling(sorted(events, key=lambda e: e.event_time), 10)
sliding = wm.sliding(sorted(events, key=lambda e: e.event_time), 30, 10)
print("窗口计算引擎:")
print(f" 事件数: {len(events)}, 迟到: {late_count}")
print(f" 水位线: {watermark.current_watermark}")
print(f" 滚动窗口(10s): {len(tumbling)}个窗口")
for wid, stats in sorted(tumbling.items())[:3]:
print(f" 窗口{wid}: {stats}")
print(f" 滑动窗口(30s/10s): {len(sliding)}个窗口")
print("✅ 验证通过 - 窗口与水位线引擎运行正常")
🎁 下一课预告:实时ETL!
| 维度 | Flink | Spark Streaming | Kafka Streams |
|---|---|---|---|
| 延迟 | 毫秒级 | 秒级(微批) | 毫秒级 |
| Exactly-Once | 原生支持 | 需配置 | 支持 |
| 状态管理 | 丰富 | 有限 | 本地Store |
| 窗口支持 | 全面 | 全面 | 基本 |
| 部署复杂度 | 高 | 中 | 低 |
在生产环境中,我们经常遇到教科书不会告诉你的问题。以下是常见的实战经验和解决方案:
| 指标 | 告警阈值 | 监控方式 |
|---|---|---|
| 管道延迟 | 超过SLA 120% | 端到端时间戳追踪 |
| 数据量偏差 | 日均偏差超过30% | 与历史同期对比 |
| 错误率 | 超过0.1% | 错误计数/总记录数 |
| 数据新鲜度 | 超过SLA 150% | 最新数据时间戳 |
| 资源利用率 | CPU持续>80% | 系统监控 |
A: 根据业务延迟要求决定:秒级用流处理、分钟级用微批、小时级用批处理。大部分场景批处理足够,流处理用于实时业务场景(风控、监控、推荐)。
A: 不会完全取代,但边界在模糊。湖仓一体(如Iceberg/Delta Lake)正在融合两者优势。未来趋势是统一存储+多引擎计算。
A: 先用云原生服务(BigQuery/Snowflake + Fivetran + dbt + Looker),验证业务后再考虑自建。过早优化是万恶之源。
A: 用钱说话:数据质量问题的直接经济损失(错误决策、合规罚款、重复劳动)远大于治理投入。量化问题是第一步。
| 角色 | 职责 | 交付物 |
|---|---|---|
| 数据工程师 | 管道开发与维护 | 数据管道、基础设施 |
| 数据分析师 | 数据分析与报表 | SQL查询、BI报表 |
| 数据科学家 | 模型与实验 | ML模型、实验报告 |
| 数据产品经理 | 需求与优先级 | PRD、数据产品规划 |
| 数据管家 | 数据质量与治理 | 质量规则、数据字典 |
选择技术方案时,需要从多个维度综合评估,避免仅凭流行度或个人偏好决策:
| 评估维度 | 权重 | 评估要点 |
|---|---|---|
| 业务适配度 | 30% | 是否满足功能需求?延迟/吞吐是否达标? |
| 团队能力 | 25% | 团队是否有经验?学习曲线如何? |
| 社区生态 | 20% | 社区活跃度?文档质量?第三方集成? |
| 运维成本 | 15% | 部署复杂度?监控能力?故障恢复? |
| 成本效益 | 10% | License费用?资源消耗?扩展成本? |
数据量级? ├── <1GB/天 → SQLite/PostgreSQL + Python脚本 ├── 1-100GB/天 → 数据仓库(Snowflake/BQ) + dbt ├── 100GB-1TB/天 → Spark + 数据湖 └── >1TB/天 → Spark/Flink + 湖仓一体 延迟要求? ├── T+1 → 批处理(Spark) + Airflow ├── 小时级 → 微批(Spark Streaming) + Airflow ├── 分钟级 → 流处理(Flink) + Kafka └── 秒级 → 纯流(Flink) + Kafka + 缓存 数据类型? ├── 纯结构化 → 数据仓库 ├── 结构化+半结构化 → 湖仓一体 └── 全类型 → 数据湖 + 元数据管理