Data Engineering Zoomcamp 2027 第 7 周作业实战:用 Redpanda 与 PyFlink 构建绿色出租车实时流处理管道
【免费下载链接】data-engineering-zoomcampData Engineering Zoomcamp is a free 9-week course on building production-ready data pipelines. Join the course here 👇🏼项目地址: https://gitcode.com/GitHub_Trending/da/data-engineering-zoomcamp
本篇指南围绕 Data Engineering Zoomcamp 2027 年 cohort 第 7 模块(流处理)的作业展开,核心任务是以 Redpanda(Kafka 兼容消息中间件)为消息枢纽、以 PyFlink 为流处理引擎,对 2025 年 10 月的纽约绿色出租车(Green Taxi)行程数据完成"生产 → 消费 → 窗口聚合"的完整实时链路演练。读完本文,你将掌握 Kafka Producer/Consumer 的 Python 写法、Flink SQL 中 tumbling/session 窗口与 watermark 的实战用法,并能在本地 Docker 环境中独立复现并验证全部 6 道作业题。
作业概览:一条完整的实时流处理链路
本作业建立在 07 模块 workshop 的基础设施之上,目标是复用同一套 Docker 编排,把 workshop 中基于黄色出租车(yellow taxi、tpep_前缀字段、ridestopic)的示例,迁移到绿色出租车(green taxi、lpep_前缀字段、green-tripstopic)数据上,从而检验三个核心技能点:
- 消息生产与消费:用
kafka-python库把 parquet 数据写入 Redpanda topic,再以消费者身份读回并做简单过滤统计; - 事件时间与窗口:在 PyFlink 中以字符串形式的时间戳声明事件时间,用 5 秒 watermark 容忍乱序,分别实现 tumbling(滚动)与 session(会话)窗口聚合;
- 结果落库:通过 Flink JDBC 连接器把聚合结果写入 PostgreSQL 并查询验证。
数据来源为纽约 TLC 公开的绿色出租车行程快照文件green_tripdata_2025-10.parquet(2025 年 10 月整月数据),作业环境由 07-streaming/workshop 目录下的 Docker Compose 提供,作业中引用的完整代码镜像位于 cohorts/2027/07-streaming/code。
环境准备:复用 workshop 的流处理基础设施
作业直接复用 workshop 的环境,无需从零搭建。在仓库根目录下进入 workshop 目录并启动全部服务:
cd 07-streaming/workshop/ docker compose build docker compose up -dbuild会基于 Dockerfile.flink 构建带 Python 与 PyFlink 的自定义 Flink 镜像(起点是官方flink:2.2.0-scala_2.12-java17,内置 Python 3.12、uv 以及 Kafka / JDBC / PostgreSQL 连接器 JAR)。启动完成后,你会得到 docker-compose.yml 中声明的四个服务:
| 服务 | 暴露地址 | 用途 |
|---|---|---|
| Redpanda | localhost:9092 | Kafka 兼容消息中间件,承接 producer 写入与 Flink/consumer 读取 |
| Flink JobManager | http://localhost:8081 | 作业协调者,提供 Web UI 用于提交、监控与取消作业 |
| Flink TaskManager | 集群内部 | 执行数据处理的 Worker,默认 15 个任务槽、默认并行度 3 |
| PostgreSQL | localhost:5432(postgres/postgres) | 结果落库,供 SQL 查询验证 |
如果你此前运行过 workshop 并残留了旧容器或数据卷,务必做一次彻底的重置,避免 topic 里的旧消息污染统计结果:
docker compose down -v docker compose build docker compose up -d注意:容器名(如
workshop-redpanda-1)假定目录名是workshop。若你重命名了目录,后续所有docker exec命令中的容器名都要相应调整。
数据准备:Green Taxi 与字段筛选
作业数据来自 NYC TLC 的绿色出租车行程文件(2025 年 10 月),读取 parquet 后只需保留以下 8 个字段:
lpep_pickup_datetime—— 上车时间(datetime,需转字符串)lpep_dropoff_datetime—— 下车时间(datetime,需转字符串)PULocationID—— 上车出租车区域 IDDOLocationID—— 下车出租车区域 IDpassenger_count—— 乘客数trip_distance—— 行程距离(英里)tip_amount—— 小费金额total_amount—— 总金额
对比 workshop 使用的 producer.py(只取 5 列、时间戳转 epoch 毫秒),本作业字段更多、且要求把 datetime 列序列化为字符串,以便 Flink 用TO_TIMESTAMP(..., 'yyyy-MM-dd HH:mm:ss')解析——这是本作业与 workshop 最关键的差异之一。
Question 1:确认 Redpanda 版本
Redpanda 自带的 CLI 工具rpk用于管理 broker、topic 与消费组。在 Redpanda 容器内执行:
docker exec -it workshop-redpanda-1 rpk version输出会给出当前运行的 Redpanda 版本号。这道题的目的是确认环境就绪,同时熟悉rpk这个后续创建/删除 topic 都要用到的工具。
Question 2:Producer——把数据写入 green-trips
先创建作业专用 topic(workshop 中的ridestopic 由 broker 首次使用自动创建,这里显式创建更稳妥):
docker exec -it workshop-redpanda-1 rpk topic create green-trips随后编写 producer:读取 parquet → 筛选 8 列 → 每行转成 dict → 以 JSON 格式发送到green-trips。关键点是把两个 datetime 列先转成'%Y-%m-%d %H:%M:%S'格式字符串,否则 JSON 序列化会失败或产生 Flink 无法解析的格式。整体骨架参照 workshop 的 producer.py,改造如下:
import json import pandas as pd from time import time from kafka import KafkaProducer columns = [ 'lpep_pickup_datetime', 'lpep_dropoff_datetime', 'PULocationID', 'DOLocationID', 'passenger_count', 'trip_distance', 'tip_amount', 'total_amount', ] df = pd.read_parquet('green_tripdata_2025-10.parquet', columns=columns) # datetime -> 字符串,与 Flink 的 'yyyy-MM-dd HH:mm:ss' 格式对齐 df['lpep_pickup_datetime'] = df['lpep_pickup_datetime'].dt.strftime('%Y-%m-%d %H:%M:%S') df['lpep_dropoff_datetime'] = df['lpep_dropoff_datetime'].dt.strftime('%Y-%m-%d %H:%M:%S') def json_serializer(data): return json.dumps(data).encode('utf-8') producer = KafkaProducer( bootstrap_servers=['localhost:9092'], value_serializer=json_serializer, ) t0 = time() for _, row in df.iterrows(): producer.send('green-trips', value=row.to_dict()) producer.flush() t1 = time() print(f'took {(t1 - t0):.2f} seconds')workshop 里发送 1000 行大约耗时 10 秒(producer.py 中可见time.sleep(0.01)的节流逻辑);本次是整月数据、全量发送,用时量级取决于网络与机器。作业给出的候选答案是 10 / 60 / 120 / 300 秒,用于判断你测得的时间落在哪个档位。
Question 3:Consumer——统计 trip_distance > 5 的行程数
写一个 Kafka 消费者读回全部消息。必须设置auto_offset_reset='earliest',否则新消费者默认从latest开始、只会看到订阅之后的新消息,读不到已经写入的历史数据。group_id用于让 Kafka 记录该消费组的读取进度。参考 consumer.py 的写法:
import json from kafka import KafkaConsumer def json_deserializer(data): return json.loads(data.decode('utf-8')) consumer = KafkaConsumer( 'green-trips', bootstrap_servers=['localhost:9092'], auto_offset_reset='earliest', group_id='green-trips-consumer', value_deserializer=json_deserializer, ) count = 0 for message in consumer: if message.value['trip_distance'] > 5.0: count += 1 print(f'trips with trip_distance > 5: {count}')作业的候选答案是 6506 / 7506 / 8506 / 9506,取最接近你实际统计结果的选项。注意trip_distance在原始数据中为英里,这里直接按数值比较,不涉及单位换算。
Part 2 预备:PyFlink 作业的三个关键差异
进入 PyFlink 部分前,先明确与 workshop 代码(aggregation_job.py)的三处不同:
- topic 名:
green-trips(替代 workshop 的rides); - 字段前缀:datetime 列是
lpep_前缀(替代tpep_); - 时间戳形态:本作业的时间是字符串(如
2025-10-01 00:00:00),而 workshop 中是 epoch 毫秒整数。因此源表 DDL 中要先用计算列把字符串转成 Flink 时间戳,并声明 watermark:
lpep_pickup_datetime VARCHAR, event_timestamp AS TO_TIMESTAMP(lpep_pickup_datetime, 'yyyy-MM-dd HH:mm:ss'), WATERMARK FOR event_timestamp AS event_timestamp - INTERVAL '5' SECONDTO_TIMESTAMP按给定格式解析字符串;watermark 取"最新事件时间 − 5 秒",作为窗口发布结果的触发器,容忍最多 5 秒的乱序到达(workshop 的 README 对此有完整图解:07-streaming/workshop/README.md)。
运行 Flink 作业前还需注意以下几点:
- 结果表先行:在 PostgreSQL 中预先创建好各题的结果表(用
docker compose exec postgres psql -U postgres -d postgres或任意 SQL 客户端连接localhost:5432); - 作业文件位置:把你的
.py作业文件放在workshop/src/job/下,该目录被挂载进 Flink 容器,容器内路径为/opt/src/job/; - 提交命令:
docker exec -it workshop-jobmanager-1 flink run -py /opt/src/job/your_job.py- 并行度必须设为 1:
green-trips只有 1 个分区,因此作业里要env.set_parallelism(1)。若并行度过高,没有分配到数据的分区子任务(idle consumer subtask)不会消费消息,会导致 watermark 无法推进、窗口迟迟不发布结果; - 流式作业常驻运行:Flink 流作业不会自行结束,让作业跑 1~2 分钟、看到 PostgreSQL 里出现结果后,可在 http://localhost:8081 的 Flink UI 上取消作业;
- 重复数据处理:如果 producer 跑过多次,topic 里会有重复消息。想彻底清空可删除并重建 topic:
docker exec -it workshop-redpanda-1 rpk topic delete green-tripsQuestion 4:5 分钟 Tumbling Window 统计热门接客区
需求:统计每个PULocationID在每个 5 分钟滚动窗口内的行程数。先建结果表(包含window_start、PULocationID、num_trips三列),并建议像 workshop 的聚合表那样加上主键,以启用 upsert 语义、让迟到数据能修正已发布的结果(参见 aggregation_job.py 中PRIMARY KEY (...) NOT ENFORCED的用法):
CREATE TABLE trips_by_pulocation ( window_start TIMESTAMP, PULocationID INTEGER, num_trips BIGINT, PRIMARY KEY (window_start, PULocationID) );Flink 作业的核心 SQL 用TUMBLE函数:第一个参数是源表,第二个参数DESCRIPTOR(event_timestamp)必须指向定义了 watermark 的那一列,第三个参数是窗口大小:
INSERT INTO trips_by_pulocation SELECT window_start, PULocationID, COUNT(*) AS num_trips FROM TABLE( TUMBLE(TABLE green_trips_source, DESCRIPTOR(event_timestamp), INTERVAL '5' MINUTE) ) GROUP BY window_start, PULocationID;处理完所有数据后查询:
SELECT PULocationID, num_trips FROM trips_by_pulocation ORDER BY num_trips DESC LIMIT 3;取num_trips最大的PULocationID,候选答案是 42 / 74 / 75 / 166。workshop 用 1 小时窗口做了同样的聚合演示,只是多加了SUM(total_amount)与env.set_parallelism(3)(见 aggregation_job.py),你可以对照该文件理解 TUMBLE 的完整调用形态。
Question 5:Session Window 找最长会话
需求:以lpep_pickup_datetime作为事件时间、5 秒 watermark 容忍,按PULocationID分组做会话窗口(gap 为 5 分钟)。会话窗口与 tumbling 的本质区别在于:窗口大小不固定,事件与上一事件间隔不超过 5 分钟就并入同一会话,一旦出现超过 5 分钟的空档,当前会话关闭。
|--events--| gap(>5min) |--events------| gap(>5min) |--events--| | Session 1| | Session 2 | | Session 3|Flink SQL 中会话窗口写作SESSION(...),第一个参数仍是要按事件时间开窗的表:
INSERT INTO longest_sessions SELECT PULocationID, COUNT(*) AS num_trips FROM TABLE( SESSION(TABLE green_trips_source, DESCRIPTOR(event_timestamp), INTERVAL '5' MINUTE) ) GROUP BY PULocationID, window_start, window_end;结果表自行定义(至少包含PULocationID与num_trips),然后找到单次会话内行程数最多的PULocationID,即"最长会话"。候选答案是 12 / 31 / 51 / 81。workshop README 的"Understanding window types"一节对比了 tumbling / sliding / session 三种窗口的适用场景,会话窗口常用于用户行为归因(07-streaming/workshop/README.md),这里把它用在出租车接客上,语义同样成立。
Question 6:1 小时 Tumbling Window 统计小费高峰
需求:用1 小时 tumbling 窗口聚合全量区域每小时的小费总额SUM(tip_amount),找出小费最高的那个小时。先建表:
CREATE TABLE hourly_tip_amounts ( window_start TIMESTAMP, total_tip_amount DOUBLE PRECISION );作业 SQL 与 Q4 同构,只是窗口变为 1 小时、聚合函数换成SUM、且不再按PULocationID分组(跨所有区域汇总):
INSERT INTO hourly_tip_amounts SELECT window_start, SUM(tip_amount) AS total_tip_amount FROM TABLE( TUMBLE(TABLE green_trips_source, DESCRIPTOR(event_timestamp), INTERVAL '1' HOUR) ) GROUP BY window_start;查询total_tip_amount最大的window_start,候选答案是2025-10-01 18:00:00/2025-10-16 18:00:00/2025-10-22 08:00:00/2025-10-30 16:00:00。因为这里的结果表没有主键、且每小时只输出一行,不会产生重复更新,可以不用 upsert;不过若担心同一窗口被重复发布,仍可像 Q4 那样加主键。
提交与检查清单
完成全部 6 题后,通过 cohort 提供的作业提交表单提交答案(homework.yaml中声明了提交表单配置与截止时间,见 cohorts/2027/07-streaming/homework.yaml)。提交前建议对照以下清单自检:
- Redpanda 版本号是否来自
rpk version的真实输出; - producer 发送耗时与题面档位一致,且
flush()已调用(未 flush 会导致部分消息滞留缓冲区、统计偏少); - consumer 统计用的是
earliest偏移,且只跑了一次(重复运行同一group_id会从上一次位置继续,导致计数不完整); - Flink 作业都设置了
env.set_parallelism(1),watermark 能正常推进,窗口结果已出现在 PostgreSQL 中再取消作业; - 若 producer 重复发送过数据,已用
rpk topic delete green-trips清空后重新发送。
与 workshop 源码的对照学习路径
本作业的所有技术细节都能在仓库的 workshop 源码中找到对应实现,建议按以下顺序对照研读:
- 07-streaming/workshop/docker-compose.yml:四个服务的完整编排,含 Redpanda 双监听地址、Flink 内存与并行度参数;
- 07-streaming/workshop/Dockerfile.flink:PyFlink 镜像构建过程与连接器 JAR 清单;
- 07-streaming/workshop/src/models.py:Ride 数据模型与序列化/反序列化函数,可据此改造成 green taxi 的 8 字段版本;
- 07-streaming/workshop/src/producers/producer.py 与 producer_realtime.py:批量与实时两种生产方式,后者还模拟了 ~20% 的 3~10 秒延迟事件,用于观察 watermark 与 upsert 行为;
- 07-streaming/workshop/src/consumers/consumer.py:
earliest偏移消费与反序列化; - 07-streaming/workshop/src/job/pass_through_job.py:最简 Kafka→JDBC 透传作业,含
latest-offset与TO_TIMESTAMP_LTZ的用法; - 07-streaming/workshop/src/job/aggregation_job.py:TUMBLE 窗口 + watermark + upsert 主键的完整聚合作业,是 Q4/Q6 的直接蓝本。
把 workshop 中ridestopic、tpep_字段、epoch 毫秒时间戳这三处替换成本作业要求的green-tripstopic、lpep_字段与字符串时间戳,再按上述要点调整并行度与结果表,即可系统性地完成全部作业——这也正是"以流处理思维迁移数据形态"的实战训练目标。
【免费下载链接】data-engineering-zoomcampData Engineering Zoomcamp is a free 9-week course on building production-ready data pipelines. Join the course here 👇🏼项目地址: https://gitcode.com/GitHub_Trending/da/data-engineering-zoomcamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考