【数据工程】16. Notions of Time in Stream Processing
流处理中的时间概念(Notions of Time in Stream Processing)
流处理系统会区分三种不同的时间概念:事件时间(Event time)、摄取时间(Ingestion time) 和 处理时间(Processing time)。
区分这些时间对于定义窗口(window)和正确处理乱序数据非常重要。
幻灯片用《星球大战(Star Wars)》系列电影的上映顺序来说明这个区别:
-
事件时间(Event Time):是事件在现实世界中实际发生的时间。
比如在星战宇宙中,“第四部”的剧情发生在时间线上最早,但它在现实上映时是 1977 年;
后来拍摄的“第一部”(1999 年)反而在事件时间上更早。
→ 所以事件时间描述的是“逻辑顺序”或“发生时间”。 -
处理时间(Processing Time):是系统处理数据的时间。
对应到这个例子,就是电影真实上映的时间顺序:IV、V、VI、I、II、III、VII、VIII、IX(1977–2019)。
→ 系统可能按这个顺序接收和处理数据。
这说明:
- 事件时间与处理时间不一定相同。
- 数据可能乱序到达(out-of-order processing),系统必须能处理这种情况。
- 我们可以选择按事件时间或处理时间定义窗口,取决于我们想要分析的逻辑。
事件时间与水位线(Event Time and Watermarks)
当流处理系统基于“事件时间”运行时,它需要有一种机制来衡量事件时间的进展。
因为事件的到达顺序不一定与它的时间戳顺序一致。
-
事件时间(Event Time) 可以独立于 处理时间(Processing Time) 前进。
例如,系统当前时间可能是下午 3 点,但仍在处理上午 10 点的事件。
系统时钟(wall clock)并不反映事件流的真实“逻辑时间”。 -
关键挑战:
- 乱序事件(Out-of-Order Events):事件到达顺序与事件时间不符;
- 延迟数据(Late Data):事件到达得太晚,可能落在已关闭的窗口之后。
水位线(Watermark)的作用
为了解决上述问题,流处理引擎(如 Flink)引入了 Watermark(时间水位线) 概念。
-
水位线是一种声明(declaration):
表示“截至此时(watermark 的时间戳),系统认为所有事件时间早于这个时间戳的事件都已经到达”。 -
当一个算子(operator)收到水位线后,它可以:
- 将自己的内部事件时间时钟(event-time clock)推进到水位线的时间;
- 触发那些窗口结束时间早于该水位线的计算。
换句话说:
水位线是一种机制,用来告诉系统“现在可以安全地认为某一时间段的事件都到齐了”。
- 数据流乱序到达(out of order);
- 水位线 w(11) 表示系统认为 “时间 ≤ 11” 的事件都到齐;
- 所以可以安全地计算窗口 [0, 11] 的聚合;
- 后续的 w(17) 表示系统推进到 “时间 ≤ 17” 的阶段。
示例:Apache Flink 中的 Watermark 处理
Flink 的事件时间处理逻辑通过时间戳和水位线一起工作:
-
事件进入流中,并带有自己的事件时间戳;
-
Watermark 策略(WatermarkStrategy) 会定期生成水位线;
- 可以是固定周期;
- 或在检测到某些“标点符号(punctuation)”时触发;
- 或基于“允许最大延迟(allowed lateness)”计算。
-
晚到事件(late events):在水位线之后才到的事件,可以丢弃或特殊处理。
Flink 代码示例:
env.assignTimestampsAndWatermarks(WatermarkStrategy.for_bounded_out_of_orderness(Duration.ofSeconds(5))
)result = stream.window(TumblingEventTimeWindows.of(Duration.ofSeconds(10))).allowedLateness(Duration.ofSeconds(5)).sideOutputLateData(lateOutputStream)
解释:
- 每 5 秒生成一次水位线;
- 定义了一个 10 秒的滚动事件时间窗口;
- 系统允许事件最多延迟 5 秒;
- 超过 5 秒仍未到的事件被认为是“晚到”,放到
lateOutputStream里单独处理。
总结:
- Watermark 让系统能在乱序环境中判断“何时窗口可以关闭”;
- 同时允许对延迟数据设置一定容忍度,保证既不丢数据,也不阻塞计算。
流连接(Stream Joins):将流与其他数据结合
在流处理中,Join 是关键操作之一,用于将不同来源的数据结合起来。常见三种类型:
1. 传统表连接(Conventional table joins)
两个独立的数据流(Stream1、Stream2)分别对应两张表(Table1、Table2),
然后用传统数据库式 Join 算法产生结果。
→ 这适合“流入表”的批式场景,不是真实时流。
2. 丰富化(Enrichment)
使用静态的参考数据表来补充流数据。
比如:
- 实时交易流(stream)输入;
- 静态商品信息存储在 Redis 或数据库;
- 实时连接两者,得到交易+商品详情的组合结果。
3. 流与流连接(Stream-to-Stream Joining)
两个实时流同时流入,一个专门的 Stream Join Processor 负责匹配。
例如订单流与支付流,或者传感器流与告警流。
流与表的连接(Stream-Table Join, Enrichment)
通过将流式数据与静态参考数据结合,可以进行更丰富的分析。
-
挑战:
如何既能访问最新的参考数据,又不影响实时性能?
因为直接查数据库会延迟很高。 -
解决方案:
使用内存缓存(in-memory store)或外部高性能存储(如 Redis)加速查找。 -
示例:
把实时交易流与 Redis 中的商品表关联(join)。 -
最佳实践:
- 高频访问的数据放内存;
- 大规模数据放外部存储;
- 避免直接读取生产数据库(以免拖慢主系统)。
流与流的窗口连接(Stream-Stream Join, Windowed)
右侧表格演示了一个 15 秒窗口的流-流连接:
假设:
- 每条记录都有相同的连接键;
- 所有记录按时间戳顺序处理;
- 窗口长度为 15 秒。
工作机制:
- 当左流(Left Stream)在时间戳 3 到达记录 A;
- 右流(Right Stream)在时间戳 4 到达记录 a;
- 它们都落在同一个窗口内,于是输出 [A, a];
- 时间戳 5、6 的 B、b 同理;
- 超过窗口范围的旧事件会被清除,不再参与匹配。
这种连接方式在实时分析中很常用,比如:
- 实时匹配用户点击流与广告展示流;
- 关联交易事件与支付确认事件。
总结(Summary)
-
数据流查询(Data Stream Querying)
- 查询是持续执行的(continuous querying);
- 困难在于:流是无界的(unbounded),可能乱序(out-of-order)。
-
窗口机制(Windowing)
- 是从无限流中提取有限关系的机制;
- 为聚合、连接等操作提供上下文边界。
-
系统必须应对的挑战
- 数据速率可能非常高、不稳定(variable/bursty);
- 数据可能不可预测(unpredictable),时间乱序,延迟到达。
流处理工具(Tools for Stream Processing)
1. 概念回顾
Kafka 或 MQTT 自身 不是流处理引擎,
它们只是消息传递(Pub/Sub)和数据存储系统。
真正的“流处理”是由 计算引擎(Stream Processor) 完成的。
最常见的流处理引擎是:
- Apache Spark Streaming
- Apache Flink
数据流处理架构(Data Stream Processing)
Flink 示例
- Kafka 提供数据流的持久化和分发;
- Flink 负责计算与一致性;
- 输出结果可以写入 HDFS、Elasticsearch 或前端可视化。
Spark 示例
- Spark Streaming 从 Kafka、Flume、HDFS、Kinesis、Twitter 等读取数据;
- 执行实时计算;
- 输出结果到 HDFS、数据库或仪表盘(Dashboards)。
使用 Apache Spark Streaming 的流处理(Data Stream Processing with Apache Spark Streaming)
-
Spark Streaming 是 Spark RDD API 的扩展
- RDD(Resilient Distributed Dataset)是一种“弹性分布式数据集”。
-
输入数据流被划分为多个 微批次(micro batches),
Spark Engine 以批的方式处理这些小块。 -
支持多种窗口:固定窗口、滑动窗口、翻转窗口。
-
高层抽象:
- Spark Streaming 中的流称为 DStream(Discretized Stream);
- 实际上是由一系列按时间划分的 RDD 组成。
数据流经过的路径:
输入数据流 → Spark Streaming → Spark Engine → 处理后的输出流
这是一种“准实时(near real-time)”架构:
虽然不是逐条处理(像 Flink 那样是真正流式),但微批次的延迟通常很低,适用于大部分实时分析场景。
示例:Spark 实现流式 WordCount(Example: Data Stream WordCount with Spark)
这是一个使用 Spark Streaming 实现的实时单词计数示例。
代码逻辑说明:
from pyspark import SparkContext
from pyspark.streaming import StreamingContext# 创建本地 StreamingContext(两线程,每 1 秒一个批次)
sc = SparkContext("local[2]", "NetworkWordCount")
ssc = StreamingContext(sc, 1)# 从指定主机端口读取实时流(如 localhost:9999)
lines = ssc.socketTextStream("localhost", 9999)# 对每行进行分词
words = lines.flatMap(lambda line: line.split(" "))# 统计每个词出现的次数
pairs = words.map(lambda word: (word, 1))
wordCounts = pairs.reduceByKey(lambda x, y: x + y)# 打印每个批次的前几条结果
wordCounts.pprint()# 启动计算
ssc.start()# 等待任务终止
ssc.awaitTermination()
流程说明:
-
设定计算任务(setup computation)
- 初始化 Spark Streaming 环境;
- 连接网络输入流;
- 定义处理逻辑(分词 → 映射 → 聚合)。
-
执行计算任务(execute computation)
- 启动流处理;
- 程序持续运行,直到手动停止。
示例输出说明(Example: Data Stream WordCount with Spark (output))
DStream(离散化流)由一系列按时间分割的 RDD 组成,每个 RDD 代表 1 秒的数据片段:
lines DStream:存储从 socket 读取的文本;words DStream:经过flatMap分词后的单词流;- 每个时间段(time 0→1、1→2、2→3 等)都会生成新的 RDD。
输出示例:
终端 1(数据源)
$ nc -lk 9999
hello world
终端 2(运行 Spark 程序)
$ ./bin/spark-submit examples/src/main/python/streaming/network_wordcount.py localhost 9999
...
Time: 2014-10-14 15:25:21
-------------------------
(hello,1)
(world,1)
每一秒钟,Spark 会收集输入流的内容并输出每个词的计数。
这种方式属于 微批次(micro-batch)流处理:输入数据流按短时间片分批计算。
使用 Apache Flink 进行流数据处理(Data Stream Processing with Apache Flink)
Apache Flink 更贴近“真正的流式计算”,因为它在整个系统中都采用**流水线(pipelining)**机制。
主要特征:
-
Flink 的每个操作符都会立即将数据传递给下一个操作符;
-
数据一到达就被处理,而不是等到批次结束;
-
这使得 Flink 实现毫秒级低延迟处理;
-
与 Spark 的关键区别:
- Spark 的处理阶段是分开的(stage by stage);
- Flink 的阶段是重叠的(overlapping),属于连续数据流模式。
Flink 的结构包括:
- DataStream API(用于流处理);
- DataSet API(用于批处理);
- 底层运行时 Runtime 是一个分布式流式数据流系统;
- 可在单机、本地集群或云平台上运行。
示例:Flink 窗口聚合(Example: Window Aggregation with Flink)
目标: 每 10 秒计算一次股票价格的平均值、最高值、最低值。
数据流结构:
输入流:Stock Stream
窗口:10 秒滚动窗口,每 5 秒滑动一次
输出:MinBy Price、MaxBy Price、Mean Price
代码逻辑:
// 定义时间窗口
WindowedDataStream<StockPrice> windowedStream = stockStream.window(Time.of(10, TimeUnit.SECONDS)).every(Time.of(5, TimeUnit.SECONDS));// 计算窗口内统计量
DataStream<StockPrice> lows = windowedStream.groupBy("symbol").minBy("price");DataStream<StockPrice> highs = windowedStream.groupBy("symbol").maxBy("price");DataStream<StockPrice> means = windowedStream.groupBy("symbol").mapWindow(new WindowMean()).flatten();
自定义函数 WindowMean:
public final class WindowMean implements WindowMapFunction<StockPrice, StockPrice> {public void mapWindow(Iterable<StockPrice> values, Collector<StockPrice> out) {double sum = 0;int count = 0;String symbol = "";for (StockPrice sp : values) {sum += sp.price;symbol = sp.symbol;count++;}out.collect(new StockPrice(symbol, sum / count));}
}
该示例展示了如何通过窗口机制在实时流上计算统计指标。
Apache Flink 支持的三种时间概念(Apache Flink Supports Three Notions of Time)
Flink 完全支持三种时间语义:
- 事件时间(Event Time):事件实际发生的时间;
- 摄取时间(Ingestion Time):事件进入 Flink 数据源的时间;
- 处理时间(Processing Time):Flink 操作符处理该事件的系统时间。
流程图:
- 数据由事件生产者产生;
- 经过消息队列(如 Kafka)的不同分区;
- 被 Flink Data Source 接收;
- 进入窗口算子(Window Operator);
- 每个阶段都可能基于不同时间语义来驱动计算。
Flink 示例:时间定义(Flink Example: Time Definition)
在 Flink 中,可以灵活定义流处理使用的时间语义:
final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment();// 使用处理时间(默认)
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);// 也可以选择:
// env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);DataStream<MyEvent> stream = env.addSource(new FlinkKafkaConsumer09<>(topic, schema, props));stream.keyBy(event -> event.getUser()).timeWindow(Time.hours(1)).reduce((a, b) -> a.add(b)).addSink(...);
含义:
- 可以根据需要设定时间类型;
- 以事件时间为基准可获得精确的逻辑窗口;
- 以处理时间为基准可获得最快的实时响应;
- 以摄取时间为中间折中方案。
Spark 与 Flink 的差异(Differences between Spark and Flink)
| 特性 | Apache Spark | Apache Flink |
|---|---|---|
| 原理 | 基于阶段的集合式数据转换 | 基于流水线的连续数据流 |
| 数据抽象 | RDD | DataSet |
| 处理阶段 | 各阶段分离 | 阶段重叠 |
| 优化器 | SparkSQL 模块 | 集成于 API 内部 |
| 批处理 | 使用 RDD | 使用 DataSet |
| 流处理 | 微批次(micro-batching) | 真正的流式处理(pipelining, DataStream API) |
总结:
- Spark 更偏向批处理思维;
- Flink 更偏向低延迟流计算;
- Spark 适合复杂批量分析;
- Flink 适合实时计算与持续数据流。
总结(Summary)
数据流处理的基本原理(Data Stream Processing Principles)
- 包括窗口聚合(window aggregation)和多种时间语义;
- 核心在于:如何在无限流中提取有限片段做分析。
流处理架构(Stream Processing Architectures)
-
基于消息的发布/订阅系统:Apache Kafka、MQTT;
-
流处理引擎:
- Apache Spark:通过 Spark Streaming 实现流式处理(基于 RDD 微批次);
- Apache Flink:真正的实时流处理框架,支持流水线并行和低延迟。
二者互补:
- Spark 更适合批+流一体场景;
- Flink 更适合实时、高吞吐、低延迟的连续数据流应用。
