慢接口定位实战:从应用日志一路追到 SQL 执行计划

慢接口定位实战:从应用日志一路追到 SQL 执行计划

用户说"接口慢",开发人员第一反应往往是看代码,运维人员第一反应往往是看数据库。但其实慢接口从来不是单纯属于某一端的问题,它是一条链路问题:请求进入应用,应用从连接池拿连接,执行 SQL,数据库生成执行计划,扫描数据,返回结果,应用再组装响应。任何一段变慢,用户看到的都是接口慢。

本文用一个可复现的排查路径,演示如何从 Spring Boot 应用日志一路追到金仓数据库侧的 SQL 和执行计划。

@toc


一、慢接口不要先猜

慢接口排查这个事儿,最忌讳的就是凭经验直接下结论。比如啥呢:

  • 一看到接口慢,你就说这是数据库慢。
  • 看到 SQL 慢,你就说是索引缺失了。
  • 看到连接池在等待,你二话不说就直接加连接数。
  • 看到 CPU 高了,那就先重启服务再说。

那更稳妥的方式是什么呢?我们得按链路把时间拆开来看:

text 复制代码
接口总耗时 = 排队等待 + 业务计算 + 获取连接 + SQL 执行 + 结果处理 + 网络传输

也就是说,只有把时间这么拆开,你才能真正知道瓶颈到底在哪里。

二、准备示例慢查询

那么这里呢,我们沿用第一篇里创建好的 kb_app、shop 和 app_user。接着我们先准备一张用来做慢查询演示的订单表:

sql 复制代码
CREATE TABLE shop.t_order_query_demo (
    order_id       INT PRIMARY KEY,
    user_id        INT NOT NULL,
    order_status   VARCHAR(20) NOT NULL,
    amount         NUMERIC(12,2) NOT NULL,
    created_at     TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

然后给应用账号授个权:

sql 复制代码
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.t_order_query_demo TO app_user;

通常来说,一个很常见的接口就是按用户去查订单,对吧:

sql 复制代码
SELECT order_id, user_id, order_status, amount, created_at
FROM shop.t_order_query_demo
WHERE user_id = ?
ORDER BY created_at DESC;

这个查询要是数据量小,它肯定不会慢。但是当你的数据增长到几十万、几百万行以后,要是没有合适的索引,它往往就可能变慢了。

三、第一步:在应用日志里记录接口耗时

首先第一步,咱们得给接口加上耗时日志。最简单的做法,你可以在 Controller 或者拦截器里面去记:

java 复制代码
long start = System.currentTimeMillis();
try {
    return orderService.findByUserId(userId);
} finally {
    long cost = System.currentTimeMillis() - start;
    log.info("api=/orders, userId={}, costMs={}", userId, cost);
}

那如果是生产环境的话,通常来说我建议你统一用过滤器或者 AOP 来记。记点啥呢?

  • 请求的路径。
  • 请求参数的摘要。
  • TraceId。
  • 总共花了多少时间。
  • 响应状态是啥。

这里提醒一句,千万别在日志里打印敏感数据啊。

四、第二步:记录 SQL 执行耗时

那接口总耗时慢了,是不是就代表 SQL 一定慢呢?其实不是的。我们需要把 SQL 层的耗时单独记下来。

要是你用的是 JdbcTemplate,那可以在 Repository 层里面去记:

java 复制代码
long start = System.currentTimeMillis();
try {
    return jdbcTemplate.query(sql, rowMapper, userId);
} finally {
    long cost = System.currentTimeMillis() - start;
    log.info("sql=findOrdersByUserId, userId={}, costMs={}", userId, cost);
}

那我们来分析一下。假如接口总耗时是 3 秒,但是 SQL 只用了 50 毫秒的话。这说明啥?说明瓶颈在业务计算、远程调用或者结果处理上。那反过来,接口总耗时 3 秒,SQL 就用了 2.8 秒。那这种情况的话,你就可以继续往数据库侧去查了。

五、第三步:数据库侧找正在运行的 SQL

赶上接口慢的时候,咱们进到数据库里去执行一下看看:

说明一下哈,运行状态视图的名称和字段,你得看当前 KingbaseES 环境是啥样的。正式发布前,我建议大家用自己的服务器先验证一遍。把示例 SQL 调整成当前版本能直接执行的排查脚本再去跑。

sql 复制代码
SELECT pid,
       usename,
       client_addr,
       state,
       query_start,
       now() - query_start AS running_time,
       query
FROM sys_stat_activity
WHERE state = 'active'
ORDER BY running_time DESC;

要是你能看到对应的 SQL,那就说明它正在数据库里执行着呢。这时候你要记下几个东西:

  • 执行的账号是哪个。
  • 客户端地址是啥。
  • 运行时长有多久。
  • SQL 文本内容是啥。

那要是数据库侧看不到慢 SQL,但是应用侧记录的 SQL 耗时又很长呢?这时候你就要考虑了,是不是连接池等待啊,网络等待啊,或者驱动层处理有问题,也有可能是应用日志统计的位置不准确导致的。

六、第四步:查看执行计划

拿到 SQL 以后,咱们在测试环境里用执行计划来分析一下。看个示例:

sql 复制代码
EXPLAIN
SELECT order_id, user_id, order_status, amount, created_at
FROM shop.t_order_query_demo
WHERE user_id = 1001
ORDER BY created_at DESC;

那要是环境允许的话,你可以进一步看看它实际的执行情况:

sql 复制代码
EXPLAIN ANALYZE
SELECT order_id, user_id, order_status, amount, created_at
FROM shop.t_order_query_demo
WHERE user_id = 1001
ORDER BY created_at DESC;

这里要注意哈,生产环境里一定要谨慎使用这种会实际执行 SQL 的分析方式。特别是涉及到写操作或者大查询的时候,我建议还是在测试环境里复现比较好。

七、第五步:判断是否需要索引

刚才那个查询,条件是 user_id,排序字段是 created_at。那我们其实可以考虑给它建个组合索引:

sql 复制代码
CREATE INDEX idx_order_query_user_created
ON shop.t_order_query_demo(user_id, created_at DESC);

建好索引以后,咱们再来看一次执行计划:

sql 复制代码
EXPLAIN
SELECT order_id, user_id, order_status, amount, created_at
FROM shop.t_order_query_demo
WHERE user_id = 1001
ORDER BY created_at DESC;

怎么判断索引有没有效呢?其实不是看你"有没有建索引"。而是看执行计划有没有走合适的路径,还有实际执行时间是不是真的降下来了。

八、第六步:控制返回数据量

很多慢接口出现的原因,其实并不是数据库不会查。而是啥呢?应用一次性查的数据太多了。

所以千万别这么写:

sql 复制代码
SELECT order_id, user_id, order_status, amount, created_at
FROM shop.t_order_query_demo
WHERE user_id = ?
ORDER BY created_at DESC;

更合理的做法是分页,对吧:

sql 复制代码
SELECT order_id, user_id, order_status, amount, created_at
FROM shop.t_order_query_demo
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

那要是数据量特别大的话,你还得进一步考虑,用基于上一页边界值的分页方式。这样可以避免深分页带来那种很高的扫描成本。

九、第七步:排查连接池等待

接口慢还有另外一种情况。啥情况呢?SQL 本身跑得不慢,但是请求一直在那等连接。

当 HikariCP 出现连接等待的时候,应用日志里往往就会出现这种信息:

text 复制代码
Connection is not available, request timed out

那这个时候你就要同时看几个地方了:

  • 看看当前的活跃连接是不是快到连接池上限了。
  • 数据库那边有没有慢 SQL 或者长事务的情况。
  • 应用这边有没有连接泄漏。
  • Web 线程池是不是堆积了。

这时候不要光去改 maximum-pool-size。要是连接都被慢 SQL 给占住了,你去增加连接数的话,往往仅仅只是让更多的慢 SQL 同时压到数据库上而已。

十、慢接口排查模板

咱们可以把每次慢接口排查的过程,都按下面这个模板记下来:

text 复制代码
接口路径:
请求时间:
请求参数摘要:
接口总耗时:
SQL 执行耗时:
是否等待连接:
数据库账号:
客户端地址:
SQL 文本:
执行计划摘要:
返回行数:
是否命中索引:
优化动作:
优化前耗时:
优化后耗时:

有了这个模板,优化过程就能复盘了。也就不会到最后只留下一个"加了个索引,快了"这种口头结论了。

十一、常见误区

1. 只看平均耗时

其实平均值这个东西,它是会掩盖毛刺的。接口性能好不好,你得看 P95、P99 还有那些最慢的请求。

2. 只在小数据量环境验证

数据量小的时候,就算全表扫描它也可能很快。所以调优的时候,你必须得考虑接近生产环境的数据规模才行。

3. 只加索引不看写入成本

索引这东西,虽然能加速查询,但它是会增加写入和维护成本的。那些高频写入的表,你加索引得谨慎评估一下。

4. SQL 返回字段过多

接口要是只需要 5 个字段,那你就别去 SELECT *。因为你查的字段越多,网络传输和对象组装的成本也就越高嘛。

十二、小结

那么小结一下。慢接口排查这个事儿,得从应用日志开始搞。用数据去证明时间到底花在哪了,然后再进到数据库里看会话和执行计划。连接池等待啊、慢 SQL 啊、返回数据过多啊、索引缺失或者长事务这些情况,往往表现出来的都是接口慢。所以说,只有把链路拆开,你才能避免盲目去调参。

那下一篇呢,我们就来看看多环境配置治理的事儿。重点解决一下开发、测试、生产配置混乱,还有连接信息串环境的这些问题。大家到时候看。

相关推荐
考虑考虑1 小时前
SQL中的 CASE WHEN
数据库·后端·sql
SelectDB1 小时前
Doris 替换 ClickHouse:部署核查命令、常见问题排查与适配说明
大数据·数据库·数据分析
为你学会写情书1 小时前
从零搭建 SQL 数据库到 ORM 选型:Prisma 与 Drizzle 深度对比
数据库
Nturmoils1 小时前
OceanBase VS 金仓:同一组复杂 SQL,分布式与集中式架构怎么跑
数据库
旺仔不是程序员1 小时前
索引膨胀与重建:PostgreSQL REINDEX 生产实践
数据库·后端·sql
IT枫斗者枫哥1 小时前
MyBatis只查两次,为什么还会查到别家客户?
java·数据库
SelectDB1 小时前
日志成本打不下来?Doris 降本实操笔记:建表、参数、冷热分层与四个排错现场
大数据·数据库·数据分析
这个DBA有点耶1 小时前
数据库迁移不停机方案:双轨并行技术架构与3TB核心系统落地实践
数据库·架构·dba
SelectDB1 小时前
Doris vs ClickHouse:企业级分析场景下,OLAP 的能力边界正在如何变化?
大数据·数据库·数据分析