一、源端数据读取格式与机制
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)读取数据文件
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、算子拆解
[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 的写入链路:
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 会执行部分列更新:
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、类型转换发生的时间点
编译期(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 列名默认小写
|
五、完整数据流转总结图
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 的过滤值是否覆盖所有脏值格式(比如除了 '-' 还有 '' 空串)
2446

被折叠的 条评论
为什么被折叠?



