2、核心概念:事件时间 vs 处理时间、水位线(Watermark)、窗口类型(滚动、滑动、会话)

好,咱们进入第二章。这一章是整个课程的基石,说白了,你后面遇到的所有「实时离线口径不一致」的问题,根源几乎都能在这里找到。

我见过太多团队,上线前一天才发现实时报表和离线报表对不上。一查,发现实时用的是处理时间,离线用的是事件时间。那能对上才怪。所以这一章,咱们把这三个核心概念彻底掰扯清楚。

2.1 事件时间 vs 处理时间

这两个概念,是实时计算里最基础、也最容易搞混的东西。

  • 事件时间(Event Time):数据真实发生的时间。比如用户点击按钮的那一刻,手机上报的时间戳。
  • 处理时间(Processing Time):数据到达计算引擎的时间。比如 Flink 任务所在机器收到这条数据的系统时间。

你想想看,如果网络延迟、数据重传、或者客户端离线缓存,事件时间和处理时间就会差很多。离线数仓里,我们几乎只用事件时间,因为那是「事实」。但实时计算里,很多人图省事,直接用处理时间。

核心原则:统一口径,必须统一使用事件时间。处理时间只适合对精度要求极低的场景(比如实时大屏的粗略趋势)。

我在项目中遇到过,有个业务方坚持用处理时间做实时 UV,结果凌晨流量高峰时,数据因为网络抖动延迟了 30 秒,实时 UV 比离线 UV 少了 15%。后来改成事件时间,加上合理的乱序容忍,才对齐。

维度 事件时间 处理时间
定义 数据产生的时间 数据到达引擎的时间
确定性 确定,不会变 不确定,依赖系统负载
离线对齐 天然一致 几乎无法对齐
适用场景 所有需要精确统计的场景 实时监控、简单聚合

2.2 水位线(Watermark)

好,既然要用事件时间,那问题来了:数据可能迟到,你总不能永远等下去吧?

水位线就是用来解决这个问题的。它本质上是一个「时间进度标记」,告诉引擎:在这个时间戳之前的数据,我已经基本收齐了,你可以触发计算了。

嗯,这里要注意。水位线不是真实时间,而是基于事件时间推断出来的一个「逻辑时钟」。比如你设置水位线延迟为 10 秒,那么当引擎收到一条事件时间为 12:00:15 的数据时,它会认为水位线推进到了 12:00:05。也就是说,12:00:05 之前的数据,引擎认为不会再来了。

我的习惯:水位线延迟不要拍脑袋设。我会先分析离线数据中事件时间的延迟分布,取 P95 或 P99 的延迟值作为初始值。上线后再根据实际迟到数据量微调。

我曾经踩过一个坑。有个任务水位线设了 5 分钟,结果业务方反馈数据一直不出。查了半天,发现上游 Kafka 某个分区挂了,数据积压了 2 小时。水位线一直卡在 5 分钟前,窗口永远不触发。后来加了「空闲分区检测」和「周期性推进水位线」才解决。

避坑指南:我曾经以为水位线设得越大越好,结果窗口延迟严重,实时报表变成「准实时」。后来我总结:水位线是「精度」和「延迟」的 trade-off。设小了,迟到数据多,口径不准;设大了,实时性差。建议先小后大,逐步调优。

2.3 窗口类型

有了事件时间和水位线,我们还需要一个东西来「切分」数据流。这就是窗口。

实时计算里,窗口类型主要有三种:滚动、滑动、会话。离线数仓里其实也有类似概念,但实现方式完全不同。

2.3.1 滚动窗口(Tumbling Window)

最简单的一种。固定大小,互不重叠。比如每 5 分钟一个窗口,0-5 分、5-10 分……

离线里对应的就是按天、按小时分区。所以滚动窗口最容易对齐离线口径。

-- 统一SQL示例:滚动窗口
SELECT
  TUMBLE_START(event_time, INTERVAL '5' MINUTE) AS window_start,
  TUMBLE_END(event_time, INTERVAL '5' MINUTE) AS window_end,
  COUNT(*) AS pv
FROM user_clicks
GROUP BY TUMBLE(event_time, INTERVAL '5' MINUTE);

2.3.2 滑动窗口(Sliding Window)

固定大小,但可以重叠。比如窗口大小 10 分钟,滑动步长 5 分钟。这样每 5 分钟输出一次过去 10 分钟的数据。

这个在离线里没有直接对应物。离线要做滑动窗口,通常是用自关联或者窗口函数,性能很差。所以实时用滑动窗口时,离线口径往往需要单独处理。

注意:滑动窗口的「重叠」特性会导致数据被重复计算。比如一条数据可能同时属于多个窗口。离线对齐时,必须保证离线也采用同样的滑动逻辑,否则口径必差。

2.3.3 会话窗口(Session Window)

这个最特殊。它不是固定大小,而是根据数据的「活跃间隙」来切分。比如用户连续点击,如果超过 30 分钟没有新点击,就认为一次会话结束。

离线里做会话,通常是用「相邻事件时间差 > 阈值」来标记。实时里则是用会话窗口函数。

-- 统一SQL示例:会话窗口
SELECT
  SESSION_START(event_time, INTERVAL '30' MINUTE) AS session_start,
  SESSION_END(event_time, INTERVAL '30' MINUTE) AS session_end,
  userId,
  COUNT(*) AS click_count
FROM user_clicks
GROUP BY SESSION(event_time, INTERVAL '30' MINUTE), userId;

我个人觉得,会话窗口是实时离线口径差异最大的地方。因为离线会话的「超时时间」往往是从业务角度定义的,而实时会话的「超时时间」还受数据延迟影响。我曾经调一个会话窗口任务,离线口径是 30 分钟超时,实时也设 30 分钟,结果因为网络延迟,实时会话数比离线多了 8%。后来把实时超时时间改成 35 分钟才对齐。

2.4 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个「决策地图」:

实时离线口径统一核心概念图 实时离线口径统一 · 核心概念决策图 统一口径基础 事件时间 处理时间 水位线 滚动窗口 滑动窗口 会话窗口 离线:按事件时间分区 实时:水位线 + 窗口 统一SQL桥接 目标:实时离线口径一致 说明:统一口径必须统一使用事件时间 + 水位线 + 窗口,缺一不可。处理时间仅用于辅助监控。

这张图你看懂了吗?从上到下,就是一条「决策链」:先选时间语义,再配水位线,然后选窗口类型,最后用统一SQL桥接实时和离线。每一步选错,口径就偏了。

2.5 小结

这一章的内容,我建议你反复看。因为后面每一章都会用到这些概念。尤其是事件时间和水位线,它们是整个统一SQL体系的「锚点」。锚点稳了,后面的路才好走。