Day 17|为什么给 Paimon 加 REFRESH CATALOG:加对了,加错位置把查询打挂了
系列:《Cloud GIDO 三产品特性深讲》加餐 · 场记第 2 篇
标签:Doris、Paimon、REFRESH CATALOG、元数据缓存、DolphinScheduler、湖仓调度
开篇:不加会挂,加上也挂,问题出在「刷几次」
湖仓上线一段时间后,几乎每个团队都会撞同一面墙:
- Flink 往 Paimon 写得好好的
- Doris 读 ODS 做
INSERT OVERWRITE时突然 FileNotFound - 有人说「加一句
REFRESH CATALOG paimon」 - 读 Paimon 的任务全部加上了
- 调度开始成片变红,探查、报表也像「系统查不了」
前半段判断是对的:Paimon 读任务确实需要 refresh。
后半段是事故:不要每条叶子都刷整个 catalog,尤其不要同一秒并行刷。
这篇按真实排查写完:为什么加 → 加错之后怎么挂 → 怎么定位 → 最终方案。
配图已脱敏:集群名、业务库表、调度 ID 均用通用写法。

为什么必须加:不是「迷信 DDL」,是 snapshot 已经被删了
链路很短:
CDC / Kafka → Flink 写出 snapshot → Paimon 只保留最近若干份
↓ expire 旧 manifest / 数据文件
Doris Catalog 默认缓存(常见小时级)
Flink 侧 snapshot.num-retained / expire 一跑,对象存储上的旧文件就没了。
Doris 若还拿着旧 snapshot 去扫,BE 会去读已经不存在的文件。现场常见:
FileNotFoundException
No such file or directory
paimon snapshot expired
这就是「不加 REFRESH 会报错」的真实原因,不是 SQL 口径写错。
REFRESH CATALOG paimon 会把该 catalog 的 partition / schema / file cache 一把失效(FE 日志里是 invalidCache true)。下一次读才会跟到最新 snapshot。
所以当时的决策是合理的:
读 Paimon 的离线任务,执行前必须让 Doris 丢掉过期元数据。
错的是实现方式:复制粘贴到每一条叶子 SQL 的第二条。
加错位置之后:系统查询「全挂」长什么样
典型叶子脚本变成了:
USE lakehouse_dw;
REFRESH CATALOG paimon;
INSERT OVERWRITE TABLE lakehouse_dw.dwd_biz_xxx
SELECT ...
FROM paimon.paimon_ods.ods_xxx
;
一个业务 DAG 十几条 DWD / DWS / ADS 同一秒扇出。Dolphin 按分号拆语句,于是同一秒对 Master 连打 N 次 catalog 级 refresh。

调度日志里真正要命的不是 INSERT,而是第二条:
ForwardToMasterException
forward to master FE fe-0:9020
failed, cause: EOF, Socket is closed by peer
对应关系:
| 语句 | 打到哪 | 结果 |
|---|---|---|
USE ... | 当前 FE(Follower 也能做) | 成功 |
REFRESH CATALOG paimon | 必须 forward Master :9020 | EOF,任务失败 |
INSERT OVERWRITE ... | 还没轮到 | 根本没跑 |
于是你会看到:
- 调度实例整片失败(红的是「刷新元数据」,不是加工)
- 探查 / 报表像系统查询挂了——下游表根本没被 overwrite
- 有人去改 SELECT 口径、去翻 FE 的 rebalancer INFO,全部打空
JDBC 还经常走 jdbc:mysql:loadbalance://...:9030。9030 是 MySQL 协议,REFRESH 这种 DDL 要转到 Master 的 9020。Follower 一忙、连接被掐,就是 EOF。
「系统查询失败」之所以听起来像整库挂了,是因为现象叠了三层:
- 调度失败:叶子卡在第二条,后面的 overwrite 全部没跑,实例中心一片红。
- 探查失败:有人在 Studio 里随手
SELECTPaimon 表,catalog 正在被 N 次invalidCache来回打,偶发超时或读到半刷新状态。 - 报表失败:ADS 没被写成新分区,业务侧看起来像「数仓查不出来」。
这三层的根都是同一件事:把本该一次完成的 catalog 刷新,做成了 DAG 扇出风暴。
插件有时会顺带打出 Connect strings must start with jdbc:snowflake://,那是 SQL 插件误判,可忽略。脚本头被剥注释后留下的大段空行,也不影响执行。
定位:Master 其实是活的,只是被刷爆了
排查时最容易被带跑的,是把 Follower 的 replay 日志当成「接任务的那台 FE」。
现场时间线(已脱敏)大致是:
| 时刻 | 证据 | 含义 |
|---|---|---|
| T+0 | Worker 日志:USE 成功 | 当前 FE 活着 |
| T+0 同秒 | ForwardToMasterException / 9020 EOF | 这条任务的 REFRESH 没转过去 |
| T+0 同秒 | 另一台 Follower:refresh catalog paimon with invalidCache true | Master 已经接受并落盘了别人的 REFRESH |
| T+2s~T+10s | 若干 INSERT OVERWRITE 的临时表 rename 被 replay | 同 DAG 里有人刷成功、加工成功了 |
| 之后 | journal 恢复成每十几秒 +1 | Master 没有整体宕机 |
结论就三句:
- 失败点在第二条 REFRESH,不是 INSERT。
- Master 没挂——同秒已经有一次 catalog refresh 成功 replay。
- 其余叶子再刷一遍 零收益,只增加 9020 上的并发转发。

不要在 1 秒一轮的 CloudTabletRebalancer INFO 里找根因。
也不要用「这段 FE 日志时间对不上」去否定调度失败——要对着 同一秒、同一 statement id 搜 REFRESH CATALOG / Socket is closed。
现场还有两个常见误判,建议写进排障手册:
误判 A:去改 SELECT 口径。
任务连 INSERT 都没进,改 JOIN、改过滤、改业务日期,一条都不会让 ForwardToMasterException 消失。
误判 B:对着错误的 FE、错误的时间戳翻日志。
调度失败若是东八区 19:53,FE 若打 UTC 就是 11:53。差 8 小时翻到上午的 CloudTabletRebalancer,只能看到「tablet 很均衡」这种常态 INFO。要对齐时区,再搜 REFRESH CATALOG。
接到 Follower 的那台,只能证明 Master 的 edit log 在往前走;解释不了「我这条 JDBC 转发被掐」。同秒里部分节点 overwrite 成功,反而说明集群没有整体不可用——挂的是 并发刷新这条窄通道。
可复制的核对:
SHOW FRONTENDS; -- IsMaster / Alive / LastHeartbeat
SHOW PROCESSLIST;
# 对着失败时间戳搜 FE
ForwardToMasterException
Socket is closed
REFRESH CATALOG
refresh catalog paimon
最终方案:刷一次,后面只跑 INSERT
原则:元数据照样新,Master 转发从 N 次变成 1 次。

1. 工作流头节点(推荐,立即做)
单独一个 SQL 节点 refresh_paimon:
REFRESH CATALOG paimon;
约束:
- 所有读 Paimon 的 DWD 依赖它
- 失败重试 3 次,间隔 30 秒~1 分钟
- 叶子脚本删掉
REFRESH CATALOG,只留USE+INSERT OVERWRITE
叶子日常形态:
USE lakehouse_dw;
INSERT OVERWRITE TABLE lakehouse_dw.dwd_biz_xxx
SELECT ...
FROM paimon.paimon_ods.ods_xxx
WHERE COALESCE(`__deleted`, 'false') <> 'true'
;
改完必须 Studio 保存并重新发布生产,调度引擎才会换成新 SQL。只改草稿、不发布,线上还是旧的「每条都 REFRESH」。
发布前用这一张表自检:
| 检查项 | 通过标准 |
|---|---|
DAG 里 REFRESH CATALOG paimon 出现次数 | = 1(只在头节点) |
| 叶子 SQL 第二条 | 是 INSERT / DELETE,不是 REFRESH |
| 头节点失败重试 | ≥ 3,间隔 ≥ 30s |
| 叶子失败重试 | ≥ 3(瞬时 EOF 仍可能发生) |
| 依赖方向 | 所有读 Paimon 的 DWD → refresh_paimon |
Dolphin 会按分号拆语句。头节点里就放这一句,不要再拼 USE、不要再塞业务 DML,避免「刷新成功但后面莫名失败」把重试语义搅浑。
2. DAG 暂时改不了:叶子改成刷表
必须留在叶子里时,不要 catalog,只刷本任务用到的表:
REFRESH TABLE paimon.paimon_ods.ods_xxx;
比 REFRESH CATALOG 轻,也不和别的任务抢同一把「整个湖仓 cache 失效」。
仍然要开失败重试,否则偶发 9020 EOF 还是整任务失败。
3. 中长期:catalog 自己定时刷
调度 SQL 里不再手写 refresh,把刷新交给 catalog 属性(等价于后台 REFRESH CATALOG):
ALTER CATALOG paimon SET PROPERTIES (
"metadata_refresh_interval_sec" = "60"
);
间隔按 Flink snapshot 周期来,常见 1~5 分钟。确认后台刷稳定后,再从头节点拿掉手工 REFRESH。
若集群已是 Doris 4.1+,还可以把 meta.cache.paimon.table.ttl-second 调短。Cloud FE 3.x 优先用 metadata_refresh_interval_sec + 头节点,不要赌新参数。
定时刷不是银弹:间隔太短等于后台自己对 Master 做小型风暴;太长又会在 Flink expire 之后、下一次后台刷之前,把临时查询打回 FileNotFound。和离线 DAG 对齐的经验值是 略短于 expire 周期、略长于一次 DAG 扇出窗口。头节点仍建议保留,直到你确认后台刷已经覆盖「调度触发那一刻」。
4. 平台侧(GIDO)配套
| 项 | 做法 | 为什么 |
|---|---|---|
| 发布默认重试 | failRetryTimes=3(显式 0 才表示不重试) | 偶发 9020 EOF 重试一次通常就过 |
| 超时 | timeoutFlag=OPEN | 刷 catalog / 大 overwrite 不能无限挂 |
| 永不自动注入 | 平台不往用户 SQL 里塞 REFRESH CATALOG | 注入会把「刷一次」重新变成「每个节点都刷」 |
旧作业要 重新发布 才会带上重试 3。只改平台默认、不 republish,线上仍是 failRetryTimes=0。
不要做的
| 做法 | 结果 |
|---|---|
每个读 Paimon 的节点都 REFRESH CATALOG | 今天这场事故的结构 |
| 先去掉 refresh 再发布 | 回到当初的 snapshot / FileNotFound |
| 让平台自动给所有 SQL 前面拼 REFRESH | 等于把错误做法产品化 |
| 只加内存、只改口径、只盯 rebalancer | 失败点根本不在这些地方 |
和「扫云表把 BE 打挂」是两件事
上一篇场记(Day 16)写的是 BE RScan_normal 线程泄漏打满 pids.max。
这篇是 FE 元数据刷新打满 Master:9020。
两者会叠加,但不要混为一谈:
| Day 16 RScan | 本篇 REFRESH CATALOG | |
|---|---|---|
| 位置 | BE 线程 / PID | FE Master 转发 |
| 表象 | Compute 周期性 abort | 调度红、INSERT 没跑 |
| 加速因素 | 高频 INSERT…SELECT 扫云表 | N 个叶子并行 REFRESH CATALOG |
| 止血 | 降频、限并发扫云 | 刷一次 + 重试 |
invalidCache true 之后,下一次扫 Paimon 会重新拉 manifest,可能加重远端扫描。所以「全 catalog 失效」既伤 FE,也给 BE 加戏。头节点刷一次,两头都更干净。
经验教训
- 「必须 refresh」和「每条 SQL 都 refresh」不是一回事。
- 看失败语句序号:
USE成功、REFRESH失败、INSERT 没开始——不要先改口径。 - Follower replay ≠ 接 JDBC 的那台。 replay 成功只说明 Master 写成功了。
- Catalog 级失效是全局锁。 同秒刷 N 次,N-1 次是浪费。
- 调度默认重试次数是事故放大器。
failRetryTimes=0时,瞬时 EOF 就是整次失败。 - 平台不要自作聪明注入 DDL。 刷新策略属于 DAG 拓扑,不是脚本头装饰。
排查叙事可记成:
不加会 FileNotFound → 每条都 REFRESH → 9020 EOF → INSERT 没跑 → 头节点刷一次 + 重试 3
上线当天的操作顺序
不要「先全量去掉 REFRESH」。正确顺序是:
- 先加头节点
refresh_paimon,打开重试 3 次,先发一版(叶子暂时还带着旧 REFRESH 也行,最坏是多刷,但头节点能先把 snapshot 对齐)。 - 再改叶子:去掉
REFRESH CATALOG,保存并发布。 - 看一场完整调度:头节点成功、叶子从 INSERT 开始跑、下游 ADS 有新分区。
- 再决定要不要开 catalog 定时刷。后台刷稳定之前,不要撤掉头节点。
回滚也很明确:如果去掉叶子 REFRESH 之后又出现 FileNotFound,说明头节点没被依赖到,或调度实例跑的还是旧定义。先确认发布版本,而不是立刻把 N 条 REFRESH 贴回去。
Studio 里临时探查可以单独执行一次 REFRESH TABLE,不要养成「查询窗口也 REFRESH CATALOG」的习惯——那会在上班高峰和离线 DAG 抢同一条 Master 通道。
今日小结
Paimon + Doris 这条湖仓路上,REFRESH CATALOG 是对的药,吃错剂量会把调度和查询一起打挂。药要吃,但一次就够:让 Flink expire 之后,Doris 仍然能读到还在的 snapshot;不要让十几条叶子在同一秒去抢 Master 的 9020。
最终方案就一句话:
工作流最前面刷一次 Paimon catalog,后面只跑 INSERT;平台永不自动注入。
对跑 GIDO Batch、把 Flink/Paimon ODS 接到 Doris DWD 的团队:发布前先看 DAG 里有几个 REFRESH CATALOG。多于 1 个,就要改拓扑。
讨论:
- 你们 Doris 读 Paimon,是 catalog 定时刷,还是每个作业手动 REFRESH?
- 撞过
ForwardToMasterException时,最后是切主、9020 被打满,还是 JDBC 没打到 Master?
延伸
- 系列总览:https://cloud-gido.com
- 上一篇场记:Day 16|Doris BE 周期性重启:不是内存不够,是 RScan 把 PID 打满了
- Apache Doris:
REFRESH CATALOG/REFRESH TABLE/ External Catalog 属性 - 发 CSDN:本地上传
images/fig-doris-refresh-*.png(平台不认相对路径)