【数据工程】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)并不反映事件流的真实“逻辑时间”。

  • 关键挑战:

    1. 乱序事件(Out-of-Order Events):事件到达顺序与事件时间不符;
    2. 延迟数据(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 的事件时间处理逻辑通过时间戳和水位线一起工作:

  1. 事件进入流中,并带有自己的事件时间戳;

  2. Watermark 策略(WatermarkStrategy) 会定期生成水位线;

    • 可以是固定周期;
    • 或在检测到某些“标点符号(punctuation)”时触发;
    • 或基于“允许最大延迟(allowed lateness)”计算。
  3. 晚到事件(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()

流程说明:

  1. 设定计算任务(setup computation)

    • 初始化 Spark Streaming 环境;
    • 连接网络输入流;
    • 定义处理逻辑(分词 → 映射 → 聚合)。
  2. 执行计算任务(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 PriceMaxBy PriceMean 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 完全支持三种时间语义:

  1. 事件时间(Event Time):事件实际发生的时间;
  2. 摄取时间(Ingestion Time):事件进入 Flink 数据源的时间;
  3. 处理时间(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 SparkApache Flink
原理基于阶段的集合式数据转换基于流水线的连续数据流
数据抽象RDDDataSet
处理阶段各阶段分离阶段重叠
优化器SparkSQL 模块集成于 API 内部
批处理使用 RDD使用 DataSet
流处理微批次(micro-batching)真正的流式处理(pipelining, DataStream API)

总结:

  • Spark 更偏向批处理思维;
  • Flink 更偏向低延迟流计算;
  • Spark 适合复杂批量分析;
  • Flink 适合实时计算与持续数据流。

总结(Summary)

数据流处理的基本原理(Data Stream Processing Principles)

  • 包括窗口聚合(window aggregation)和多种时间语义;
  • 核心在于:如何在无限流中提取有限片段做分析。

流处理架构(Stream Processing Architectures)

  • 基于消息的发布/订阅系统:Apache KafkaMQTT

  • 流处理引擎:

    • Apache Spark:通过 Spark Streaming 实现流式处理(基于 RDD 微批次);
    • Apache Flink:真正的实时流处理框架,支持流水线并行和低延迟。

二者互补:

  • Spark 更适合批+流一体场景;
  • Flink 更适合实时、高吞吐、低延迟的连续数据流应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值