查询很慢,但所有指标都是绿的

一个所有指标都正常的慢查询

那张表 4.0 TB,55 亿行,3727 列,存算分离,数据在对象存储上,八个 BE 每个挂 1.3 TB 本地 NVMe 做 file cache。一条最朴素的点查,按业务事件 ID 取十行,跑了 144 秒。

第一反应当然是对象存储慢。于是我把同一个 ID 反复查,让缓存暖起来。暖之后仍然要 1.12 秒。这一次的现场是:缓存全命中,远程读次数为零,CPU 不高,内存不高,对象存储带宽用了不到 1%。

手上那套以主机资源为中心的 dashboard,在这个故障面前一片绿。

真因跟存储没有关系。事件 ID 既不是分桶键也不是分区键,所以一次点查要探遍全部 566 个 tablet、6689 个 segment,时间全花在打开这些倒排索引的 searcher 上。对象存储只是冷路径上叠加的一个乘数,扇出才是地板。

把 144 秒拆开更清楚:扇出占 114 秒,真正的冷列读占 30 秒。如果只带上分桶键把范围剪到单个 tablet,仍然要 30 秒,因为 3727 列的宽表 SELECT * 有独立的列读放大,剪枝治不了它。同一个 ID 冷读 31.8 秒,暖读 0.46 秒,缓存能把列读压成亚秒。剪枝加上只取五列,0.55 秒。

两个成本正交,所以要两个正交的动作:剪枝去扇出,暖缓存或行存去列读放大。

最后的修复不涉及任何资源调整。事件 ID 本身可以解析出用户 ID 和毫秒时间戳,也就是说分桶键和分区键一直躺在查询条件里面,只是没人把它们拆出来。在查询层注入这两个字段,扫描范围从 566 个 tablet 收敛到 1 个,冷热两条路径同时受益。

决定这条查询下限的是它要碰多少个数据块,跟集群还剩多少余量没有关系。而"要碰多少个数据块",恰恰是主机资源监控测不到的东西。

那该测什么?下面是我后来整理出来的顺序,按 oncall 时真正会问的问题排列。

第一分钟:我承诺的东西坏了没有

数据库对外提供的是 SQL 服务,最顶上的指标应该是这个服务的契约,不是 CPU。这一步多数团队都知道要做,但落地时容易照抄在线服务的模板。

单一 P99 对分析型数据库不成立。点查是毫秒级,固定报表几十秒,adhoc 全表几分钟,把它们混进一个分位数,得到的数字在该告警的时候不响,在业务只是换了个报表的时候乱抖。可用的做法有两条:按 workload class 分桶,各自定阈值;对 adhoc 这种没法预先分类的,用查询指纹相对自身历史基线的劣化倍数。

口径上还有个常见的错位:SLI 应该是"阈值内完成的请求数除以有效请求数",分位数只是观测视角,把 quantile 直接当 SLI 本体会让后面的 error budget 算不出来。

我漏得最久的是第三组指标。这类系统同时是请求型服务、数据管道和存储系统,导入侧延迟或者部分失败的时候,查询照样成功返回,延迟也正常,返回的是旧数据或者缺了一段的数据。可用性和延迟全绿,业务已经在错的数据上做决策了。这种故障没有红灯,只有独立的新鲜度和完整性指标能抓到:端到端导入延迟、分区应到未到、导入失败率。

第五分钟:是你的问题还是我的问题

这是从 oncall 学来的最有用的一个动作。故障发生时第一个要收敛的不是根因,是责任方向:需求侧变了,还是供给侧变了。方向定错,接下来一小时全是浪费。

这件事可以用数据直接判定,前提是两件基础设施。一是 FE 的 audit log 落成可以写 SQL 查的表,它是 per-query 的,带扫描行数、扫描字节、CPU 时间、峰值内存、执行状态、用户和库名。二是 SQL 指纹归一化,把参数抹掉之后取哈希。

有了这两样,判定规则很短:

  • 同一指纹,扫描量没变,延迟变了,指向供给侧:集群资源、后台任务抢占、缓存失效、硬件降级
  • 出现新指纹,或者旧指纹的扫描字节暴涨,指向需求侧:业务上线、数据量增长、剪枝条件失效
  • 所有指纹一起劣化,指向全局过载或者供给侧
  • 单一指纹劣化而其余正常,指向这条查询自身,或者它命中的那部分数据分布变了

指纹归一化是前提而不是优化项。没有它,所谓的 Top Slow SQL 每次都是一段不同的文本,既没法聚合,也没法看趋势,更没法判断这次慢是不是一次回归。

开头那个案例按这套规则一眼就能定方向:同一条 SQL,扫描量一直就这么大,扇出一直就是全表级,集群侧什么都没变。所以正确动作是改查询,加资源属于走错方向。

我们确实先走错过。加 BE、调大 scan 线程池,对这类点查完全无效,因为 LIMIT 会把并发压到 1,最终落在单 tablet 单 BE 上,多出来的机器一台都用不上。remote 线程池还不能随手往大调,那个版本上有个池子过大导致 BE 冻住的缺陷。最顺手的旋钮通常是错的,这跟上次那台 ClickHouse OOM 里"把 pod 内存调大"是同一类错误。

第一小时:还能扛多久,扛不住怎么退

容量规划的直觉误区是把它当加法。真实约束来自排队论:等待时间随利用率按 ρ/(1-ρ) 放大,利用率从 0.5 涨到 0.9,排队时间涨九倍,从 0.9 再涨到 0.95,又翻一倍。

所以要权衡的三样是利用率、延迟、余量,三选二。想省钱就是把利用率往上推,代价是 P99 的非线性劣化和突发时没有缓冲。这是数学的结论,跟工程水平无关。按平均利用率做的容量规划一定会在某个峰值上翻车。

跟容量配套的那一半是隔离和降级,我最初那版清单里也漏掉了。一张 BI 看板打满整个集群,在分析型数据库上属于常态。可用的手段包括 workload group 的 CPU 内存软硬限、让查询排队(优于直接拒绝)、单查询内存上限、大查询熔断。

没有降级方案的容量规划是赌博。容量总有被突破的那天,区别只在于突破时是有序退化还是雪崩。

再往后:改它和修它是同一件事

变更管理和灾难恢复通常被写成两份清单,其实是一件事的两面:系统状态发生了非预期的跳变,怎么收敛回去。

数据库变更的特殊性在于它的危险动作都是长时间运行的后台任务,并且跟在线查询抢同一份 CPU、IO 和内存。schema change 触发的数据重写、tablet 再均衡、BE 下线、compaction 策略调整、升级过程中的元数据兼容,全都属于这一类。所以变更可观测性的核心是后台任务的进度和资源占用可见:还剩多少没搬完、现在吃掉多少 IO、预计多久结束、能不能暂停、能不能限速。

在线服务那套灰度加回滚,到数据库这里经常不成立,因为很多变更根本没有回滚路径。能暂停和能限速比能回滚更现实。

恢复侧只有一条要说:备份存在不等于能恢复。FE 的元数据是整个集群最致命的单点,BE 数据一份不少,元数据丢了集群等于不存在。RTO 和 RPO 写在文档里没有意义,得定期真跑一遍并且计时。

这里还有一次我自己的判断失误值得记下来。调查过程中我认定 file cache 在重启后会丢,据此提了一个升级方案去换持久化缓存。现场核实的结果是缓存盘本来就是持久卷,缓存元数据也有 dump 和 replay,实测重启后从 2% 自暖到 61%,那个升级理由整个不成立。推断和现场核实之间的差距,往往就是一个季度的工作量。

资源指标应该待在哪里

回到 CPU 和内存。它们作为告警源价值低,因为跟用户痛感的相关性太弱,开头那个案例就是最好的反例。它们真正的位置在诊断侧和容量输入侧,那里价值很高:判断供给侧是否劣化、算容量模型、做事后归因,全都要用。

告警看症状,诊断看原因,这是两条不同的指标链路。我见过读完 SRE 那套方法论之后把资源 dashboard 全部删掉的做法,结果是真出事的时候手上没有证据。

还有一件双方都容易忘的事:监控体系本身要有基数预算。tablet 粒度的指标在这类系统上会直接把 TSDB 打垮,一张表就有 566 个 tablet,一个生产集群几十万个是常态。

收尾

现在我看一套数据库监控体系,先问一个问题:真出事的时候,它能不能在五分钟内告诉我,该去找写 SQL 的那个人,还是该去找管集群的那个人。这个问题答不上来,指标再多也只是装饰。