日期: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-opsT26/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 不全。
一、现象
用户报告两个问题:
langfuse-web容器持续显示 unhealthy,但页面实际能正常打开- 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(volumelangfuse-v2_clickhouse_logs)里。
Code: 241 (MEMORY_LIMIT_EXCEEDED) 累计 10 万+ 条
其中 merge 失败 57,937 次,堆栈全部指向 MergeTask → 内存分配失败
第 4 步:默认配置三宗罪
读容器内 /etc/clickhouse-server/config.xml:
| 项 | 实际值 | 问题 |
|---|---|---|
max_server_memory_usage | 700MB(历史遗留) | 远低于实际需要(1GB+) |
uncompressed_cache_size | 默认 8GB | 机器总共 4G 内存,形同虚设 |
| 自监控日志表 ×4 | 全开,7.5 秒 flush 一次 | metric_log/part_log/trace_log/text_log 高频写入 |
background_pool_size | 32(配 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 需求 → 更多失败 → 更多日志 → 循环放大
三个关键认知:
- CPU 打满 ≠ 数据量大。业务表只有个位数 part,烧 CPU 的是系统日志表的死循环。
- 内存上限不是越保守越安全。给 ClickHouse 的内存低于其真实工作集,merge 永远做不完, 结果比内存紧张本身更糟——空转烧 CPU。
- 700MB 这个值是 09-03 OOM 事故的遗留(当时为防 OOM kill 压到 700MB)。 防住了 OOM,防不住"慢性自杀"。
四、解决办法
4.1 配置修正(clickhouse-config/memory.xml + compose)
| 项 | 原值 | 新值 | 目的 |
|---|---|---|---|
max_server_memory_usage | 700MB | 1000MB | 让 merge 能成功(治本) |
compose mem_limit | 1280m | 1792m | 容器配额 > 服务端上限,留出余量 |
background_pool_size | 32(48 线程) | 8(16 线程) | 4 核机器砍掉无效并发 |
uncompressed_cache_size | 8G | 64MB | 缓存收敛,不再画饼 |
| 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_ratio 在 25.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 CPU | 350% | 2 ~ 3%(多次采样 2.16 / 2.26 / 2.59 / 3.39 / 8.08%) |
| ClickHouse 内存 | 830MB(超限震荡) | ~550MB(稳定在上限 55%) |
| 主机 load average | 7.40 | 0.05 ~ 0.28 |
| err.log 新增错误 | 57,937 次 merge 失败 | 35 分钟零新增 |
| 磁盘占用 | 5.35GB | 81MB |
| 对外服务 | — | 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 → 健康检查恒失败。
修复(两处缺一不可):
- web 环境变量显式设
HOSTNAME: "0.0.0.0" - 健康检查用
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.log,
docker 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) |