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-size256 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 策略的治理;但触发点和停顿点留对了,高频写入就不会隔一阵被合并顶停一次。
- 点赞
- 收藏
- 关注作者
评论(0)