基础及入门(3)-批数据处理机制

一、源端数据读取格式与机制

1、Connector 抽象层

Flink SQL 通过 Connector 机制屏蔽底层存储差异。你的源表配置的是 'connector' = 'iceberg',Flink 在运行时会:

  1. 加载 FlinkIcebergTableSource 实现类
  2. 通过 REST Catalog(http://192.168.5.143:9007)获取表的 schema、partition 信息、snapshot 元数据
  3. 根据 Iceberg 的 metadata 定位 S3 上的实际数据文件路径(Parquet/ORC/Avro 格式)
  4. 调用 S3FileIO 从 MinIO(http://192.168.3.62:9000)读取数据文件
sql 复制代码
Flink TaskManager
     │
     ▼
Iceberg Source Connector
     │── REST Catalog → 获取 snapshot/manifest
     │── S3FileIO     → 读取 Parquet 数据文件
     ▼
内部行格式:RowData(二进制紧凑格式)

2、内部数据格式:RowData

数据进入 Flink 引擎后, 不是 String/Map ,而是 RowData 对象,每一列对应一个强类型的槽位(slot):

|------------------|-------------------------------|
| Flink SQL 类型 | 内部存储 |
| STRING | StringData(UTF-8 字节数组) |
| INT | 原始 int |
| BIGINT | 原始 long |
| TIMESTAMP_LTZ(3) | TimestampData(epoch毫秒 + 纳秒部分) |
| DECIMAL | DecimalData |

这意味着 类型必须在进入引擎前就已确定,Iceberg schema 与 Flink DDL 的类型必须兼容,否则在 Source 阶段就会报错。

二、计算引擎对数据的处理过程

1、算子拆解

sql 复制代码
[Source: IcebergSource]
       │  RowData 流
       ▼
[Calc(投影 + 表达式计算)]
       │  转换后的 RowData 流
       ▼
[Sink: DorisSink]

Calc 算子是核心,它完成所有列的映射和表达式求值,逐行处理, 无 Shuffle、无聚合

2、内置函数(Built-in Functions)

你脚本里用到的都是 Flink SQL 内置函数,运行在 Calc 算子内部,JVM 本地执行,无网络开销:

|-------------------------|------------------|------------------------------------------|
| SQL 表达式 | Flink 内置函数 | 说明 |
| CAST(x AS TIMESTAMP(3)) | CastRule | 类型转换,编译期确定转换逻辑 |
| CAST(x AS BIGINT) | CastRule | String → long |
| CAST(x AS DATE) | CastRule | String → 天数整数 |
| CAST(x AS TINYINT) | CastRule | String → byte |
| NULLIF(x, '-') | NullIfFunction | 等价于 CASE WHEN x='-' THEN NULL ELSE x END |
| COALESCE(a, b) | CoalesceFunction | 返回第一个非 NULL 值 |

重点 : NULLIF(S.HOSPITAL_DAY, '-') 先把 '-' 变成 NULL,再 CAST 为 BIGINT。如果不做这一步,直接 CAST '-' 为 BIGINT 会抛出运行时异常。这是你这个脚本里的一个关键防御性写法。

3、批处理执行模型

批模式下( execution.runtime-mode = BATCH):

  • 数据不是流式逐条处理 ,而是按批次(buffer) 拉取
  • Iceberg Source 会并行读取多个 data file,每个 split 对应一个 SubTask
  • 无 Watermark、无 Checkpoint,失败需整体重跑

三、数据写入 Doris 的机制

1、DorisSink 的写入流程

Flink Doris Connector 的写入链路:

sql 复制代码
Calc 算子输出 RowData
       │
       ▼
DorisRowDataSerializer(RowData → JSON bytes)
       │  你配置了 'sink.properties.format' = 'json'
       ▼
DorisBatchWriter 内存缓冲区
  ├── 满 10000 行  → flush(sink.buffer-flush.max-rows)
  └── 超 5000ms   → flush(sink.buffer-flush.interval)
       │
       ▼
HTTP Stream Load 请求 → FE(192.168.2.111:30031)
       │
       ▼
Doris BE 接收数据

2、Doris 收到数据后的处理

因为你配置了 'sink.properties.partial_update' = 'true',Doris 会执行部分列更新:

sql 复制代码
Stream Load JSON → Doris BE
    │
    ├── 解析 JSON,按列名匹配
    ├── 查找目标表已有行(通过 Key 列)
    │       ├── 存在 → 只更新本次写入的列,其他列保持不变
    │       └── 不存在 → 插入新行
    └── 写入列存文件(Segment)

这里需要注意:Doris 的 partial_update 依赖目标表是 Unique Key 模型,Key 列必须包含在每次写入数据中(你的脚本中 inp_no 和 inst_code 应是 Key 列)。

四、为什么必须显式类型转换?何时发生?

1、根本原因:类型系统不互通

|-----------|-------------------------------------------------|
| 层次 | 类型系统 |
| Iceberg 表 | Iceberg 类型(string, int, timestamptz...) |
| Flink 内部 | Flink SQL 类型(STRING, INT, TIMESTAMP_LTZ...) |
| Doris 表 | Doris 类型(VARCHAR, BIGINT, DATETIME, TINYINT...) |

Flink DDL 声明了源表和目标表的 schema, 但并不保证列与列之间类型天然匹配。例如:

  • 源表 HOSPITAL_DAY 是 STRING(存的是 "5" 或 "-")
  • 目标表 actual_inp_day_count 是 BIGINT
    Flink 不会自动隐式转换 STRING → BIGINT(不像某些数据库会),必须你 显式声明意图

2、类型转换发生的时间点

sql 复制代码
编译期(Planner 阶段):
  └── 验证 CAST 是否合法(STRING→BIGINT 是否支持)
  └── 生成 CodeGen 代码(将 CAST 编译成具体的 Java 方法调用)

运行期(TaskManager 执行阶段):
  └── 逐行执行 CAST,对每条 RowData 的对应列做转换
  └── 若转换失败(如 CAST('abc' AS BIGINT)),抛出运行时异常

3、常见转换失败场景(便于你排查问题)

|-------------------------|--------------------------------|------------------------------------|
| 错误 | 现象原因 | 解法 |
| NumberFormatException | STRING 列有非数字字符直接 CAST 为数值类型 | 先 NULLIF(col, '-') 再 CAST |
| DateTimeParseException | 时间字符串格式不符合 yyyy-MM-dd HH:mm:ss | 用 TO_TIMESTAMP(col, 'format') 指定格式 |
| NullPointerException | NOT NULL 列写入了 NULL | 用 COALESCE 提供默认值 |
| Doris Stream Load 报列不匹配 | JSON 字段名大小写与 Doris 列名不一致 | 检查列名,Doris 列名默认小写 |

五、完整数据流转总结图

sql 复制代码
MinIO(S3)Parquet 文件
       │
       │ S3FileIO 读取
       ▼
Iceberg Source(SubTask × N 并行)
       │ RowData(强类型,二进制)
       ▼
Calc 算子(逐行处理)
  ├── NULLIF:脏数据转 NULL
  ├── COALESCE:NULL 填默认值
  ├── CAST:类型强制转换
  └── 列重命名映射
       │ 转换后 RowData
       ▼
DorisBatchWriter(内存缓冲)
  └── 缓冲满 / 定时 flush
       │ JSON 格式 HTTP POST
       ▼
Doris FE → BE
  └── partial_update Unique Key Merge
       │
       ▼
Doris 列存(zoestd_hdc.hdc_case_homepage_inp)

六、快速排查错误的思路

掌握上述机制后,遇到报错可按这个顺序定位:

  1. Source 阶段报错 → Iceberg schema 与 Flink DDL 类型不匹配,或 S3/REST Catalog 连接问题
  2. Calc 阶段报错(运行时) → CAST 失败,检查源数据脏值,补 NULLIF 或 TRY_CAST
  3. Sink 阶段报错 → JSON 序列化问题、Doris Stream Load 返回错误码、partial_update Key 列缺失
  4. 数据写入但结果不对 → 检查 NULLIF 的过滤值是否覆盖所有脏值格式(比如除了 '-' 还有 '' 空串)