从一次“神不知鬼不觉”的小抖动说起。某个周三下午,业务群里突然有人反馈“后台导出报表特别慢,转圈半天没反应”,紧接着客服那边说“用户下单提交一直转菊花”。第一反应当然是先看一眼监控,结果Prometheus上MySQL连接数曲线直接拉满,Grafana面板一片飘红,连接池使用率接近100%。说实话,看到这个画面心里反而踏实了一些——至少故障是“看得见”的,而不是那种连日志都没有、只能靠猜的悬案。这篇文章我就用这次故障作为完整案例,把“监控发现异常 → 日志定位线索 → 排查根因 → 恢复验证”整条链路串起来,讲讲我在实际排障中是怎么操作的,监控该怎么配、日志该怎么挖、排查命令该怎么用,希望能给正在做运维或后端开发的同路人一些能直接用上的思路。

1. 监控先行:先有“眼睛”,才有排障的底气

1.1 从Prometheus到Grafana,监控体系是怎么搭出来的

这次故障能在一开始就被发现,靠的不是运气,而是之前花时间搭好的一套监控体系。我们用Prometheus做指标采集,Grafana做可视化展示,Alertmanager负责告警推送。架构上其实非常简单,数据链路大概是:各类exporter(mysqld_exporter、node_exporter、redis_exporter等)暴露/metrics接口,Prometheus定时拉取,存进时序数据库,Grafana从Prometheus查询数据画面板,再配上告警规则。

具体到MySQL的监控,我建议至少要盯这几个核心指标:连接数(threads_connected)、活跃连接数(threads_running)、慢查询数量(slow_queries)、QPS/TPS、缓存命中率、临时表创建量、以及InnoDB的行锁等待次数。我这次就是靠连接数曲线发现问题——正常情况下连接数稳定在30到50之间,故障发生时直接冲到了400多,而且threads_running长期大于20,说明大量SQL在真正地执行而不是空闲等待,这已经是比较危险的信号。

Grafana面板我习惯按业务维度分层:第一层是总览视图(所有核心服务的黄金指标),第二层是某个服务的详细指标,第三层是数据库、缓存这类中间件的深度指标。理论上告警只需要在第一层配,让你“知道出事了”,剩下的交给登录服务器去看。这里有个特别重要的教训: 告警不是越多越好,规则太敏感会让人麻木,最终变成“狼来了” 。我们后来把所有告警收敛成三类:可用性告警(进程挂没挂)、性能告警(响应时间、连接数超过阈值)、容量告警(磁盘使用率、连接池水位)。

1.2 监控指标怎么选才不“裸奔”

很多团队搭监控只盯着CPU和内存,说实话这远远不够。以MySQL为例,如果你只看了CPU使用率,故障发生时你可能会看到CPU到了80%以上,但你不知道是哪个SQL导致的、是连接数暴涨导致的还是全表扫描导致的。我的经验是: 数据库监控要纵向打通 ,从操作系统层(CPU、内存、磁盘IO、网络)、数据库层(连接数、慢查询、锁等待、buffer pool命中率)一直看到业务层(核心接口的P99延迟、错误率)。

这里分享一个我自己的选择标准:每个组件至少配4个“黄金指标”,即延迟(Latency)、流量(Traffic)、错误(Errors)、饱和度(Saturation)。比如MySQL的黄金指标可以这样对应:

指标维度 具体监控项 告警阈值参考
延迟 慢查询数量、SQL平均执行时间 慢查询数 > 5/分钟 或 平均执行时间 > 1s
流量 QPS、TPS、连接数 QPS突增50%以上
错误 主从延迟、复制错误、死锁数 主从延迟 > 10s 或 复制线程停止
饱和度 threads_running、磁盘使用率、连接池使用率 threads_running > 20 持续5分钟,磁盘 > 85%

另外强烈建议把告警信息推到企业微信或者钉钉机器人上,这样不用一直盯着屏幕也能第一时间收到通知。Alertmanager的配置不复杂,核心就是配一个webhook地址,告警规则用PromQL表达式去描述,比如MySQL连接数过高就可以写成:

groups:
  - name: mysql_alerts
    rules:
      - alert: MySQL连接数过高
        expr: mysql_global_status_threads_connected > 300
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "MySQL连接数超过300,当前值 {{ $value }}"

2. 日志挖坑:从慢查询日志到应用日志的全链路追踪

2.1 从慢查询日志找出“罪魁祸首”

监控只能告诉我们“系统出问题了”,但具体是哪个请求、哪条SQL导致的问题,必须靠日志来定位。这次故障排查中,我打开MySQL慢查询日志(日志目录在/var/log/mysql/mysql-slow.log),用 mysqldumpslow 工具做了个排序分析,很快就发现一条之前从没注意过的SQL:

SELECT * FROM orders 
WHERE user_id = 12345 
ORDER BY created_at DESC 
LIMIT 10 OFFSET 20000;

这个查询本身逻辑很简单,就是“查某个用户的历史订单分页数据”,但问题在于 ORDER BY created_at DESC 走了文件排序(filesort),而且因为 user_id 虽然有索引,但排序字段不在索引里,MySQL只能先把所有符合条件的行找出来,再在内存或磁盘上排序,最后才取10条返回。数据量小的时候感觉不到,等订单表积累到几百万行之后,这个操作瞬间变成噩梦。

为什么慢查询日志如此重要? 因为它是数据库侧的“第一现场”。没有它,你只看应用日志只能看到“超时”“连接池耗尽”这类表象结果,根本不知道底层在干什么。这里提醒一下:MySQL默认是关闭慢查询日志的,一定要记得在配置文件里打开,并设置合理的阈值:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

我个人习惯把long_query_time设置成1秒,也就是说任何执行超过1秒的SQL都会记录下来,配合log_queries_not_using_indexes把没走索引的查询也记一版,对日常优化非常有用。

2.2 应用日志与系统日志:拼接完整的故障现场

慢查询日志定位到了具体SQL,但还没有回答一个关键问题:为什么这条SQL会在那个时候集中出现?这时候就要去看应用日志了。我们后端是Java Spring Boot项目,日志统一由Logback输出到/var/log/app/,通过Loki采集。在Grafana的Explore页面里,我按时间范围搜索“orders”相关的日志,结果发现大量 org.springframework.dao.DataAccessException 异常,核心堆栈是:

java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms

HikariCP连接池的连接获取超时,原因是数据库端的连接被那些慢SQL占着不释放,新的请求拿不到连接,只能排队等30秒然后超时——用户侧感知到的“转菊花”就是这么来的。这是一个典型的“滚雪球”效应:慢SQL导致连接被占满,连接占满导致新请求排队超时,超时又让应用线程堆积,最终整个服务的可用性崩塌。

Linux系统层面也别忘了看,登录到数据库服务器上第一步就是看负载和内存:

uptime
free -h
top -c
iostat -x 1

那台数据库服务器当时load average到了28(4核机器),iowait占比也明显偏高。这些系统日志和指标进一步印证了数据库确实在“满负荷”工作,而不是网络问题或者应用本身卡死。 我的排查顺序很固定:先看监控和系统层确认是哪个组件异常,再进该组件查慢日志/错误日志,最后回到业务日志找触发条件 ,这样不会在无关方向上浪费时间。

2.3 日志不完备时,如何靠现有线索“拼图”

说实话,很多团队的日志并没有做得非常规范,这次的日志也并不是一开始就全的。早期我排查问题经常遇到日志里啥都没有的尴尬情况——应用日志只打印了Info级别,错误堆栈被吞了,慢查询日志也没开。所以 排障的过程对我来说往往也是“补日志”的过程

如果这次没有慢查询日志,我大概率会怎么做?第一,在数据库上用 SHOW FULL PROCESSLIST 看当前正在执行的SQL,这个命令能实时看到每个连接的执行状态,是排查数据库问题最常用的手段之一。第二,通过 performance_schema 或者 sys 库里的 statement_analysis 视图找出历史执行次数多、总耗时长的SQL。第三,对于MySQL 8.0,还可以直接查 information_schema.innodb_trx sys.innodb_lock_waits 来看事务和锁等待情况。

补一条建议,如果你用的是MobaXterm这类SSH工具,记得把会话的日志保存功能打开(MobaXterm自带“Save session output to a file”的功能),这样你在终端敲过的每一个排查命令、看到的每一行输出都有存档,复盘时翻一翻比回忆靠谱得多。这个习惯在我多次实战中救了命,特别是那种“临门一脚找到根因但忘了记录当时看到什么”的情况。

3. 根因定位:一次完整的SQL优化与系统恢复实录

3.1 用EXPLAIN看执行计划,锁定索引问题

在拿到慢SQL之后,我第一时间用 EXPLAIN 看了这条查询的执行计划,结果如下:

EXPLAIN SELECT * FROM orders 
WHERE user_id = 12345 
ORDER BY created_at DESC 
LIMIT 10 OFFSET 20000;

执行计划里有几个关键信息让我确定了根因:

列名 说明
type ref 用到了user_id索引的等值查找,没有全表扫描
rows 352000 预估扫描35万行,这是最大的问题
Extra Using where; Using filesort 排序没有走索引,需要文件排序

问题已经很清楚了:虽然user_id索引帮我们过滤出了某个用户的35万条订单记录,但MySQL还需要对这35万行按created_at重新排序,然后才取第20001到20010条。大家感受一下, MySQL为了一次展示10条数据,干了一整套“把35万行捞出来、全部排好序、再丢掉不需要的”的体力活 ,而且这个动作在一个很短的时间窗口内被高频触发,数据库不崩溃才怪。

为什么骑手一开始没走这个索引?因为 ORDER BY created_at DESC 不在索引里,MySQL的优化器评估下来觉得用user_id索引再排序,成本不一定比全表扫描低,所以选了perform文件排序。解决思路就清晰了:让排序也能走索引。我们可以联合索引 (user_id, created_at) ,这样MySQL既可以用user_id做等值过滤,又可以直接按created_at的顺序读取索引,不需要额外排序。

3.2 从根源修复:建联合索引 + 改写分页SQL

索引问题找到后,接下来的操作就比较“常规操作”了:

ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);

这里说明一下索引字段顺序, user_id放前面是因为查询条件是等值过滤,created_at放后面负责排序 ,这样索引B+树天然就是按“用户+时间”排好序的,只需要顺序扫描索引节点,跳过前20000条,取接下来10条就好,不需要filesort。

为了保险起见,我还把分页SQL改写了。深分页OFFSET(就是上面那种OFFSET 20000)是另一种意义上的性能杀器,即使加了索引,OFFSET越大,MySQL扫描的索引节点也越多。改用“游标分页”是更好的方案:

-- 原来的方式(OFFSET越大越慢)
SELECT * FROM orders 
WHERE user_id = 12345 
ORDER BY created_at DESC 
LIMIT 10 OFFSET 20000;

-- 改写成游标方式(记住上次查询的最后一条时间戳)
SELECT * FROM orders 
WHERE user_id = 12345 
AND created_at < '2026-02-20 12:34:56'
ORDER BY created_at DESC 
LIMIT 10;

对于“加载更多”这种场景,游标分页体验更好,数据也不容易重复。当然如果产品形态必须支持任意页码跳转,那可以考虑用搜索引擎(Elasticsearch)或者离线数据仓库来兜底,这是另一个话题了。对于这次故障,添加联合索引已经是见效最快的操作。

3.3 恢复与验证:从“恢复服务”到“确认根因”的完整闭环

索引创建完成后,我并没有立刻宣布“故障解决”,而是按照下面的步骤做了完整的验证:

  1. 先看监控指标:Prometheus上连接数曲线是否回落,threads_running是否恢复到20以下。
  2. 再查慢查询日志:等在慢日志里再也看不到那条SQL后,才算真正过关。
  3. 最后做功能验证:用接口实际调用一次分页查询,确认响应时间从原来的5秒降到50毫秒以内,同时观察应用日志中是否还有获取连接超时的异常。

这里说一个细节:建索引本身是DDL操作,如果表特别大,直接执行 ALTER TABLE 可能会锁住写入请求,加剧故障。实际操作中我先在从库上验证索引创建耗时,评估主库执行的风险,然后选择在业务低峰期一次性执行。当时为了快速止血,还设置了一个连接池超时时间的临时调整,让应用侧能更快地把失败请求fail fast,而不是傻等30秒,这种“先恢复服务、再优化根因”的思路在故障处理里很重要。

最后用 SHOW FULL PROCESSLIST 确认一下没有残留的慢查询进程占着连接,整个恢复流程就完整了。 故障处理的完整体验应该是:监控说“我出问题了” → 日志说“我是谁” → 人工分析说“我为什么出问题” → 修复后监控和日志联合确认“我彻底解决了” ,缺了任何一环都不算闭环。

4. 常见问题与排查技巧实录

4.1 这次故障之外的3个“同类坑”

类似的故障我踩过不止一次,这里把常见的坑和排查思路整理成一个速查表,方便大家直接参考:

故障现象 典型原因 第一时间排查命令/方法
数据库连接池耗尽 慢SQL、连接泄漏、突发流量 SHOW FULL PROCESSLIST 、查看HikariPool超时日志
接口响应突然变慢 慢查询、外部服务依赖抖动、GC问题 慢查询日志、 jstack 抓线程栈、Nginx访问日志看响应时间
死锁/锁等待 事务乱序、缺少索引导致行锁升级 SHOW ENGINE INNODB STATUS sys.innodb_lock_waits
系统load飙高但CPU不高 磁盘IO瓶颈、swap频繁 iostat -x 1 vmstat 1
Redis慢查询/阻塞 大key、热key、慢命令 SLOWLOG GET redis-cli --bigkeys

比如有一次线上整条链路都很慢,但MySQL侧完全没问题,后来排查发现是某个外部接口响应越来越慢,调用方没有设置超时时间,导致线程大量阻塞。这种“外部依赖抖动”在日志上表现得很明显——应用日志里大量超时异常都来自同一个第三方客户端。用 jstack 抓一下线程栈,看看线程到底卡在哪个调用上,基本就能实锤。

还有一次更隐蔽,数据库磁盘没满但IO等待很高,排查下来是binlog刷盘策略问题, sync_binlog 从0改成了1之后没有评估写入压力的变化。这个案例想说明的是, 排障时不要只盯着应用层,数据库自己的各种日志(binlog、redo log、error log)都是线索来源 ,不过binlog主要用于数据复制和恢复,只有存在复制延迟或数据不一致时通常才需要重点关注。

4.2 日志排查实战:从小白到熟练的3个阶段

第一阶段是“会看”。这个阶段只要掌握几个核心命令就行:查看实时日志 tail -f 、按关键字搜索 grep 、按时间段过滤用 sed 结合时间戳,再加一个查看系统日志的 journalctl -u 服务名 。够用了,大部分问题靠这几个命令就能定位个七八成。

第二阶段是“会捞”。当线上服务有多个实例时,不可能一台一台登录去找日志,这时候就需要日志采集系统。我们用Loki + Promtail做了一套轻量日志收集,所有服务日志统一进入Loki,Grafana上直接搜Label和关键字。相比ELK那套重方案,Loki胜在轻量、部署快、成本低,特别适合中小团队。如果你对Docker容器比较熟,Loki官方文档里的docker-compose一键部署脚本可以让你十分钟内跑起来。

第三阶段是“会挖”。到了这个阶段,你会开始关注日志本身的质量——有没有打印关键参数、有没有合理的traceId串联整条调用链、异常堆栈是否完整。我强烈建议在日志里为每个请求生成一个traceId,全链路透传下去,这样排查问题时只需根据一个ID,就能把网关、微服务、数据库连接操作完整地串起来,效率提升不是一点半点。

4.3 我个人的几个排障习惯

最后分享几个长期实践中沉淀下来的习惯,不一定多高级,但在关键时刻很管用:

  • 做任何变更前先看监控基线 。如果不知道系统正常时的指标长什么样,你无法判断异常是变更引起的还是偶发抖动。我习惯在Grafana里保存每个核心组件的“健康运行截图”,没事就翻一翻,时间长了自然有了体感。
  • 日志里永远要有时间戳和上下文 。只有错误堆栈没有时间点的日志,基本等于废日志。排查时必须能在日志里还原出“在哪个时间点、是哪个请求、做了什么操作”这三要素。
  • 处理故障时先保存现场再动手 。脚本也好、数据库状态也好、线程dump也好,先把证据留下来,然后再去做修复。很多人一上来就重启服务,结果根因被覆盖,过几天故障卷土重来,变本加厉。
  • 为常用命令做一个小备忘单 。比如我自己就保存了一个“排障命令速查”文档,从系统层(top、free、iostat、vmstat)到中间件层(MySQL、Redis、Nginx),每种组件的重点排查命令、日志路径、常见问题应对全都列出来。故障来的时候时间就是生命,少敲几个命令、少走几次弯路都是实实在在的效率提升。

这次从监控告警到慢查询定位,再到索引修复和日志复盘,整个过程下来,最深的体会是: 监控和日志从来不是两个独立的东西,它们是一个闭环的两端 。没有监控,你不知道什么时候该去查日志;没有日志,你看到了异常也只能干瞪眼。希望这篇实战记录能帮你把这条链路建立起来,下次再遇到类似的问题,不至于手忙脚乱。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐