日期:2026-09-04 | 机器:81.71.xx.xx(4C4G,Langfuse 专用机) 栈:Langfuse v4(web + worker 分离)+ ClickHouse 25.12 + Postgres/Redis/MinIO 本地配置源:F:\<user>\ai-daily\lighthouse\langfuse\ 关联文档:readme.md(🔧 ClickHouse CPU 打满一节)、技能 lighthouse-docker-ops T26/T27

摘要

2026-09-04,81.71.xx.xx 上的 Langfuse 栈 ClickHouse CPU 长期打满(实测 350%)。排查发现活跃 merge 全落在系统日志表而非业务表,err.log 记录 57,937 次 merge 失败。根因是 max_server_memory_usage 只给 700MB、低于真实工作集 1GB+,导致每次合并因内存不足失败并无限重试,失败又写 part_log 生成新 part,形成正反馈死循环。解法:内存上调至 1000MB、后台池 32→8、缓存收敛、关闭 5 张自监控日志表并清理历史垃圾 5.35GB。修复后 CPU 稳定 2~3%,磁盘回收至 81MB。核心教训:内存上限不可拍脑袋压低,ClickHouse 异常先看挂载出来的 err.log,docker logs 不全。


一、现象

用户报告两个问题:

  1. langfuse-web 容器持续显示 unhealthy,但页面实际能正常打开
  2. ClickHouse CPU 占满(用户侧观测约 99%)

重启栈之后实测,比报告的更严重:

docker stats(修复前)
langfuse-v2-clickhouse-1   CPU 350.28%   MEM 830MB / 1.75G
主机 load average: 7.40          ← 4 核机器,load 7.4 已严重过载
clickhouse 累计 CPU 时间 103 分钟(启动后不久)

注:用户看到的"99%“与实测 350% 不矛盾——前者多半是单核口径(htop 单核 100%), 后者是容器占满 3.5 个核。两者指向同一个事实:CPU 被吃满了。

同时 web 的 unhealthy 与 ClickHouse 问题相互独立(见第六节),本文主线是 CPU。


二、排查过程(按实际顺序)

第 1 步:活跃 merge 在忙什么

SELECT database, table, num_parts, progress, elapsed FROM system.merges;

结果出人意料:活跃 merge 全部落在 system.metric_log / system.part_log, Langfuse 业务表(langfuse.events_core / events_full)反而没有活跃 merge。 → 问题不是"业务数据量大”,方向转向系统自身。

第 2 步:内存水位对不上

  • 查询 system.parts(纯元数据表)直接报 MEMORY_LIMIT_EXCEEDED
  • 实测 RSS 在 820MB ~ 1.04GB 之间震荡,而硬上限 max_server_memory_usage 只有 700MB
  • 连元数据查询都被 OvercommitTracker 杀掉 → 服务端处于持续内存超限抖动状态

第 3 步:err.log 给出决定性证据

⚠️ 排查弯路:docker compose logs看不到这些错误。 全量错误在挂载出来的 clickhouse-server.err.log(volume langfuse-v2_clickhouse_logs)里。

Code: 241 (MEMORY_LIMIT_EXCEEDED) 累计 10 万+ 条
其中 merge 失败 57,937 次,堆栈全部指向 MergeTask → 内存分配失败

第 4 步:默认配置三宗罪

读容器内 /etc/clickhouse-server/config.xml

实际值问题
max_server_memory_usage700MB(历史遗留)远低于实际需要(1GB+)
uncompressed_cache_size默认 8GB机器总共 4G 内存,形同虚设
自监控日志表 ×4全开,7.5 秒 flush 一次metric_log/part_log/trace_log/text_log 高频写入
background_pool_size32(配 ratio 拉起 48 个 merge 线程4 核机器 48 线程互相抢内存

第 5 步:磁盘佐证

langfuse-v2_clickhouse_data 卷占 5.35GB——绝大部分是系统日志表 churn 出来的垃圾。


三、根因:一条"合并失败 → 重试"的正反馈死循环

max_server_memory_usage = 700MB(太小)
        ↓
每次后台 merge 在内存分配阶段失败(Code 241)
        ↓
失败自动重试,48 个线程全部空转 → CPU 350%
        ↓
每次失败还往 system.part_log 写一条事件、产生新 part
        ↓
新 part 触发新的 merge 需求 → 更多失败 → 更多日志 → 循环放大

三个关键认知:

  1. CPU 打满 ≠ 数据量大。业务表只有个位数 part,烧 CPU 的是系统日志表的死循环。
  2. 内存上限不是越保守越安全。给 ClickHouse 的内存低于其真实工作集,merge 永远做不完, 结果比内存紧张本身更糟——空转烧 CPU。
  3. 700MB 这个值是 09-03 OOM 事故的遗留(当时为防 OOM kill 压到 700MB)。 防住了 OOM,防不住"慢性自杀"。

四、解决办法

4.1 配置修正(clickhouse-config/memory.xml + compose)

原值新值目的
max_server_memory_usage700MB1000MB让 merge 能成功(治本)
compose mem_limit1280m1792m容器配额 > 服务端上限,留出余量
background_pool_size32(48 线程)8(16 线程)4 核机器砍掉无效并发
uncompressed_cache_size8G64MB缓存收敛,不再画饼
part_log / metric_log / trace_log / text_log / async_metric_log全开 7.5s全部关闭斩断正反馈环

ClickHouse 参数只能走 clickhouse-config/memory.xml(挂载到 config.d),不能用 command:。 config.d 的额外好处:配错了启动时报错,而不是静默不生效。

4.2 清理历史垃圾

关闭日志表配置后,把已禁用的旧系统日志表清掉:

磁盘:clickhouse_data 5.35GB → 81MB(根分区空闲 8.9G → 14G)
清理后业务表各只剩 1 个活跃 part,无排队 merge

4.3 过程中踩到的两个参数联动坑(差点以为改坏了)

报错原因处理
Code 115 启动失败background_merges_mutations_concurrency_ratio25.12 已被移除,写了直接不认识删掉;25.x 合并并发只由 background_pool_size 控制
Code 36 启动失败number_of_free_entries_in_pool_to_execute_mutation(默认 20) 必须 ≤ background_pool_size × ratio,池从 32 砍到 8 后校验不过同步下调到 8。这族 number_of_free_entries_in_pool_to_* 参数是联动的,调池必查

4.4 验证不止于健康端点

  • OTLP 端点 /api/public/otel/v1/traces:无凭据返回 401、假 key 也返回 401 → 写入链路与鉴权均通
  • system.metrics 确认新内存上限生效
  • 连续观察:35 分钟零新增 Error(期间仅有的 26 条是我自己的诊断脚本没带凭据造成的认证失败,非服务问题)

五、效果

指标修复前修复后
ClickHouse CPU350%2 ~ 3%(多次采样 2.16 / 2.26 / 2.59 / 3.39 / 8.08%)
ClickHouse 内存830MB(超限震荡)~550MB(稳定在上限 55%)
主机 load average7.400.05 ~ 0.28
err.log 新增错误57,937 次 merge 失败35 分钟零新增
磁盘占用5.35GB81MB
对外服务health=200,OTLP 401(鉴权正常)

worker 偶现 ~75% CPU 是每分钟一次的定时作业瞬时峰值(Event Propagation / Backfill),非持续占用,不用管。


六、同场加映:web unhealthy 的真相(另一条独立线)

用户原判断是"健康检查命令不合适"——对了一半:服务确实好,但换命令治不好。

证据链:

netstat:Next.js 只监听 172.18.xx.xx:3000(容器自身 IP),没监听 127.0.0.1
printenv HOSTNAME = de25e9ef3e1a(Docker 注入的容器 ID)

Next.js standalone 启动时直接把 HOSTNAME 当绑定地址,而 Docker 把 HOSTNAME 设成了容器 ID。 于是:端口映射走 docker-proxy 转发到容器 IP,对外完全正常;容器内访问 127.0.0.1 必然 Connection refused → 健康检查恒失败。

修复(两处缺一不可):

  1. web 环境变量显式设 HOSTNAME: "0.0.0.0"
  2. 健康检查用 127.0.0.1 而非 localhost——镜像内 Node 24 对 localhost 会先试 ::1(IPv6),服务只听 IPv4,照样失败

七、经验与防再发清单

根因层

  • 给 ClickHouse 定内存上限前,先看真实工作集(RSS),上限 ≥ 工作集 + 余量;宁可给够,不可拍脑袋压低
  • 小内存机器(≤4G)部署 ClickHouse:主动关闭自监控日志表(part_log/metric_log/trace_log/text_log/async_metric_log),默认配置是给大机器写的
  • 缓存类参数(*_cache_size)按机器内存比例设,默认值多为大机器口径(uncompressed_cache 默认 8G)
  • background_pool_size 必查 number_of_free_entries_in_pool_to_* 一族联动,且确认所用版本的参数仍存在(25.12 已移除 concurrency_ratio)

排查层

  • ClickHouse 异常先看挂载出来的 err.logdocker logs 只有 stdout,不全
  • CPU 打满先查 system.merges + system.processes,分清"业务负载"还是"系统内耗"
  • 出现大量 Code 241 + merge 失败 → 直接怀疑内存上限过低,不是数据问题
  • 健康端点绿 ≠ 可用:验收必须做端到端探针(本例 OTLP 401 即链路通)

监控层

  • 部署后必查 dmesg(OOM kill 是静默的);本例无 OOM,是"慢性病"而非"猝死",更依赖错误日志巡检

附:本次产物与备份

位置
生效配置服务器 /home/ubuntu/<user>/langfuse-v2/clickhouse-config/memory.xml
修改前备份同目录 memory.xml.bak.20260904-*docker-compose.yaml.bak.20260904-*
本地配置源F:\<user>\ai-daily\lighthouse\langfuse\(compose + memory.xml + readme)
经验沉淀技能 lighthouse-docker-ops(T25 / T26 / T27 + security-baseline)