Chapter 09

ClickHouse 调优与成本治理

日 1000 万 trace 看起来吓人,其实靠对 ClickHouse 做四件事就能扛住:分区、TTL、Projection、冷热分层。再加点采样,预算直接砍半。

成本从哪里来

一条 trace 的成本大致分三段:

开销项占比(典型)主要驱动
ClickHouse 存储40-55%trace/observation/score 行数,input/output 冗余字段
S3 对象存储15-25%大 prompt 原文、多模态 blob
计算(Web/Worker)15-25%写入吞吐、UI 查询 QPS
LLM-as-Judge可高可低采样率 × 裁判模型单价

ClickHouse 是大头,优化投入产出比最高。

ClickHouse 表结构速读

Langfuse 主要写三张表:

traces
trace 根节点,一条用户请求一条记录。字段有 id / name / user_id / session_id / tags / metadata / timestamps。体积相对小。
observations
span / generation / event 的宿主。一条 trace 可能挂 1-50 条 observation。大头就在这里,input/output 引用 + token/cost。
scores
每条 trace / observation 挂多个 score。数量 = trace 数 × evaluator 数,增长最快但单行最小。

默认分区键是 toYYYYMM(start_time),ORDER BY 基本上是 (project_id, toDate(start_time), id)。这意味着:

① TTL:老数据自动消失

最简单、最直接的降本手段。Langfuse 支持在项目级设 retention,Worker 会发对应 ALTER 改 TTL。如果想在 ClickHouse 层面再加一道保险:

ALTER TABLE observations
  MODIFY TTL start_time + INTERVAL 90 DAY DELETE;

ALTER TABLE traces
  MODIFY TTL start_time + INTERVAL 180 DAY DELETE;

ALTER TABLE scores
  MODIFY TTL timestamp + INTERVAL 180 DAY DELETE;

典型留存策略:

TTL 改了不会立刻生效
TTL 只在 part merge 时触发。改完一两天才真正瘦身属于正常。想立刻清:ALTER TABLE ... MATERIALIZE TTL IN PARTITION '202511',或者直接 DROP PARTITION

② 分区裁剪:查询永远带时间

Langfuse UI 默认就带时间过滤,自己写 SQL 查 ClickHouse 时容易忘。一个反例:

-- 慢: 扫全表
SELECT count() FROM observations WHERE user_id = 'u_42';

-- 快: 分区裁剪 + 主键前缀
SELECT count() FROM observations
WHERE project_id = 'proj_x'
  AND start_time >= now() - INTERVAL 7 DAY
  AND user_id = 'u_42';

任何写分析脚本的人都要背这个模板,否则一个慢查询能把 ClickHouse CPU 打满 10 分钟,UI 全线变红。

③ Projection:把聚合"预算"下来

UI Dashboard 常问"按天 token 消耗"、"按模型 P95 延迟"。这种 GROUP BY 查询每次扫全表很浪费。ClickHouse 的 Projection 让它自动 materialize:

ALTER TABLE observations ADD PROJECTION proj_daily_token
(
  SELECT
    project_id,
    toDate(start_time) AS day,
    model,
    sum(prompt_tokens)     AS prompt_tokens,
    sum(completion_tokens) AS completion_tokens,
    count()                AS call_count
  GROUP BY project_id, day, model
);

ALTER TABLE observations MATERIALIZE PROJECTION proj_daily_token;

之后任何符合这个聚合形状的查询,ClickHouse 自动走 Projection——毫秒级。主表改行 Projection 会同步更新,写入侧有轻微代价(5-10%),但 Dashboard 性能是量级提升。

④ 冷热分层:热盘 SSD + 冷盘 S3

近 30 天的 trace 90% 被查,老数据 99% 只是躺着。ClickHouse 支持多卷存储策略,把老 part 自动移到 S3:

<!-- clickhouse config.xml -->
<storage_configuration>
  <disks>
    <hot>
      <path>/var/lib/clickhouse/</path>
    </hot>
    <s3_cold>
      <type>s3</type>
      <endpoint>https://s3.amazonaws.com/langfuse-ch-cold/</endpoint>
      <access_key_id>AKIA...</access_key_id>
      <secret_access_key>...</secret_access_key>
    </s3_cold>
  </disks>
  <policies>
    <tiered>
      <volumes>
        <hot><disk>hot</disk></hot>
        <cold><disk>s3_cold</disk></cold>
      </volumes>
    </tiered>
  </policies>
</storage_configuration>
ALTER TABLE observations MODIFY SETTING storage_policy = 'tiered';

-- 30 天前的 part 自动下沉到 S3
ALTER TABLE observations
  MODIFY TTL start_time + INTERVAL 30 DAY TO VOLUME 'cold',
             start_time + INTERVAL 90 DAY DELETE;

效果:热盘留 30 天,冷盘存 30-90 天,90 天自动删。热盘 SSD 成本是 S3 的 20 倍以上,这一层能把存储账单砍 60%+。

⑤ 写入侧:批量 + 压缩

Langfuse Worker 已经做了批量,但有几个参数值得调:

langfuse:
  worker:
    env:
      - name: CLICKHOUSE_WRITE_BATCH_SIZE
        value: "2000"              # 默认 500, 大批写更省
      - name: CLICKHOUSE_WRITE_FLUSH_INTERVAL_MS
        value: "2000"              # 默认 500ms, 拉到 2s

ClickHouse 表默认 ZSTD 压缩就很好,不用动。如果存储还紧,把 input_ref / output_ref 的 CODEC(ZSTD(3)) 调到 ZSTD(9)——CPU 多花一点,存储省一成。

⑥ 采样:不是所有 trace 都要存

真正的降本核弹是"不收"。几档采样策略按需选:

tail-based(尾采样)
错误 / 慢请求 / 高价值用户 100% 收,其余 10-30%。需要在 SDK 层按规则丢弃。
head-based(头采样)
SDK 入口直接按 user_id 或 request 哈希采样,简单但可能漏抓异常。适合流量巨大但高度重复的场景。
采细不采粗
trace 都收,但 observation 的 input/output 只存引用(S3),甚至大 body 跳过上传。ClickHouse 体积骤降。
# SDK 层简单采样例子
import random, hashlib

def should_sample(user_id: str, rate: float) -> bool:
    # VIP 100%, 其他按 rate
    if is_vip(user_id):
        return True
    h = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
    return (h % 10000) / 10000 < rate

if should_sample(user_id, 0.2):
    trace = lf.trace(...)
else:
    trace = None  # 完全跳过 Langfuse

⑦ 监控三要三不要

日常看这几个指标就够:

容量规划公式

粗估一下,以帮你要预算:

每日 trace 数 T
每 trace 平均 observation 数 O   (典型 3-8)
每 observation 平均 body 大小 B  (典型 2-10 KB, 走 S3)

ClickHouse 增量 ≈ T * O * 200 bytes
  (已经排除 body, 只算结构化字段 + 索引)

S3 增量 ≈ T * O * B * 0.4     (ZSTD 压缩 ≈ 0.4)

按 90 天留存 + 3 副本:
  ClickHouse 磁盘 ≈ 日增量 * 90 * 3
  S3 花费      ≈ 日增量 * 90(无副本, S3 自己冗余)

代入 T=10M, O=5, B=5KB:

也就是说,1000 万 trace/天的存储开销在 $300-500 区间,相比同量级的 LangSmith SaaS 每月几千美元便宜一个量级。这就是自建的经济理由。

常见慢查询排查

-- 最近 1 小时内跑得最慢的查询
SELECT
  query_duration_ms,
  substring(query, 1, 200) AS q,
  read_rows,
  memory_usage
FROM system.query_log
WHERE event_time > now() - INTERVAL 1 HOUR
  AND type = 'QueryFinish'
ORDER BY query_duration_ms DESC
LIMIT 20;

典型病因:

本章小结