一、源端数据读取格式与机制
1、Connector 抽象层
Flink SQL 通过 Connector 机制屏蔽底层存储差异。你的源表配置的是 'connector' = 'iceberg',Flink 在运行时会:
- 加载 FlinkIcebergTableSource 实现类
- 通过 REST Catalog(http://192.168.5.143:9007)获取表的 schema、partition 信息、snapshot 元数据
- 根据 Iceberg 的 metadata 定位 S3 上的实际数据文件路径(Parquet/ORC/Avro 格式)
- 调用 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)
六、快速排查错误的思路
掌握上述机制后,遇到报错可按这个顺序定位:
- Source 阶段报错 → Iceberg schema 与 Flink DDL 类型不匹配,或 S3/REST Catalog 连接问题
- Calc 阶段报错(运行时) → CAST 失败,检查源数据脏值,补 NULLIF 或 TRY_CAST
- Sink 阶段报错 → JSON 序列化问题、Doris Stream Load 返回错误码、partial_update Key 列缺失
- 数据写入但结果不对 → 检查 NULLIF 的过滤值是否覆盖所有脏值格式(比如除了 '-' 还有 '' 空串)