十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Langfuse 中的 ClickHouse 写入优化:彻底理解并规避 `OPTIMIZE TABLE FINAL`

Langfuse 中的 ClickHouse 写入优化:彻底理解并规避 `OPTIMIZE TABLE FINAL` Langfuse 中的 ClickHouse 写入优化彻底理解并规避OPTIMIZE TABLE FINAL【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse导读OPTIMIZE TABLE ... FINAL会强制把表内所有 data part 立即合并成每个分区一个 part这是一项资源开销极高的操作在大多数生产场景中既无必要也有害。本文以 Langfuse 仓库内.agents/skills/clickhouse-best-practices/rules/insert-optimize-avoid-final.md规则为骨架结合仓库中 ClickHouse 迁移脚本与查询构建器源码讲清楚为什么应该让 ClickHouse 后台 merge 机制自动工作、OPTIMIZE FINAL与查询中的FINAL修饰符有何本质区别以及在使用 ReplacingMergeTree 的场景下如何正确完成去重、导出等操作。读完本文你将掌握一条可直接落地的 ClickHouse 写入与去重优化策略并了解 Langfuse 在events表上的真实工程实践。一、规则背景这条规则从哪来影响等级有多高insert-optimize-avoid-final.md属于 Langfuse 仓库内置的 ClickHouse 最佳实践技能包Agent Skill。该技能包位于 .agents/skills/clickhouse-best-practices/包含 28 条原子规则按schema-、query-、insert-三个前缀划分为 11 个类别每条规则都标注了影响等级CRITICAL / HIGH / MEDIUM。本规则的元数据如下标题Avoid OPTIMIZE TABLE FINAL影响等级HIGH影响描述强制合并所有 part 成本高昂应让后台 merge 自动工作标签insert、OPTIMIZE、merge、performance在技能包的规则优先级表中insert-optimize-类位于第 10 优先级HIGH 级别与其并列的还有insert-async-异步插入、insert-mutation-避免 mutation等写入相关规则。技能包 SKILL.md 中明确要求在进行 INSERT 策略评审数据摄入、更新、删除时必须按顺序阅读 5 条规则其中第 5 条就是本规则。这意味着任何涉及插入后是否要执行 OPTIMIZE的代码评审这条规则都是必查项。注意外部参考链接官方 ClickHouse 最佳实践文档在本仓库中不可访问本文的所有结论均以仓库内规则文件与源码为依据。二、为什么OPTIMIZE TABLE FINAL是危险的ClickHouse 的 MergeTree 家族采用先写小 part后台异步合并的架构。每次INSERT都会产生新的不可变 data part后台的 merge 线程会按照策略自动把小的 part 合并成大的 part。OPTIMIZE TABLE ... FINAL则会强制立即触发一次全表合并把所有 part 合并为每个分区一个 part。这条规则总结了它的四大问题无论是否需要都会重写整个分区即使分区内数据几乎不需要合并也会产生全量重写带来巨大的写放大。绕过约 150 GB 的 part 大小保护阈值ClickHouse 后台 merge 对超大 part 有保护机制而OPTIMIZE FINAL会无视这一保护可能生成超出安全范围的超大 part。可能导致内存压力或 OOM 错误合并需要把多个 part 的数据读入内存进行排序和重写全表强制合并对内存的消耗远超常规后台 merge。大数据集下执行时间极长对生产环境的大表执行OPTIMIZE FINAL可能阻塞数十分钟甚至数小时期间持续占用 CPU、磁盘 I/O 和内存。从 Langfuse 的实际数据规模来看trace、observation、score 等核心事件表承载着高吞吐的遥测数据摄入见 packages/shared/clickhouse/migrations/canonical/0001_traces.up.sql 等迁移脚本任何每次批量插入后都跑一遍 OPTIMIZE FINAL的做法都会让摄入管道陷入持续的合并风暴。三、核心辨析OPTIMIZE FINAL≠ 查询中的FINAL这是本规则最容易被误解的地方必须严格区分两个概念语法类型作用是否推荐OPTIMIZE TABLE ... FINAL表级维护操作强制物理合并所有 part仅在极少数一次性场景SELECT ... FINAL查询修饰符在查询时合并 ReplacingMergeTree 的重复行返回去重后的最新版本在 ReplacingMergeTree 上按需使用一般无害规则原文明确指出OPTIMIZE FINAL不是FINAL。SELECT 查询中的FINAL修饰符对于 ReplacingMergeTree 的去重结果可能是必要的通常可以放心使用。SELECT ... FINAL只是在读取路径上合并具有相同排序键ORDER BY的多行取版本最新的一行它不会重写磁盘上的任何 part因此成本远低于OPTIMIZE FINAL。Langfuse 仓库正是这么做的在 packages/shared/src/server/repositories/traces.ts 中多处查询显式使用FINAL修饰符例如FROM observations o FINAL第 1288 行、FROM ${traceTable} FINAL第 1124、1222、1310 行、FROM scores FINAL第 1693 行packages/shared/src/server/queries/clickhouse-sql/query-fragments.ts 第 224 行同样出现FROM scores FINAL。这些都是用查询期 FINAL 解决去重而非用 OPTIMIZE FINAL 物理去重的典型实践。四、反模式这些写法应当避免规则文件给出了两类最常见的错误用法4.1 每次批量插入后立即执行 OPTIMIZE FINAL-- 每次批量插入后都运行 OPTIMIZE FINAL错误示范 INSERT INTO events SELECT * FROM staging_events; OPTIMIZE TABLE events FINAL; -- 昂贵且没有必要问题在于INSERT刚产生的 part 本身就是新写入的小 part随后立刻OPTIMIZE FINAL等于把后台 merge 本来会做的事情提前抢做一遍而且是以最暴力的全表合并方式。4.2 定时任务反复执行 OPTIMIZE FINAL-- 定时 OPTIMIZE FINAL 任务错误示范 -- Cron: 0 * * * * clickhouse-client -q OPTIMIZE TABLE events FINAL按小时甚至按分钟调度的OPTIMIZE FINAL会在每个分区反复执行全量重写且每次重写的数据量只会随表增长而增长。Langfuse 技能包中与写入相关的其他规则insert-batch-size.md、insert-async-small-batches.md共同传达的一个核心思想是part 数量问题应该从源头批量大小、异步缓冲解决而不是事后用强制合并补救。五、正确做法让后台 merge 自动工作规则给出的正确姿势非常简单-- 让后台 merge 自动处理优化正确示范 INSERT INTO events SELECT * FROM staging_events; -- 完成ClickHouse 会自动合并 -- 对于 ReplacingMergeTree 去重在查询中使用 FINAL SELECT * FROM events FINAL WHERE user_id 123; -- 而不是运行 OPTIMIZE FINAL 去重也就是说插入后什么都不用做ClickHouse 的 merge 调度器会自动选择合适时机把多个 part 合并需要去重结果时在 SELECT 上使用FINAL修饰符或等价聚合如argMax。5.1 Langfuse 的 events 表连FINAL都不需要Langfuse 的技能包对events表有一条更激进的 Langfuse 专属规则见 SKILL.md永远不要在events表上使用FINAL该表被设计为不需要FINAL使用该关键字反而会损害性能。events表是 Langfuse v4 的统一事件表所有 trace、observation、score 事件都沉淀其中列定义见 packages/shared/src/eventsTable.ts。由于该表在写入路径上已经通过事件时间戳等机制消除了重复行查询路径上使用FINAL只会增加额外的去重开销。这也解释了仓库中 event-query-builder.ts 的设计所有针对events表的查询都通过EventQueryBuilder/CTEQueryBuilder生成而不会手写 SQL。5.2 当右侧存在未合并的 ReplacingMergeTree 重复时用 LEFT ANY JOIN一个值得注意的仓库内细节虽然events表本身不用FINAL但 Langfuse 的观察数据读取会 JOIN 到events_full等可能含有尚未被后台合并的 ReplacingMergeTree 重复行的 CTE。此时 event-query-builder.ts第 1717-1730 行提供了leftAnyJoin方法LEFT ANY join another CTE. ANY takes exactly one matching row from the right side, avoiding the row fan-out a plain LEFT JOIN produces when the right side has un-merged ReplacingMergeTree duplicates.并在实际组装查询时第 2168-2175 行明确注释// LEFT ANY JOIN (not LEFT JOIN): the io CTE reads events_full, which can // hold un-merged ReplacingMergeTree duplicates. A plain LEFT JOIN would // fan out to N_base × N_io rows; ANY takes one matching io row per base row.这个设计印证了规则的核心观点后台 merge 是异步的任何时刻表中都可能存在重复行应对方式是在查询侧正确处理FINAL或ANYJOIN而不是先跑OPTIMIZE FINAL把数据物理合并干净再查询。物理合并只保证那一刻的状态新写入的重复行很快又会出现。六、什么时候OPTIMIZE FINAL是可以接受的规则明确列出三种一次性场景强调一次性操作而非常规工作流表冻结freezing前的数据定稿例如把某张表归档或改为只读之前让所有 part 合并为每分区一个便于后续管理导出操作前的数据准备例如导出全表数据到外部系统之前先合并以减少导出时的 part 遍历开销一次性维护操作例如诊断性操作或数据修复后的一次性整理。关键判定标准是频率。如果某个OPTIMIZE FINAL出现在定时任务或每次写入后的业务代码里它就是反模式如果它是运维人员手动执行的一次性操作则可以被接受。从 Langfuse 的源码看仓库的迁移脚本packages/shared/clickhouse/migrations/canonical/和存储层代码中均没有对业务表执行OPTIMIZE TABLE ... FINAL的逻辑写入与维护完全依赖后台 merge 与查询期去重——这与规则的推荐一致。七、更好的替代方案速查规则用一张表总结了不同需求下的正确替代方案需求替代方案ReplacingMergeTree 去重在 SELECT 中使用FINAL修饰符减少 part 数量依赖后台 merge在此基础上结合 Langfuse 技能包中的相邻规则可以再补充两个治本手段控制 part 产生的源头见 insert-batch-size.md每次 INSERT 建议 10K-100K 行最低 1,000 行同步插入速率约每秒 1 次避免单行或小批量插入产生海量小 part高频小批量场景启用异步插入见 insert-async-small-batches.md服务端用async_insert缓冲并自动生成更大的 part配合wait_for_async_insert 1保证持久性更新需求不要用 mutation见 insert-mutation-avoid-update.md用ReplacingMergeTree加版本列插入新版本行 查询期FINAL或argMax聚合这是 Langfuse 迁移脚本中大量使用的模式。八、监控与验证如何确认后台 merge 在正常工作规则未直接给出监控方法但结合仓库实际使用的 MergeTree 引擎ReplacingMergeTree和技能包中 insert-batch-size.md 的校验手段推荐用system.parts表观察 part 数量趋势-- 监控各表活跃 part 数量超过约 3000 个 part 会阻塞插入 SELECT table, count() as parts, sum(rows) as total_rows FROM system.parts WHERE active AND database default GROUP BY table ORDER BY parts DESC;判断逻辑如果 part 数量长期稳定在一个合理区间说明后台 merge 正常工作不需要也不应该引入OPTIMIZE FINAL如果 part 数量持续暴涨应当回头检查写入策略批量大小是否过小、是否启用了异步插入缓冲而不是用OPTIMIZE FINAL反复兜底。Langfuse 仓库在 packages/shared/src/server/repositories/clickhouse.ts 的注释中第 396 行附近也提到了对fat pipeline chunk超大管道数据块的限制设计说明其对 ClickHouse 端到端的数据块大小是精心调校过的——这类调校正是为了减少对大块数据做FINAL/合并处理的压力。九、总结一条可复用的写入纪律围绕insert-optimize-avoid-final.mdLangfuse 的工程实践可以提炼为一条完整的写入纪律插入侧用 10K-100K 行的批量 INSERT必要时配合async_insert服务端缓冲从源头控制 part 数量合并侧完全信任 ClickHouse 后台 merge永远不要在业务代码或定时任务中调用OPTIMIZE TABLE ... FINAL读取侧需要去重时用SELECT ... FINAL或LEFT ANY JOIN/argMax聚合处理 ReplacingMergeTree 的重复行例外仅在表冻结、导出准备等一次性维护场景由运维人员手动执行OPTIMIZE FINAL。仓库中 0001_traces.up.sql、0002_observations.up.sql、0003_scores.up.sql 三张核心表全部采用ReplacingMergeTree(event_ts, is_deleted)引擎查询层用FINAL/ANY JOIN处理未合并重复行写入层不执行任何强制合并——这就是避免 OPTIMIZE FINAL这一规则在真实生产代码中的完整落地样板。【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表