基础及入门(2)-流数据处理机制

一、整体数据流转架构

二、源端数据读取:Kafka → Flink

1. 数据格式与反序列化

Flink 通过 'format' = 'json' 配置项告诉 Source 算子使用 JSON 反序列化器。每一条 Kafka 消息(一个 JSON 字节数组)进入 Flink 后,会经历如下过程:
Kafka 消息字节流
    ↓
JsonRowDeserializationSchema(内置 JSON 反序列化器)
    ↓
Flink 内部统一数据结构:RowData 对象
    ↓
字段映射:EVENT_NO / PATIENT_ID / CLINIC_DEPT_CODE / CLINIC_DOCTOR_CODE / REGISTER_TIME

这里有几个关键参数需要你深刻理解:
  • 'json.ignore-parse-errors' = 'true':当某条 JSON 消息格式异常(字段缺失、类型不匹配)时,不抛异常、直接跳过这条消息,返回 null 行。这是生产环境防止脏数据崩溃任务的重要保护。
  • 'scan.startup.mode' = 'group-offsets':从消费者组记录的上次提交偏移量继续消费,任务重启后不会重复消费也不会丢失。
  • 所有字段定义为 STRING:因为 Kafka 的 JSON 消息里 REGISTER_TIME 是字符串格式,Flink 在 Source 阶段不做类型转换,保留原始字符串,转换推迟到计算层处理。

2. 消费偏移量管理

Flink 内部有一个 Checkpoint 机制,每次做 Checkpoint 时会将 Kafka 当前消费的 offset 也存入状态后端。这意味着任务失败恢复后,能精确从上次 Checkpoint 的 offset 继续,保证 exactly-once 语义(配合 Doris 的幂等写入)。

三、Flink SQL 计算引擎内部处理机制

1. SQL 编译过程(你看不见但必须懂)

你写的 SQL 在提交时会经历:
SQL 文本
  → Parser(语法解析)→ AST 抽象语法树
  → Validator(语义校验)→ 检查字段是否存在、类型是否合法
  → Planner(优化器,基于 Calcite)→ 逻辑执行计划 → 物理执行计划
  → 生成算子 DAG 图(JobGraph)
  → 提交到 TaskManager 执行
你 SQL 最终被翻译成的算子链就是架构图里展示的:Source → Filter → Calc → GroupAggregate → Sink 这条 DAG。

2. 每个算子的具体行为

Source 算子:从 Kafka 拉取 JSON 消息,反序列化成 RowData(Flink 内部行格式),每条消息对应一个 RowData。
Filter 算子(对应你的 WHERE 子句):
WHERE REGISTER_TIME IS NOT NULL
  AND CLINIC_DEPT_CODE IS NOT NULL 
  AND CLINIC_DOCTOR_CODE IS NOT NULL
对每条流入的 RowData 做谓词判断,不满足条件的直接丢弃,不进入后续算子。这是一个无状态的轻量算子,在真正的聚合之前先过滤,能大幅减少进入 GroupAggregate 的数据量。
Calc 算子(对应 SELECT 里的表达式计算):
CAST(SUBSTRING(REGISTER_TIME, 1, 10) AS DATE) AS dim_date
这一步调用了两个内置标量函数
  • SUBSTRING(str, start, length):字符串截取,从 "2024-01-15 08:30:00" 截取 "2024-01-15"。
  • CAST(... AS DATE):显式类型转换,将字符串转为 Flink 内部的 DateData(实际存储为距离 epoch 的天数整数)。
是的,这些都是 Flink SQL 内置函数,不需要你自己实现,Flink 的内置函数体系非常完整,涵盖字符串、时间、数学、聚合、条件等各类操作。
GroupAggregate 算子(核心,有状态算子):
对应你的:
GROUP BY CAST(SUBSTRING(REGISTER_TIME,1,10) AS DATE), CLINIC_DEPT_CODE, CLINIC_DOCTOR_CODE
这是一个有状态的流式聚合,它的工作机制如下:
每来一条新数据(dim_date=2024-01-15, dept=001, doctor=D01)
  → 计算分组 Key 的哈希值
  → 查询 State Backend 中该 Key 的当前累加值(比如当前是 5)
  → 执行 SUM(1):5 + 1 = 6
  → 将新值 6 写回 State Backend
  → 向下游 Sink 发出两条消息:
      撤回消息(Retract):(-) [2024-01-15, 001, D01, 5]  ← 旧值无效
      插入消息(Accumulate):(+) [2024-01-15, 001, D01, 6]  ← 新值生效
这就是 Flink 流式聚合的 Retract(撤回)机制,每次聚合结果更新都会先撤回旧值、再插入新值。这也是为什么 Doris 端需要配置 'sink.properties.partial_update' = 'true' 的核心原因。
SUM(1) 的本质:这里 SUM(1) 等价于 COUNT(*),每来一条记录加 1,是对人次进行累计计数的标准写法。在 Flink 内部,SUM 是内置聚合函数,它维护一个累加器状态,不需要存储所有历史记录,只存储当前累计值,状态极小。

四、类型转换:为什么要显式 CAST,发生在哪个阶段

1. 为什么必须显式 CAST

源端所有字段都定义为 STRING。Doris 目标表的 dim_date 字段类型是 DATE。Flink SQL 的类型系统是强类型的,STRING 和 DATE 是两种完全不同的内部类型,不会自动隐式转换
如果你不写 CAST,Planner 在编译阶段(SQL 提交时)就会报类型不匹配错误,任务根本无法启动。

2. 类型转换发生在哪个阶段

提交 SQL 时(编译期):
  Validator 校验:发现 REGISTER_TIME 是 STRING,目标是 DATE
  → 检查是否有 CAST 表达式 → 有 → 校验通过

运行期(每条数据处理时):
  Calc 算子对每条 RowData 执行:
    1. SUBSTRING("2024-01-15 08:30:00", 1, 10) → "2024-01-15"
    2. CAST("2024-01-15" AS DATE)
       ↓
       内部:DateTimeUtils.parseDate("2024-01-15")
       ↓
       转为 int:距离 1970-01-01 的天数(如 19737)
       ↓
       存储为 Flink 内部 DateData 类型

类型转换的时序是:编译期做类型合法性校验,运行期每条数据实际执行转换逻辑。

3. 常见类型转换错误场景

当 REGISTER_TIME 值不是标准 "YYYY-MM-DD" 或 "YYYY-MM-DD HH:MM:SS" 格式时,CAST 会返回 NULL(不是抛异常,因为 Flink 默认对 CAST 失败返回 NULL)。这条记录虽然不会让任务崩溃,但 dim_date 会变成 NULL 被 WHERE 过滤掉或写入 NULL 值,是你排查数据质量问题时需要重点关注的点。

五、数据写入 Doris 的机制

1. Sink 算子的工作方式

Flink Doris Connector 的 Sink 算子接收来自上游的数据流,采用批量缓冲 + 定时刷写的策略:
来自 GroupAggregate 的数据(含 Retract 消息)
  → Sink Buffer(内存缓冲区)
  → 满足以下任一条件触发 flush:
      ① 缓冲行数 ≥ sink.buffer-flush.max-rows(10000行)
      ② 距上次 flush ≥ sink.buffer-flush.interval(5000ms)
  → 序列化为 JSON 格式
  → HTTP POST → Doris FE 的 Stream Load 接口

2. Doris 收到数据后的处理

Doris 接收到 Stream Load 请求后:
  • partial_update = 'true':开启部分列更新模式。Doris 会根据主键(dim_date + dim_dept + name_employe_no 联合主键)查找已有行,如果存在则只更新 outp_cn 字段,不影响其他列。这正好配合了 Flink 的 Retract 机制——旧值被新值覆盖,实现了流式聚合结果的持续更新。
  • Doris 内部会先将数据写入内存的 MemTable,达到阈值后 flush 成不可变的 Segment 文件,后台 Compaction 线程负责将多个小 Segment 合并成大 Segment,这就是为什么你查询 Doris 时数据会有短暂延迟的原因。

3. 为什么 outp_cn 要定义为 DECIMAL(20,2)

这是 Doris 建表时的字段类型定义。虽然 SUM(1) 的结果是整数,但 Flink 的 SUM 聚合函数在处理数值时,内部类型推断结果是 DECIMAL(防止溢出),Doris 端用 DECIMAL(20,2) 兼容接收这个值,精度上完全够用。

六、排查问题的核心思路总结

理解了上述机制后,遇到问题可以按这个链路快速定位:
现象
优先排查位置
关键点
任务提交即报错
SQL 编译期
类型不匹配、字段名拼写、连接器参数错误
任务运行但无数据
Source / Filter 算子
Kafka offset 位置、JSON 解析失败、WHERE 过滤过严
数据写入 Doris 延迟大
Sink 缓冲配置
调小
buffer-flush.interval
Doris 数据不对 / 数值偏小
GroupAggregate 状态
State 丢失(Checkpoint 配置)、Retract 消息未被正确处理
类型转换异常导致 NULL
Calc 算子
REGISTER_TIME 原始值格式不标准
任务 OOM
State Backend
分组 Key 基数过大,状态膨胀
掌握了Source 反序列化 → Filter 过滤 → Calc 计算(类型转换在此) → GroupAggregate 有状态聚合(Retract 机制) → Sink 批量刷写 → Doris 部分列更新这条完整链路,基本上 80% 的 Flink SQL 流处理问题都能快速定位到具体的算子层。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ben@dw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值