📝 摘要:OLAP 数据库 ClickHouse 被"试验性"接入 OLTP 主链路,一次普通发版将其引爆:CK 磁盘满且无告警导致查询 hang,Spring Boot /actuator/health 每秒 480 次心跳逐个 ping 依赖借走 CK 连接,HikariCP 连接池 10 连接被 374 线程争抢耗尽,TCP 连接数从 5k 飙到 22k,后端服务间断不可用 101 分钟。arthas thread+vmtool 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 磁盘 ✅ 恢复每一步单看都是"小事",但串起来就是连锁反应。看图谱:
三、排查现场:从"看不出来"到"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 占用连接 |
| 3 | ClickHouse 磁盘满 + 无监控告警 | 否(CK 平时也能熬过去) | + 1、2 → CK hang 直接卡死后端服务 |
任意两个都不致命,三个凑齐 = 101 分钟卡死。
下面挨个拆。
4.1 最大根因:OLAP 数据库不该出现在 OLTP 主链路
这次最痛的教训。先把 OLTP 和 OLAP 摆出来对比:
| 维度 | OLTP(C 端业务 API) | OLAP(ClickHouse 的主场) |
|---|---|---|
| 并发模型 | 几万 QPS,每条 < 50ms | 几十并发,每条扫几千万行 |
| 响应时间 SLA | P99 < 100ms | P95 秒级可接受 |
| 连接占用 | 短平快(几十毫秒) | 长占用(秒级 + 资源大) |
| 写入模式 | 频繁单行 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 / …)
- Redis
- MongoDB
- Kafka
- Elasticsearch
- Diskspace
- …
健康检查链路:
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) * 100>85for: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 探活检查的规范文档 | 架构组 | ⏳ |
| 4 | APISIX 网关映射巡检(每月) | 运维 | ⏳ |
| 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架构隔离