Paimon 主键表写入变慢:两个默认参数先把写入顶停了

举报
luckyz 发表于 2026/11/08 17:05:38 2026/11/08
【摘要】 某制造企业的设备状态数据走 CDC 实时同步到 Paimon 主键表,底下是对象存储。表刚建好的头两周一切正常,写入延迟稳定在秒级;第三周开始,Flink 作业的写入吞吐隔一阵就掉到接近 0,持续几十秒后又恢复,检查点耗时也跟着上下抖动。日志里没有报错,没有异常堆栈,只有一条看着像正常现象的记录:写入任务周期性地停下来等合并。这不是流量问题,而是 Paimon 主键表的合并机制在按默认值工作...

某制造企业的设备状态数据走 CDC 实时同步到 Paimon 主键表,底下是对象存储。表刚建好的头两周一切正常,写入延迟稳定在秒级;第三周开始,Flink 作业的写入吞吐隔一阵就掉到接近 0,持续几十秒后又恢复,检查点耗时也跟着上下抖动。日志里没有报错,没有异常堆栈,只有一条看着像正常现象的记录:写入任务周期性地停下来等合并。

这不是流量问题,而是 Paimon 主键表的合并机制在按默认值工作。写得越久,Level 0 的小文件攒得越快,合并一旦跟不上,写入就会被顶停。这篇文章就讲清楚:是哪两个默认参数在起作用,怎么确认,以及该怎么调。

主键表写入的真实路径:先落文件,再等合并

Paimon 主键表是 LSM 结构。写入的数据先进入内存表,写满后刷成一个 Level 0 文件;每个 Level 0 文件各自就是一个 sorted run。查询时要把这些 sorted run 连同高层文件一起合并去重,所以 Level 0 文件越多,读的代价越大;合并就是把多个 sorted run 压成一个,顺便把同一主键的旧版本清掉。

合并不是免费的,它和写入抢同一批资源。Paimon 用两个参数控制这场拉锯,官方配置文档里的默认值如下:

  • num-sorted-run.compaction-trigger:默认 5。sorted run 数量达到 5 就触发一次合并;
  • num-sorted-run.stop-trigger:没有固定默认值,默认等于触发值加 3,也就是 8。sorted run 数量达到 8,写入直接停下来,等合并把数量压下去再继续。

把两个默认值拼在一起,故障链就很清楚了:高频小批量写入让 Level 0 文件快速攒到 5 个,触发合并;如果合并速度跟不上写入速度,sorted run 继续涨到 8,写入被强制暂停;合并完成后写入恢复,文件又开始攒,于是吞吐呈现“写一阵、停一阵”的锯齿。检查点耗时抖动也是同一个原因:检查点要等写入刷盘和提交,写入一停,检查点就跟着等。

先确认:到底是不是合并跟不上

调参数之前,先用一条 SQL 看表里文件的真实分布。Paimon 每个表都有 $files 系统表:

SELECT `partition`, bucket, level, COUNT(*) AS file_cnt,
       SUM(file_size_in_bytes) / 1024 / 1024 AS total_mb
FROM `device_status$files`
GROUP BY `partition`, bucket, level
ORDER BY `partition`, bucket, level;

重点看两列数字:

  • Level 0 的文件数长期在 5 到 8 之间来回跳,说明合并一直在追赶的边缘;
  • 单个文件的 file_size_in_bytes 远小于目标文件大小。主键表 target-file-size 的默认值是 128 MB,如果文件普遍只有几 MB,说明每次刷盘的数据量太小,文件攒得自然快。

如果 Level 0 文件数常年只有一两个,写入停顿另有原因,别往合并参数上凑。如果 Level 0 长期顶在 8 附近、甚至触发了写停顿,再往下看。

一组可以直接抄用的建表配置

下面这组配置是给“高频小批量 CDC 写入、主键表、对象存储”准备的,思路不是把合并关掉,而是把触发点和停顿点一起往后挪,给合并留出追赶的窗口,同时把刷盘粒度放大:

CREATE TABLE device_status (
    device_id BIGINT,
    status STRING,
    temperature DOUBLE,
    update_time TIMESTAMP,
    dt STRING,
    PRIMARY KEY (device_id, dt) NOT ENFORCED
) PARTITIONED BY (dt) WITH (
    'bucket' = '4',
    'num-sorted-run.compaction-trigger' = '10',
    'num-sorted-run.stop-trigger' = '20',
    'write-buffer-size' = '256 mb',
    'target-file-size' = '128 mb'
);

每一项的取舍都说清楚:

  • 触发值从 5 调到 10:合并次数大约减半,写入少被打断;代价是查询时要合并的 sorted run 变多,读延迟会上升一点,写多读少的表划算;
  • 停顿值从 8 调到 20:触发值和停顿值之间留出 10 个 run 的缓冲带,合并偶尔慢一拍也不会立刻顶停写入;
  • 触发值和停顿值要一起调。只调大触发值不调停顿值,缓冲带只剩 3 个 run,照样容易顶停;只调大停顿值不调触发值,合并触发得太频繁,写资源全花在合并上;
  • write-buffer-size 256 MB:内存表攒够数据再刷盘,单个 Level 0 文件更大,文件总数自然下来。对象存储上刷盘本身有延迟,小文件多一倍,延迟就多一倍。

对已经建好的表,用 ALTER TABLE 改同一组参数即可,不用重建:

ALTER TABLE device_status SET (
    'num-sorted-run.compaction-trigger' = '10',
    'num-sorted-run.stop-trigger' = '20'
);

改参数之前,先排掉三个真问题

参数只能拉开缓冲,不能代替治理。调参之前,按顺序查这三处:

第一,看是不是全量同步阶段开了 changelog。 如果建表时配了 'changelog-producer' = 'full-compaction',再加上 'full-compaction.delta-commits' = '1',每次增量提交都会触发一次同步全量合并,写入效率直接被拖垮。官方文档的建议是:全量同步阶段先不配这两个参数,等进入增量阶段再打开。历史数据还没灌完就谈合并参数,是调错了地方。

第二,看分桶数是不是太少。 合并是按桶进行的,桶太少,每个桶里的文件攒得快,合并压力全集中在少数几个桶上;桶太多,每个桶数据量小,又会反过来制造小文件。分桶数要跟着写入并发和单桶数据量一起定,不是一个可以抄作业的数字。

第三,看是不是小文件已经积压成灾。 如果 Level 0 之外,高层文件数也已经几百上千,说明历史欠账太多,只调触发值救不回来。先用一次手动全量合并清掉欠账,Spark 引擎可以直接调存储过程:

CALL sys.compact(table => 'ods.device_status');

清完欠账再用上面那组配置守住增量,别让文件数再爬回去。

小结

Paimon 主键表“写一阵、停一阵”,很多时候不是写入本身出了问题,而是两个默认值在按部就班地工作:sorted run 到 5 触发合并,到 8 顶停写入。先用 $files 系统表确认 Level 0 文件数是不是长期在 5 到 8 之间徘徊,再把触发值和停顿值一起往后挪、把刷盘粒度放大,给合并留出追赶的窗口。参数调得再大,也替代不了分桶和 changelog 策略的治理;但触发点和停顿点留对了,高频写入就不会隔一阵被合并顶停一次。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。