![]()
下午2点03分,闪购开始。仓库操作员录入订单,输错数量,立刻修正。两个事件,一个事实,间隔30秒。凌晨2点跑的批处理作业会同时看到这两条记录,它不知道哪个是对的。如果修正恰好落在下一个批次窗口,早上的报表就错了,干净利落地错,没有任何技术报错。
这是数据团队的真实噩梦。处理事件按到达顺序、保留时间上下文,彻底改写了这个问题的解法。这就是本文项目的起点:基于Olist电商数据集,用Apache Flink 2.0、Kafka和Iceberg搭建的端到端流处理管道。
为什么选Flink 2.0,而不是Kafka Streams
Olist是公开的巴西电商数据集,包含订单、商品、卖家、客户、评价,两年10万单。作者之前用同一数据集搭过批处理湖仓,这次走向另一个极端:流处理、1分钟计算窗口、秒级异常检测。
Java生态里做流处理不止Flink一个选择。Kafka Streams原生集成、部署简单;ksqlDB甚至不用写代码。但Flink胜在两个地方。
一是CEP(复杂事件处理)。检测"同一客户5分钟内下3单"不是聚合,是事件间的时间关联。Flink CEP用模式DSL原生支持,Kafka Streams得手写状态管理和时间逻辑。二是版本。Flink 2.0带来原生Java 21支持,作者刻意选当前版本而非生命周期结束的旧版。
管道架构:三个独立作业,一个共享数据源
数据流从Olist CSV出发,经模拟器进入Kafka的orders主题。然后分叉三路:
RevenueAggregationJob,1分钟滚动窗口聚合营收;AnomalyDetectionJob,5分钟CEP窗口检测异常模式;RealtimeKpiJob,1分钟全局窗口计算实时KPI。三路人马各自落入Kafka的不同主题,最终汇入Iceberg数据湖(MinIO存储)。
作业独立是刻意设计。生产环境里,你需要能重启异常检测作业而不影响营收聚合。每个作业有自己的检查点、自己的状态、自己的拓扑。
窗口类型选错,数据就错
![]()
作者花了相当篇幅解释窗口选择的代价。滚动窗口(tumbling window)不重叠,适合"每分钟营收"这种明确切分。滑动窗口(sliding window)有重叠,适合"过去5分钟均值"这种平滑需求。会话窗口(session window)按活动间隙动态切分,适合用户行为分析。
选错窗口类型,结果会 quietly wrong(静默错误)——不是报错,是答案不对。比如用滚动窗口算"过去5分钟",你会错过跨窗口的事件关联。
CEP模式匹配是另一个易错点。"3单5分钟"的检测,Flink用Pattern.begin("first").next("second").next("third").within(Time.minutes(5))描述。时间语义选event time还是processing time,决定了迟到事件怎么处理。作者选了event time,因为业务关心的是订单实际发生时间,而非系统处理时间。
状态后端:RocksDB不是默认选项,是必要选项
三个作业都维护状态。RevenueAggregation要存1分钟窗口的聚合值,CEP要存待匹配的事件序列,KPI要存全局计数。内存状态后端(HashMapStateBackend)在数据量大时会OOM,RocksDBStateBackend把状态刷到磁盘,支持TB级状态。
代价是序列化开销。作者提到,RocksDB的读写比内存慢一个数量级,但生产环境没得选。检查点间隔设成10秒,权衡恢复速度和存储压力。
Kafka分区:并行度的隐形天花板
一个常被忽略的事实:Flink作业的并行度上限等于Kafka主题的分区数。orders主题有12个分区,RevenueAggregationJob的并行度最多12。强行设更高,多余subtask只会空转。
作者用keyBy(orderId)保证同一订单的事件进入同一分区,这是状态一致性的前提。如果key选择不当,比如用随机UUID,同一用户的订单会散到不同分区,CEP模式就永远匹配不上。
分区倾斜是另一个坑。某些卖家订单量极大,对应分区成为热点。作者没有展开解法,但生产环境通常需要重新分区(rebalance)或自定义分区器。
Iceberg集成:流批统一的存储层
![]()
三个作业的输出落入Kafka后,用Flink SQL的INSERT INTO iceberg_catalog.db.table SELECT ... 写入Iceberg。Iceberg的隐藏分区(hidden partitioning)让时间戳列自动按小时分区,查询时不用手动指定分区键。
版本2的Iceberg格式支持行级删除,这对CDC(变更数据捕获)场景很重要。作者提到,Olist数据集没有更新操作,但生产环境的订单状态变更(待发货→已发货→已签收)需要这个能力。
MinIO作为S3兼容存储,跑在本地Docker里。作者特意提到,生产环境会换真正的S3或GCS,但API完全兼容,迁移成本只在配置。
Flink 2.0的新特性:Java 21和检查点改进
版本升级不只是数字变化。Java 21的虚拟线程(Virtual Threads)让I/O密集型操作更轻量,RocksDB的异步检查点利用了这一点。检查点间隔从1.17版本的默认10分钟降到可配置的秒级,作者设成10秒,意味着故障时最多丢10秒数据。
增量检查点(incremental checkpoint)只存状态变更,把检查点大小从GB级降到MB级。这对CEP作业尤其重要,它的状态是事件序列,随时间线性增长。
作者没有展开但值得注意:Flink 2.0的检查点格式向前兼容,但状态后端不兼容。从1.17升级需要迁移状态,或者接受冷启动。
实测数字:延迟与吞吐
作者在本地Docker环境跑完整管道,给出几个实测数字。端到端延迟(事件产生到Iceberg可查)约3-5秒,其中Kafka-Flink占1-2秒,Flink-Iceberg占2-3秒。吞吐方面,单并行度能处理约5000事件/秒,12并行度下摸到6万/秒,CPU成为瓶颈。
这些数字是MacBook Pro M3 Pro的本地结果,生产环境的网络延迟和磁盘I/O会改写它们。但比例关系有参考价值:CEP作业因为状态复杂,吞吐只有聚合作业的1/3。
异常检测的误报率作者没有量化,这是CEP的通病——模式太松漏掉真异常,太紧全是假警报。调参没有通用公式,得对着业务数据反复试。
项目代码开源在GitHub,作者留了TODO:用Flink的Side Output把迟到事件(late events)单独导出,而不是直接丢弃。这是数据质量审计的常见做法,但代码里还没实现。
如果让你选,凌晨2点的批处理报表和下午2点的实时看板,哪个数字更敢拿来做业务决策?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.