OLAP 数据库混进 OLTP 链路:一次 ClickHouse 拖死 API 101 分钟的复盘

发布时间:2026/7/30 5:31:33
OLAP 数据库混进 OLTP 链路:一次 ClickHouse 拖死 API 101 分钟的复盘
摘要:OLAP 数据库 ClickHouse 被试验性接入 OLTP 主链路,一次普通发版将其引爆:CK 磁盘满且无告警导致查询 hang,Spring Boot /actuator/health 每秒 480 次心跳逐个 ping 依赖借走 CK 连接,HikariCP 连接池 10 连接被 374 线程争抢耗尽,TCP 连接数从 5k 飙到 22k,后端服务间断不可用 101 分钟。arthas threadvmtool 5 分钟锁定 CK 连接池,根因是 OLAP 不该进 OLTP 主链路。某天晚上发版,约半小时后客服群炸了——支付不上、登录转圈、商详打不开。折腾 101 分钟才恢复服务。事后追凶,所有人都没想到罪魁居然是几个月前试验性加进后端服务的一个 ClickHouse 连接——加完就没人再动它,一个普通发版当晚就把它点燃了。这篇文章把这次OLAP 数据库混入 OLTP 主链路的连锁反应讲清楚,顺便聊聊哪些场景不该连 ClickHouse。一、问题现象项值故障时间某天晚上;首个异常信号 3min(Sentry 报错),30min 才真正引爆总时长约 101 分钟影响范围后端服务间断不可用(不是全挂,是抽风式 502)直接资损略(估损方案见下)触发时机发版后约 30 分钟引爆怎么估的这笔损失(方法比数字重要):支付系统读不出因这次故障少赚了多少,于是用基线对比法——拿故障前近 7 天同时段(晚间 2 小时)支付成功的均值当基线,故障当晚相对基线的缺口,就是这次的直接损失。⚠️这次真正的杀伤是「101 分钟间断不可用」:所有依赖这个后端服务的功能都在抽风。而且这只是个普通夜晚——要是撞上大促,就是另一个量级的事故。一个没人再看一眼的 OLAP 连接,就把 OLTP 主链路拖到了离大事故只差一步的地方。二、火药桶链路:跨度 5 个月的伏笔这次事故最让人后背发凉的是——5 个月之前埋的雷,在一次毫无关联的发版里被精准引爆。把时间线倒回去看:5 个月前 搭建 ClickHouse,忘了挂磁盘告警 引信 #1 约 4 个月前 上线埋点接口 /collect 给业务方 A 用, 但忘了配 APISIX 网关映射 引信 #2 约 4 个月前 admin(后台管理)加了 ClickHouse 依赖 约 3 个月前 后端服务(C 端 OLTP 主接口)试验性加 CK 依赖 引信 #3 ← 最关键 理由:想试一下能不能用,反正不调 发版前 3 天 某 H5 渠道发版,会调用 /collect → 返回 404 → 触发 APISIX 健康检查 /** 火苗 #1 发版前 1 天 后端服务 Network.IN 报警上升,无人在意 发版当天 发版 关闭心跳(本意降负载) → 反触发大量心跳(APISIX 热加载 bug) 火苗 #2 发版当天30min CK 磁盘满,无告警 引爆 发版当天30min TCP 连接数 5k → 22k 服务卡死 发版当天101min 定位 CK 磁盘满 → 扩容 CK 磁盘 ✅ 恢复每一步单看都是小事,但串起来就是连锁反应。看图谱:5 个月前搭 CK,缺磁盘告警约 4 个月前 admin 加 CK 依赖约 3 个月前 后端服务 试验性 加 CK 依赖约 4 个月前上线 /collect,忘了配网关发版前3天 H5 调 /collect → 404APISIX 兜底回源触发健康检查 /**发版前1天 后端服务 Network.IN 报警发版当天30min CK 磁盘满 (无告警)发版当天 关心跳APISIX 热加载 bug 反触发心跳/actuator/health 480 req/s心跳触发 CK 健康检查HikariPool-CK 等待 374,连接 10TCP 5k → 22k,新连接建不出 后端服务间断不可用 101 分钟三、排查现场:从看不出来到arthas 一锤定音3.1 第一波:Redis 类型转换异常(假线索)发版3min Sentry 收到 7 次: Unknown redis exception; nested exception is java.lang.NumberFormatException: For input string: ... ClassCastException: java.lang.Long cannot be cast to [B值班大哥第一反应:发版导致 Redis 序列化挂了,准备回滚。但代码 review 过、SkyWalking 链路里看不到这些异常的堆栈——这条线索是真问题但不是主因(后来确认 Redis 那边有少量类转换的脏数据需要修,但跟卡死无关)。排查方向被带歪了大约 15 分钟。3.2 第二波:回滚 重启 再回滚(无效循环)发版34min 电话联系开发回滚 发版37min 第一次回滚代码 发版41min 第二次回滚 重启后端服务 发版49min 第三次回滚 重启后端服务 发版72min 第四次回滚 重启后端服务 发版82min 第五次回滚 重启后端服务 发版90min 第六次回滚 重启后端服务回滚 6 次,每次都是几分钟好转,然后又卡死。事后复盘才发现,这一波从一开始就回滚错了对象:大家回滚的是这次发版的代码,可真凶那行 CK 依赖压根不是这次发版引入的——它是上千次提交之前试验性加进来的,想回滚到没有它的版本,等于把这几个月所有功能一起撤掉,根本不可能;这次发版的回滚更是碰都碰不到它。回滚 重启唯一的作用,只是把 TCP 连接和健康检查短暂清零,于是每次都假好转几分钟,然后又从干净状态一路堆到耗尽。真凶(CK 磁盘满)没动过,症状自然反复。3.3 第三波:arthas 抓现场,5 分钟锁定真凶发版80min 决定用 arthas:java-jararthas-boot.jarthread命令一打:Threads Total: 642, NEW: 0, RUNNABLE: 72, BLOCKED: 0, WAITING: 101, TIMED_WAITING: 457, TERMINATED: 0, Internal: 12457 个线程 TIMED_WAITING 101 个 WAITING——80% 的线程在等什么东西。挑一个高 CPU 的 http-nio 线程看堆栈,马上看到:at com.zaxxer.hikari.util.ConcurrentBag.borrow(ConcurrentBag.java:157) at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:173)HikariCP 在排队等连接。下一步用vmtool看具体哪个池有问题:vmtool--actiongetInstances--classNamecom.zaxxer.hikari.pool.HikariPool-x1# 返回 3 个池: HikariPool-1 / HikariPool-2 / HikariPool-3逐个看池状态:HikariPool-3: 连接数: 10 等待线程: 374 ← 罪魁 dataSource: jdbc:clickhouse://10.x.x.x:8123/ods——HikariPool-3 就是 ClickHouse 连接池,10 个连接全占用,374 个线程在排队。10 vs 374,1:37 的供需比——这池子已经废了。3.4 同时另一条线:网关 TCP 监控发现异常发版95min 运维同学看 grafana TCP 监控:TCP_alloc (已分配 socket): 5k → 22k (4 倍!) TCP_tw (TIME_WAIT): 几乎与 alloc 同步上涨后端服务器的 TCP 连接数被打爆,新连接进不来——这就是间断不可用的直接原因(不是后端进程挂了,是网络层先饱和了)。3.5 发版101min:真相发版101min 运维终于发现ClickHouse 磁盘在发版30min 就满了:df -h /data/clickhouse /dev/vdb 1.0T 1.0T 0 100% /data/clickhouse且CK 磁盘一直没有磁盘告警:5 个月前搭的时候漏配了云盘磁盘告警;虽然也有 Prometheus、磁盘指标能采到,但同样是历史遗留、始终没配告警规则。两套监控都在,却都没对磁盘满吭一声。到这里链条对上了:发版30min CK 磁盘满 → CK 查询 hang后端服务健康检查每秒去 ping CK(/actuator/health默认会 ping 所有依赖)CK hang → HikariPool-CK 连接被占住 → 10 连接全满后续请求排队 → 374 线程等连接 → TCP 雪崩四、根因分析:三个独立的温问题叠加成沸水#温问题单独是否致命跟其他的耦合1后端服务试验性接入 ClickHouse否(平时不调用) 2 → 一接入心跳就活跃2/actuator/health默认 ping 所有依赖否(只是慢一点) 1 → ping CK 占用连接3ClickHouse 磁盘满 无监控告警否(CK 平时也能熬过去) 1、2 → CK hang 直接卡死后端服务任意两个都不致命,三个凑齐 101 分钟卡死。下面挨个拆。4.1 最大根因:OLAP 数据库不该出现在 OLTP 主链路这次最痛的教训。先把 OLTP 和 OLAP 摆出来对比:维度OLTP(C 端业务 API)OLAP(ClickHouse 的主场)并发模型几万 QPS,每条 50ms几十并发,每条扫几千万行响应时间 SLAP99 100msP95 秒级可接受连接占用短平快(几十毫秒)长占用(秒级 资源大)写入模式频繁单行 CRUD大批量 append,极少更新单 SQL 资源多并发分摊一条能吃满 CPU适合的连接池几百~几千几十封顶健康检查友好度心跳无感每次心跳都是开销结论: OLTP 在线主链路里永远不要直连 ClickHouse。哪怕你只是试验性加个依赖,只要spring.datasource.clickhouse配进去了,health check 就会去 ping 它——你不调用业务,Spring 也帮你调用。可以连 CK 的在线场景(仍要小心):BI 报表 / 用户行为分析(用户接受秒级响应)异步任务、定时统计、月报后台 admin 管理系统(并发低,响应宽松)服务端预聚合后供 API 读(此时 CK 不在请求链路上)不能连 CK:主 OLTP 链路上的高并发接口(本次后端服务就是这类)健康检查 / 心跳路径(致命!)任何要求 P99 200ms 的接口4.2 次要根因:/actuator/health是个昂贵的接口Spring Boot Actuator 的/actuator/health默认配置下,每次调用都会去 ping 所有注册的组件:DataSource (MySQL / PostgreSQL / ClickHouse / …)RedisMongoDBKafkaElasticsearchDiskspace…健康检查链路:APISIX → /actuator/health → Spring 遍历 HealthIndicator → ├── DataSource (MySQL) ping ├── DataSource (CK) ping ← 借走一个 CK 连接 ├── Redis ping └── ... 全部串行在本次事故里,APISIX 每秒打过来480 req/s的/actuator/health,其中 480 个都会去借 CK 连接——连接池 10 个,瞬间用完。故障当晚 Grafana QPS-Top10:/actuator/health稳定压在480 req/s,健康检查风暴一图坐实。这是一个国内深度博客很少讲的反模式,我后面专门写了一篇 【待发布后补充引用关系《Spring Boot 的 /actuator/health 不是免费的:健康检查打爆连接池的反面教材》】 详细讲怎么避坑。4.3 触发因素:APISIX 健康检查/**的误放大APISIX 配了兜底回源,当某个路径返回 404 时会触发健康检查/**。发版前 3 天 H5 渠道发版后开始调/collect,但这个接口没配网关映射→ 404 → 触发/**健康检查 → 每秒几百次打到/actuator/health。CK 侧ProfileEvent_Query从发版前 3 天(/collect开始 404)逐日爬升到 ~500 req/s——放大效应不是一夜爆发,而是慢慢累积到临界。这本是一个温问题,但发版当天关心跳(APISIX 热加载 bug) CK 磁盘满后,瞬间从温变沸。4.4 隐藏根因:CK 磁盘 5 个月没接监控CK 是 5 个月前搭的,搭的时候漏了挂云盘磁盘告警。跑了 5 个月,磁盘从 60% 涨到 95% 也没人知道,发版当晚跨过 100% 才被发现。ClickHouse 数据增长: 约 3 个月前 60% 占用 约 2 个月前 75% 约 1 个月前 90% 发版前 3 天 95% (3 天涨了 5%,跟某个新埋点表有关) 发版当天30min 100% ← 引爆磁盘从平日的 ~80% 一路爬升,发版当天 20:30 触顶100%(右上角容量% 99.9%),随后扩容才回落——「引爆」二字看图一目了然。五、解决方案5.1 紧急止血(发版当晚)时点动作发版101min定位到 CK 磁盘满 →给 CK 扩容磁盘,查询解除 hang,后端服务随之恢复发版105min验证服务启动正常发版117min验证各业务功能全部恢复⚠️ 注意:扩容只是止血,不是治本。它让 CK 别再 hang、把后端服务从耗尽里捞出来;但只要 CK 还挂在 OLTP 主链路上,下次磁盘满 / CK 抖动照样会重演。真正的根治是次日把 CK 依赖从主链路里拔掉(见 5.2)。至于回滚——上面说过,真凶 CK 是上千次提交前加的,回滚这次发版根本够不着它。5.2 短期治本(72 小时内)1. 业务剥离 ClickHouse(P0,发版次日上午)- spring: - datasource: - clickhouse: - jdbc-url: jdbc:clickhouse://... # OLTP 服务不再依赖 ClickHouse效果立竿见影:发版次日下午 上线 后端服务移除 ClickHouse 配置后: Clickhouse QPS: 几百 → 0 CPU Load: 持续下降发版次日下午上线移除 CK配置后,健康检查 QPS 从 ~500直接归零,CPU 随之回落——OLAP 一从主链路里拔掉,世界瞬间清净。2. 给所有云服务器挂上磁盘告警-alert:服务器磁盘使用率过高expr:(1-node_filesystem_avail_bytes / node_filesystem_size_bytes) * 10085for:5mlabels:severity:warning3. APISIX 网关:补齐网关映射 关掉兜底回源# 之前: /** 兜底,任何 404 都触发健康检查# 改后: 显式路由,匹配不到的直接返回 404,不再放大4./actuator/health改成轻量探活management:health:db:enabled:false# 禁用 DB 健康检查diskspace:enabled:falseredis:enabled:falseclickhouse:enabled:false更激进一点是自己写一个空接口替代/actuator/health,因为我们的场景是探活,不是健康检查。详见专题 【待发布后补充引用关系《Spring Boot 的 /actuator/health 不是免费的》】。5.3 中长期治理#措施负责方状态1主线 OLTP 服务永久剥离 OLAP 数据库后端✅ 已落2所有云主机磁盘告警检查清单运维⏳ 进行中3健康检查 vs 探活检查的规范文档架构组⏳4APISIX 网关映射巡检(每月)运维⏳5上线最佳时间窗调整(避开高峰)全员✅ 已立规矩6应急预案文档落地 桌面演练后端 运维⏳六、举一反三:5 条值得带走的经验经验 1: 试验性代码不是没有成本“我加了个连接,但又不调用,应该没影响吧?”——错。只要 DataSource bean 注入了,默认的 health indicator 就会用它,Prometheus 也会去采它的指标,连接就会被借走。“代码进了 Spring 容器,就不是免费的。”经验 2: 健康检查 ≠ 探活检查名称用途应该检查啥探活检查(liveness)进程还活着吗?TCP 端口 简单字段返回,不查依赖就绪检查(readiness)进程能服务请求吗?关键依赖可用(注意:关键≠ 全部)健康检查(health)给运维 / 监控看的诊断信息各组件状态明细把这三个混在一起,就是/actuator/health反模式的源头。经验 3: 慢问题 慢问题 快问题§四那四瓢温水(连 CK 不用 / 磁盘缓慢满 / health 全检 / 健康检查放大),单看每一瓢都不致命。真正要命的是:监控只在沸水那一刻才报警,温水阶段仪表盘一片绿——等它报,人已经在群里挨骂了。对策: 周期性盘点温水清单,主动做架构隔离 / 降级 / 监控补齐,别等它自己烧开。经验 4: OLAP 跟 OLTP 之间必须做物理隔离不仅是OLTP 不连 OLAP——更进一步:不同环境(生产 / 测试)隔离不同业务等级(C 端在线 / 后台管理)隔离不同模式(审核 / 非审核)隔离不同读写(OLAP read / OLTP write)隔离判定标准很简单: 如果 A 系统挂了 B 系统跟着挂,就说明它俩没隔离干净。经验 5: 监控覆盖率盘点要定期做CK 磁盘 5 个月无告警这件事,本质是搭新中间件的监控接入没纳入上线 checklist。建议每季度做一次监控覆盖率盘点:- 所有云主机 → 是否有 CPU / Mem / Disk / Network 告警 - 所有中间件 → 是否有应用级监控 - 所有 DataSource → 是否有连接池监控 - 所有外部依赖 → 是否有调用成功率告警七、延伸阅读跟本文配套的两篇专题:专题:【待发布后补充引用关系《Spring Boot 的 /actuator/health 不是免费的:健康检查打爆连接池的反面教材》】 — 把 health 反模式讲透专题:【待发布后补充引用关系《arthas 5 分钟揪出 HikariCP 耗尽:从 thread 到 vmtool 的实战排查》】 — 同款排查工具,5 分钟从症状到根因同主题连接池耗尽的另一种姿势(根因不同的同款现象):《AI 服务 502 雪崩排查:从 Nginx 超时到连接池耗尽,查了两次才找到真凶》 — 根因是 yaml 配置不生效 事务内调大模型同系列排查实战组合拳:《CPU 占用高排查实战:从 top 到火焰图,一套组合拳搞定》《MongoDB 主从切换排查实战:从 docker ps 到 jq,一套 SOP 定位死因》️ 标签:ClickHouseOLTP/OLAP线上排障HikariCPSpring Boot架构隔离