微服务为什么会变慢?从 QPS、P99 到 Trace 与性能瓶颈

前面我们学习了微服务的超时、重试、熔断、限流和 Backpressure,知道下游服务一旦变慢,故障就可能沿着调用链不断传播。

但在真正的系统中,我们首先需要解决一个更基础的问题:

怎么知道服务到底慢在哪里?

假设用户提交订单,需要经过下面这条调用链:

text 复制代码
Client
   |
   v
API Gateway
   |
   v
Order Service
   |
   +----> Inventory Service
   |             |
   |             v
   |           MySQL
   |
   +----> Coupon Service
   |             |
   |             v
   |           Redis
   |
   v
返回订单结果

正常情况下,创建订单只需要 200ms。

但某一天,用户反馈下单很慢,部分请求甚至需要 3 秒。

这时我们可能产生几种猜测:

  • Order Service 的 CPU 不够了,是不是应该增加 Pod?
  • Inventory Service 变慢了,是不是 MySQL 查询出了问题?
  • Redis 缓存命中率下降了,是不是大量请求开始回源?
  • 服务之间的网络是不是出现了延迟?
  • 是不是重试让一次订单请求变成了多次下游调用?

这些原因都可能导致接口变慢,但对应的解决方法完全不同。

如果瓶颈在 MySQL,盲目增加 Order Service Pod,不但可能没有效果,反而可能产生更多数据库请求。

所以性能排查的关键,不是先决定如何优化,而是先用指标和调用链证据确定时间花在了哪里。

这一篇从 QPS、响应时间和 P99 开始,再介绍 Trace,最后用完整案例梳理微服务性能问题的排查方法。


一、为什么不能只看服务有没有报错?

假设 Order Service 的监控数据显示:

text 复制代码
请求成功率:99.9%

平均响应时间:150ms

看起来系统似乎运行得很好。

但用户仍然不断反馈:

text 复制代码
下单时偶尔需要等好几秒

这并不矛盾。

因为请求成功,不代表请求足够快。

而平均响应时间正常,也不代表所有请求都很快。

例如一个接口执行了 100 次:

text 复制代码
99 次请求:100ms

1 次请求:5000ms

它的平均响应时间约为:

text 复制代码
149ms

平均值看起来并不高。

但那一次等待 5 秒的用户,体验显然很差。

如果类似的慢请求持续发生,对于高流量系统来说,就可能影响大量用户。

所以性能分析不能只关注:

text 复制代码
请求是否成功?

还需要关注:

text 复制代码
请求量有多大?

响应时间如何分布?

最慢的部分请求慢到什么程度?

请求慢的时候,系统资源发生了什么变化?

这就需要几个基础指标。


二、QPS:系统每秒处理多少请求?

QPS 是 Queries Per Second 的缩写。

在微服务监控中,我们经常用它表示某个接口或服务每秒处理的请求数量。

例如:

text 复制代码
Order Service

QPS = 500

可以理解为:

在相应统计时间窗口内,Order Service 平均每秒处理约 500 个请求。

不过,严格来说,QPS 原本更偏向查询数量;对于 HTTP 服务,RPS(Requests Per Second)在术语上更准确。

日常讨论微服务性能时,两者经常被混用,但应明确统计口径。

1. QPS 高意味着什么?

假设一个商品详情接口:

http 复制代码
GET /products/1001

在正常情况下:

text 复制代码
QPS = 100

促销活动开始后:

text 复制代码
QPS = 3000

这说明请求量明显增加。

如果系统可以稳定处理,响应延迟和错误率没有显著恶化,那么较高的 QPS 本身不是问题。

但如果:

text 复制代码
QPS 增加
    |
    v
CPU / 数据库连接使用率上升
    |
    v
响应延迟增加
    |
    v
请求开始超时

就说明系统可能逐渐接近某个资源瓶颈。

2. QPS 不等于系统最大处理能力

假设服务当前运行在:

text 复制代码
QPS = 500

不能据此推断它的最大处理能力就是 500 QPS。

因为它可能还有大量空闲资源。

反过来,一个服务即使已经达到:

text 复制代码
QPS = 2000

也不能说明它能够长期稳定处理这个流量。

可能当前已经出现:

text 复制代码
P99 延迟持续升高

数据库等待队列不断增长

错误率开始增加

所以评估系统吞吐能力时,应当同时观察:

请求量、响应延迟、错误率和资源使用情况。

例如:

负载 QPS P99 错误率 状态
低负载 200 120ms 0% 稳定
中负载 800 180ms 0% 稳定
高负载 1500 900ms 0.5% 开始退化
过载 2000 3500ms 8% 明显过载

以上数字只是一个假设的压测示例。

它说明的是:

吞吐量增加到一定程度后,响应延迟可能明显恶化,甚至出现大量失败。

而不是说所有系统都会在 1500 QPS 时遇到瓶颈。

3. QPS 还要区分不同层级

假设:

text 复制代码
Gateway QPS = 1000

但:

text 复制代码
Inventory Service QPS = 2500

为什么 Inventory Service 的 QPS 反而更高?

一种可能是:

text 复制代码
一次外部请求
      |
      v
Order Service
      |
      +-- 库存查询
      |
      +-- 库存预留

也可能是因为上游发生了重试。

因此:

text 复制代码
外部请求 QPS

和:

text 复制代码
内部 RPC 调用 QPS

并不一定相等。

排查系统性能时,需要知道统计的到底是哪个服务、哪个接口,以及哪一层调用。


三、平均响应时间、P95 和 P99 到底有什么区别?

假设某个接口有一万次请求。

如果只看平均值,很难发现少量特别慢的请求。

所以在实际系统中,我们还经常关注:

text 复制代码
P50

P95

P99

它们属于百分位延迟指标。

1. P50:中位数附近的响应时间

P50 表示大约一半请求的响应时间不超过这个值。

例如:

text 复制代码
P50 = 100ms

可以理解为:

大约 50% 的请求在 100ms 内完成。

它反映比较典型的请求体验。

2. P95:关注最慢的 5% 请求

例如:

text 复制代码
P95 = 300ms

表示大约 95% 的请求在 300ms 内完成。

剩余较慢的约 5% 请求可能需要更长时间。

3. P99:关注尾部慢请求

例如:

text 复制代码
P99 = 2000ms

表示大约 99% 的请求在 2000ms 内完成。

最慢的约 1% 请求可能超过 2000ms。

这部分慢请求通常被称为尾延迟(Tail Latency)。

需要注意,P99 并不是最大响应时间。

如果:

text 复制代码
P99 = 2000ms

仍然可能存在少量请求需要 5 秒甚至更长时间。

4. 为什么 P99 对微服务特别重要?

因为一个外部请求通常不只是执行一次本地计算。

例如:

text 复制代码
Order Service
    |
    +-- Inventory Service
    |
    +-- Coupon Service
    |
    +-- MySQL

用户感知到的是整条调用链的总耗时。

如果其中某个下游偶尔特别慢,就可能影响整个外部请求。

尤其是在存在多个下游依赖时,尾部延迟问题更容易暴露。

例如:

text 复制代码
Inventory Service:

P50 = 30ms

P99 = 1200ms
text 复制代码
Coupon Service:

P50 = 20ms

P99 = 900ms

这说明两项依赖的大部分调用都比较快,但它们都存在明显的慢请求。

不过,不能简单把两个服务的 P99 相加,就认为最终接口的 P99 一定等于:

text 复制代码
1200ms + 900ms

因为各次请求的慢调用未必同时发生,而且不同调用可能并行执行。

整条调用链的 P99,需要通过真实请求的端到端延迟数据计算,而不是直接相加各个服务的百分位指标。

5. 为什么要一起看 P50、P95 和 P99?

例如某一天:

text 复制代码
P50 = 100ms

P95 = 200ms

P99 = 3000ms

这说明大部分请求仍然很快,但少量请求出现了严重的长尾延迟。

可以重点排查:

text 复制代码
偶发慢 SQL

连接池等待

锁等待

网络抖动

GC 停顿

少量请求重试

而如果:

text 复制代码
P50 = 2000ms

P95 = 3500ms

P99 = 5000ms

说明不只是少数请求慢,整体请求体验都已经明显恶化。

这时更需要关注:

text 复制代码
系统性资源瓶颈

持续过载

下游整体响应变慢

因此:

P50 可以帮助判断典型请求是否正常,P95 和 P99 则帮助发现慢请求和尾延迟问题。


四、为什么 QPS 没增加,服务也可能越来越慢?

这是一个非常值得理解的问题。

假设 Order Service 每秒接收:

text 复制代码
200 个请求

每个请求平均停留:

text 复制代码
50ms

在稳定情况下,系统内同时存在的请求数量大约为:

text 复制代码
200 × 0.05 = 10

如果下游响应变慢,平均请求耗时增加到:

text 复制代码
2 秒

而请求到达速率仍然是:

text 复制代码
200 次/秒

那么按照稳定系统中的 Little's Law(利特尔法则),对应的平均在系统请求数约为:

text 复制代码
200 × 2 = 400

这个关系可以写为:

text 复制代码
L = λ × W

其中:

  • L:系统内平均请求数。
  • λ:稳定状态下的实际吞吐速率。
  • W:请求在系统中的平均停留时间。

这个例子主要用于解释延迟与并发数量之间的关系。严格应用该公式需要系统达到相应稳定条件,不能在队列无限增长时仍然机械地把输入 QPS 直接代入。

但它揭示了一个重要现象:

即使请求到达速率没有增加,只要请求停留时间明显变长,系统中的并发请求数量也可能大幅增加。

1. 并发数量上升后会发生什么?

假设 Order Service 的线程池只能同时处理有限数量的同步请求。

当库存 RPC 变慢:

text 复制代码
Worker 1 → 等待 Inventory

Worker 2 → 等待 Inventory

Worker 3 → 等待 Inventory

Worker 4 → 等待 Inventory

越来越多线程处于等待状态。

新的请求只能进入队列。

text 复制代码
新请求
   |
   v
等待队列
   |
   v
等待可用 Worker

这样,即使 Order Service 自身没有进行复杂计算,接口响应时间仍然可能越来越长。

2. 为什么 CPU 低不代表服务没有瓶颈?

假设监控显示:

text 复制代码
Order Service CPU = 20%

但同时:

text 复制代码
P99 = 4000ms

CPU 很低,接口却很慢。

这并不奇怪。

因为大量请求可能正在等待:

text 复制代码
MySQL 连接

远程 RPC 返回

网络 I/O

线程池空闲线程

数据库锁

这些等待不一定会消耗大量 CPU。

所以看到:

text 复制代码
CPU 低

不能直接推断:

text 复制代码
服务没有性能问题

CPU 只是资源指标之一。

真正需要判断的是:

请求是在执行计算,还是在等待其他资源?


五、微服务响应时间到底花在了哪里?

假设用户提交订单,最终耗时:

text 复制代码
1200ms

但 Order Service 内部只是执行了几个判断和 SQL。

这 1200ms 到底由哪些部分组成?

我们可以把一次请求拆成多个阶段:

text 复制代码
Gateway 接收请求
       |
       v
Order Service 排队
       |
       v
执行本地业务逻辑
       |
       v
调用 Inventory Service
       |
       v
等待 MySQL
       |
       v
返回结果

这里可能存在不同类型的耗时。

1. 排队时间

请求到达 Order Service 后,可能无法立即获得执行资源。

例如线程池已经满了。

text 复制代码
请求到达
   |
   v
等待 300ms
   |
   v
开始执行

这 300ms 并不属于业务代码真正执行的时间。

如果监控只统计业务方法开始之后的耗时,就可能漏掉这部分等待。

2. 本地计算时间

例如:

text 复制代码
参数校验

业务规则判断

数据转换

这些操作可能消耗 CPU。

但如果只是简单的参数判断,通常不会占用很长时间。

3. 远程调用时间

例如:

text 复制代码
Order Service
      |
      v
Inventory Service

Order Service 发起请求后,需要等待下游处理完成。

这个耗时可能包含:

text 复制代码
网络传输

下游排队

下游计算

数据库访问

结果返回

所以一次 RPC 耗时高,并不意味着网络本身一定慢。

4. 数据库访问时间

例如:

sql 复制代码
SELECT *
FROM inventory
WHERE product_id = 1001;

数据库相关耗时可能包括:

text 复制代码
等待数据库连接

SQL 执行

索引扫描

锁等待

结果传输

这里要特别区分:

应用侧记录的数据库调用耗时,不一定等于 MySQL 真正执行 SQL 的时间。

例如:

text 复制代码
等待连接池:500ms

SQL 执行:20ms

结果返回与处理:10ms

应用看到的数据库访问耗时可能约为:

text 复制代码
530ms

但 SQL 本身只执行了 20ms。

如果不知道这个区别,就可能把大量时间浪费在 SQL 索引优化上,却忽略真正的连接池等待问题。


六、Trace:如何看到一条请求经过了哪些服务?

前面一直在讨论:

text 复制代码
请求耗时在哪里?

如果只有每个服务自己的日志和监控数据,我们可能知道:

text 复制代码
Order Service 变慢

Inventory Service 变慢

MySQL 查询有时变慢

但仍然无法直接判断它们是不是属于同一条用户请求。

这就需要 Distributed Tracing(分布式链路追踪)。

1. 什么是 Trace?

Trace 可以理解成一次业务请求在多个服务之间执行的完整轨迹。

例如用户发起:

http 复制代码
POST /orders

请求经过:

text 复制代码
Gateway
    |
    v
Order Service
    |
    +----> Inventory Service
    |             |
    |             v
    |           MySQL
    |
    +----> Coupon Service
                  |
                  v
                 Redis

如果链路追踪信息能够正确跨服务传递,就可以把这些操作关联到同一个 Trace。

例如:

text 复制代码
Trace ID = T10001

这个 ID 用于关联此次请求中不同组件产生的执行记录。

2. 什么是 Span?

一个 Trace 通常由多个 Span 组成。

Span 表示一次具体操作或某个执行阶段。

例如:

text 复制代码
Trace T10001
     |
     +-- Gateway Request Span
     |
     +-- Order Service Span
              |
              +-- Inventory RPC Span
              |        |
              |        +-- MySQL Query Span
              |
              +-- Coupon RPC Span
                       |
                       +-- Redis GET Span

每个 Span 可以包含:

text 复制代码
操作名称

开始时间

结束时间

耗时

状态

父子关系

必要的业务或技术属性

例如:

text 复制代码
Span:

InventoryService.reserve

Duration:1020ms

Status:OK

它表示这一次库存服务调用耗费了约 1020ms。

3. Trace 和日志有什么区别?

日志主要记录某个时间点发生了什么。

例如:

text 复制代码
Inventory reservation started
text 复制代码
MySQL query completed

Trace 则更强调操作之间的关系和时间分布。

它不仅能展示:

text 复制代码
Inventory Service 执行了什么

还可以展示:

text 复制代码
它属于哪一次订单请求?

由哪个上游 Span 发起?

从什么时候开始?

持续了多久?

与其他下游调用是串行还是并行?

因此,Trace 特别适合分析跨服务调用的性能问题。

不过,Trace 不会自动替代所有日志和指标。

真正排查问题时,通常需要把 Trace、Metrics 和 Logs 结合起来。


七、怎么看一张 Trace 瀑布图?

现在假设一条订单请求总耗时:

text 复制代码
1200ms

链路追踪系统记录了以下操作。

Span 耗时
Gateway 请求 1200ms
Order Service 处理 1150ms
Inventory RPC 1020ms
MySQL 调用 850ms
Coupon RPC 110ms
Redis GET 20ms

这时不能直接把这些时间相加。

因为很多 Span 是父子包含关系。

例如:

text 复制代码
Order Service
      |
      v
Inventory RPC
      |
      v
MySQL

MySQL 的耗时已经包含在 Inventory RPC 的执行时间中,而 Inventory RPC 又包含在 Order Service 的总处理过程中。

如果把全部 Span Duration 相加,就会重复统计时间。

1. 瀑布图应该怎么看?

可以把一次请求的时间轴画出来:

text 复制代码
时间轴:0ms                                  1200ms
        |----------------------------------------|

Gateway        [========================================]

Order Service    [======================================]

Inventory RPC       [=================================]

MySQL Query             [============================]

Coupon RPC        [====]

Redis GET          [=]

这只是帮助理解的简化示意。

真实的 Trace 瀑布图会根据各 Span 的开始时间和持续时间,画出更准确的水平时间条。

2. 为什么要看时间条的位置?

假设 Inventory RPC 和 Coupon RPC 是并行执行的。

text 复制代码
Order Service
      |
      +----> Inventory RPC:1020ms
      |
      +----> Coupon RPC:110ms

如果它们大致同时开始,Order Service 就不一定需要等待:

text 复制代码
1020 + 110 = 1130ms

而可能主要受到较慢的 Inventory RPC 限制。

也就是说:

并行调用的总耗时不等于各个调用耗时简单相加。

而如果两个调用是严格串行的,那么后一个必须等待前一个完成,这两段时间就可能都处在整个请求的关键路径上。

所以 Trace 不只是看哪个 Span 最长,还要看:

text 复制代码
调用之间的父子关系

开始时间和结束时间

哪些调用并行

哪些步骤互相等待

3. 找到最慢的 Span,就找到根因了吗?

不一定。

假设 Trace 显示:

text 复制代码
Inventory RPC = 1020ms

这只是说明大量时间花在库存调用上。

继续向下看:

text 复制代码
MySQL 调用 = 850ms

说明数据库访问是重要的耗时来源。

但这 850ms 内可能包含:

text 复制代码
连接池等待 = 700ms

SQL 执行 = 120ms

其他开销 = 30ms

这时真正需要优化的,可能并不是 SQL 本身。

如果连接池等待很长,还需要判断:

  • 是否有大量连接被长事务占用。
  • 数据库处理能力是否不足。
  • 是否存在请求并发过高。
  • 连接池配置是否与数据库承载能力匹配。

因此,Trace 的作用是逐层缩小问题范围,而不是看到最长时间条就直接认定根因 。


八、Trace、Metrics 和 Logs 应该怎么配合?

前面已经了解了 QPS、P99 和 Trace。

现在再把 Logs 放进来。

这三类数据关注的重点不同。

1. Metrics:系统整体发生了什么变化?

例如:

text 复制代码
Order Service:

QPS = 1200

P99 = 3000ms

Error Rate = 5%

这些指标可以帮助我们快速发现:

text 复制代码
系统的响应延迟突然上升

或者:

text 复制代码
某个时间段的错误率明显增加

Metrics 更适合观察整体趋势。

2. Trace:某条请求为什么慢?

当我们发现:

text 复制代码
Order Service P99 上升

就可以查看相应时间段的慢请求 Trace。

例如:

text 复制代码
Gateway
   |
   v
Order Service
   |
   v
Inventory Service
   |
   v
MySQL

Trace 显示 80% 以上的请求耗时集中在库存服务相关调用上。

这帮助我们把排查范围从整个系统缩小到某条依赖路径。

3. Logs:具体发生了什么?

继续查看 Inventory Service 的相关日志:

text 复制代码
MySQL connection acquisition timeout

或者:

text 复制代码
Deadlock detected

或者:

text 复制代码
Query execution exceeded threshold

就可能进一步找到实际问题。

因此,三者可以形成一条排查路径:

text 复制代码
Metrics
   |
   | 发现异常时间段
   v
Trace
   |
   | 定位异常调用路径
   v
Logs
   |
   | 查看具体错误与上下文
   v
确定原因

这里不是说每次都必须严格按照这个顺序。

例如,明显的数据库错误日志也可能直接成为排查入口。

真正重要的是:

Metrics 帮助发现整体异常,Trace 帮助定位跨服务耗时,Logs 帮助解释具体执行过程。

另外,Trace 采样不一定覆盖全部请求,因此不能仅凭少量正常 Trace 就认定系统没有尾延迟问题。

排查 P99 时,需要确保能够观察到足够多的慢请求,或者针对慢调用采用合适的采集策略。


九、常见的微服务性能瓶颈有哪些?

现在我们已经有了基本排查工具。

接下来,把之前学过的各个组件放进同一张性能分析表中。

1. CPU 计算瓶颈

典型表现:

text 复制代码
CPU 使用率持续很高

请求处理耗时增加

Runnable 线程较多

可能原因:

text 复制代码
业务计算开销大

序列化 / 反序列化成本高

大量重复计算

解决思路包括减少不必要的计算、优化热点代码,或者在其他资源充足且可扩展时增加实例。

但不能只看到 CPU 高就立刻扩容,还应判断是否存在低效循环或者异常任务。

2. MySQL 查询瓶颈

典型表现:

text 复制代码
MySQL 查询耗时增加

慢 SQL 数量上升

数据库 CPU 或 I/O 压力明显增加

可以结合上一篇学习的索引知识:

sql 复制代码
EXPLAIN
SELECT *
FROM orders
WHERE user_id = 888
ORDER BY created_at DESC
LIMIT 20;

检查:

text 复制代码
是否使用合适的索引?

是否扫描太多行?

是否大量回表?

是否需要额外排序?

如果是数据库锁等待导致的延迟,则需要继续查看事务执行和锁等待情况。

SQL 执行慢与等待数据库锁,不一定是同一种问题。

3. 数据库连接池瓶颈

典型表现:

text 复制代码
应用等待数据库连接的时间很长

但:

text 复制代码
SQL 实际执行时间并不高

这时应该关注:

text 复制代码
连接池活跃连接数

空闲连接数

等待连接的请求数

连接持有时间

如果某些事务长时间持有连接,或者下游并发过高,就可能让其他请求一直等待连接。

直接把连接池从 50 扩大到 500,也未必是正确方案。

因为 MySQL 本身能够承受的并发连接和查询处理能力仍然有限。

连接池变大可能只是让更多请求同时压向数据库。

4. Redis 缓存问题

假设商品查询服务突然变慢。

Trace 显示:

text 复制代码
Redis GET
    |
    v
Cache Miss
    |
    v
MySQL Query

如果缓存命中率明显下降,大量请求就可能重新访问 MySQL。

这与前面学习的:

text 复制代码
缓存穿透

缓存击穿

缓存雪崩

有关。

但不能因为 Redis 命中率下降,就认定 Redis 自身变慢。

可能是 Redis 返回 Miss 很快,而真正慢的是后续 MySQL 回源。

因此需要同时查看:

text 复制代码
Redis 请求延迟

缓存命中率

MySQL 回源 QPS

数据库响应时间

5. RPC 和网络等待

假设:

text 复制代码
Order Service → Inventory Service

调用耗时很高。

但 Inventory Service 内部业务处理只需要很短时间。

这时应该继续检查:

text 复制代码
上游连接池是否等待

服务端请求是否排队

网络连接是否异常

是否发生了自动重试

是否存在超时和连接重建

这里尤其要避免一种常见误判:

RPC 总耗时高,不代表网络传输本身一定慢。

一次 RPC 包含调用方、网络和服务端多个阶段。

需要根据更细的指标拆分。

6. Kafka 消费积压

如果用户反馈:

text 复制代码
支付已经成功

但订单状态迟迟没有更新

而支付接口本身响应很快,那么问题可能不在同步 RPC 链路上。

例如:

text 复制代码
Payment Service
      |
      v
Kafka
      |
      v
Order Consumer
      |
      v
MySQL

如果 Consumer 处理速度下降,就可能出现较大的 Consumer Lag。

这时需要查看:

text 复制代码
Kafka Consumer Lag

Consumer 处理耗时

Consumer 错误率

重试次数

MySQL 写入耗时

需要注意,Kafka 消费延迟可能不会体现在最初支付 HTTP 请求的 Trace Duration 中。

因为支付接口可能早已返回。

因此,异步系统的性能分析不能只关注同步请求的响应时间,还需要关注:

事件从产生到最终业务处理完成的端到端延迟。


十、为什么 HPA 扩容以后,P99 反而可能更差?

这一点可以联系第三篇 Kubernetes 和第十四篇级联故障的内容。

假设 Order Service 有三个 Pod:

text 复制代码
Order Pod A

Order Pod B

Order Pod C

现在 P99 明显上升。

团队认为:

text 复制代码
实例不够

于是 HPA 扩容到六个 Pod。

但是实际瓶颈在 MySQL。

text 复制代码
Order Service
      |
      v
MySQL

增加 Order Service Pod 后:

text 复制代码
更多请求可以并行执行
       |
       v
更多 MySQL 查询同时发出
       |
       v
数据库连接和锁竞争增加
       |
       v
MySQL 响应进一步变慢

这时候 Order Service 的 CPU 可能下降了,但整个请求的 P99 反而继续恶化。

所以扩容之前应该判断:

当前限制吞吐量的到底是哪个资源?

例如:

观察结果 更值得先排查的方向
Pod CPU 持续饱和 本地计算、实例容量
Pod CPU 低,但 RPC 等待很长 下游依赖、网络、连接池
MySQL 连接池大量等待 数据库并发容量、事务持有时间
Redis Miss 明显增多 缓存失效和数据库回源
Kafka Lag 持续增长 消费能力与下游处理能力
某个接口 P99 特别高 慢请求 Trace、锁等待、异常重试

HPA 是重要的扩容机制,但它只能扩展相应工作负载的资源。

如果瓶颈在另一个服务或者共享数据库,扩容当前服务并不一定能够解决问题。

因此:

扩容之前先定位瓶颈,通常比盲目增加 Pod 更重要。


十一、一个完整案例:订单接口 P99 从 200ms 上升到 3 秒

现在用一个具体案例,把这篇文章的知识串起来。

假设我们监控到:

text 复制代码
Order Service:

QPS = 800

P50 = 120ms

P99 = 3000ms

而在正常情况下:

text 复制代码
QPS = 800

P50 = 100ms

P99 = 200ms

注意:

QPS 基本没有变化,但 P99 明显恶化。

这说明不能直接把问题归因于流量暴增。

第一步:确定异常是否集中在少量请求

我们发现:

text 复制代码
P50 变化不大

P99 大幅上升

说明大部分请求仍然较快,但尾部慢请求显著增加。

因此,应当重点分析异常时间段的慢请求。

第二步:查看对应的慢请求 Trace

选取一条耗时约 3 秒的 Trace:

text 复制代码
Gateway:3000ms
   |
   v
Order Service:2960ms
   |
   +---- Inventory RPC:2700ms
   |
   +---- Coupon RPC:100ms

可以发现,Inventory RPC 是主要耗时来源。

但这仍然不是最终根因。

第三步:继续查看 Inventory Service 内部

进一步查看库存服务 Span:

text 复制代码
Inventory Service:2680ms
       |
       v
MySQL Access:2500ms

说明大量时间出现在数据库访问阶段。

于是继续拆分。

第四步:区分连接池等待与 SQL 执行

假设监控显示:

text 复制代码
数据库连接池等待:2300ms

SQL 实际执行:150ms

其他开销:50ms

这说明 SQL 本身并不是 3 秒慢请求的主要原因。

真正的问题是:

库存服务长时间无法获得可用数据库连接。

如果这时候只去优化某条 150ms 的 SQL,可能有改善,但并没有直接处理主要的 2300ms 连接等待。

第五步:检查为什么连接池被占满

进一步发现:

text 复制代码
部分数据库事务持有连接时间很长

而且 Inventory Service 某些请求在事务中等待下游操作。

例如:

text 复制代码
BEGIN
   |
   v
修改库存记录
   |
   v
等待外部 RPC
   |
   v
COMMIT

由于事务持续时间过长,连接和相关锁一直无法及时释放。

最终导致其他请求等待连接。

第六步:确定修复方向

这时更合理的处理包括:

text 复制代码
减少数据库事务中的非必要远程调用

缩短连接和锁的持有时间

控制库存 RPC 并发量

检查慢事务和锁等待

验证连接池容量是否合理

而不是直接把 Order Service 扩容一倍。

第七步:验证修复效果

优化后,再观察同一类负载下:

text 复制代码
QPS

P50

P95

P99

数据库连接池等待

MySQL 锁等待

错误率

例如修复后:

text 复制代码
QPS = 800

P50 = 110ms

P99 = 260ms

如果连接池等待也明显下降,就能形成比较完整的证据链:

text 复制代码
发现 P99 异常
       |
       v
Trace 定位 Inventory RPC
       |
       v
进一步定位数据库访问
       |
       v
发现连接池等待
       |
       v
找到长事务原因
       |
       v
缩短事务并控制并发
       |
       v
验证 P99 恢复

这才是一次完整的性能排查。

不是找到某个看起来很慢的模块就结束,而是要找到造成延迟的具体机制,并通过修复前后的指标验证判断是否正确。


十二、性能排查中几个容易出现的误区

学完前面的内容,可以再把几个容易混淆的问题总结一下。

误区一:CPU 不高,说明服务不需要优化

不一定。

服务可能大量等待 MySQL、Redis、RPC 或线程池资源。

CPU 低不代表响应时间一定短。

误区二:P99 高,就说明所有请求都很慢

不一定。

如果 P50 正常,P99 却很高,可能主要是尾部慢请求问题。

需要结合完整延迟分布判断。

误区三:Trace 中最长的 Span 就是根因

不一定。

最长 Span 可能只是等待某个真正的下游瓶颈。

而且父 Span 可能包含子 Span 的执行时间。

所以需要沿调用链逐层定位。

误区四:增加 Pod 一定能够提高 QPS

不一定。

如果真正瓶颈在共享 MySQL,增加 Pod 可能进一步增加数据库压力。

误区五:消息处理不及时,就是 Kafka 性能不好

不一定。

可能是 Consumer 的 MySQL 写入慢,也可能是下游限流、重试或任务处理逻辑出现问题。

Kafka Lag 只是说明消费位置落后,不能直接指出根因。

误区六:平均响应时间正常,说明用户体验正常

不一定。

少量极慢请求会被平均值掩盖。

所以应该结合 P95、P99 和实际业务超时情况进行分析。


十三、总结:性能排查不是猜瓶颈,而是沿着证据逐层缩小范围

这一篇从 QPS、响应时间开始,逐步学习了 P50、P95、P99 和分布式 Trace,并结合 MySQL、Redis、Kafka 和 Kubernetes 分析了常见的性能瓶颈。

可以把整个性能排查过程概括为:

text 复制代码
用户反馈接口变慢
       |
       v
查看 QPS / P95 / P99 / 错误率
       |
       v
确定异常时间段与接口
       |
       v
分析慢请求 Trace
       |
       v
找到主要耗时路径
       |
       v
结合 Metrics 和 Logs
       |
       v
判断具体瓶颈
       |
       v
选择优化方案
       |
       v
通过相同负载验证改善

其中最值得记住的是以下几个结论。

第一,QPS 反映请求吞吐,不等于系统的最大承载能力。

必须结合延迟、错误率和资源使用情况判断系统是否过载。

第二,P99 反映尾部请求的响应体验。

平均延迟正常,不代表少量慢请求没有问题。

第三,Trace 用于理解跨服务请求的真实执行路径。

它不仅告诉我们某个调用持续多久,还能展示父子关系、并行执行和依赖路径。

第四,性能瓶颈不一定在 CPU。

数据库连接池等待、锁竞争、慢 SQL、Redis 回源和下游 RPC 都可能导致响应时间增加。

第五,扩容必须针对真正的瓶颈。

增加 Order Service Pod,并不能自动提升 MySQL 的处理能力。

最后用一句话总结:

微服务性能优化的关键,不是看到哪里慢就增加资源,而是先通过 QPS、P99 和 Trace 确认请求时间消耗在哪里,再根据真实瓶颈选择对应的优化方式。

到这里,我们已经完成了从微服务架构、运行基础、缓存、消息队列、数据库,到一致性、故障治理和性能排查的主要知识。

这些知识实际上是互相关联的。

例如:

text 复制代码
Redis 缓存击穿
      |
      v
MySQL 回源增加
      |
      v
数据库查询和连接等待上升
      |
      v
Inventory RPC 变慢
      |
      v
Order Service P99 上升
      |
      v
上游超时与重试
      |
      v
进一步增加下游压力

当我们能够沿着这样一条链路分析系统时,就不再只是记住某个中间件的功能,而是开始具备从整体角度理解分布式系统行为的能力。

下一篇作为系列补充篇,将继续讨论后台任务系统:从 Queue、Worker、ACK、Lease 到任务调度与故障恢复,看看长期运行和异步执行的任务究竟应该如何管理。

相关推荐
xunberg1 小时前
【Agent 生产化手记-02】Agent 怎么向人提问
人工智能·算法·架构
利威尔xu2 小时前
Go IM 源码实战(11):token 只是个 HASH,多端互踢的顺序错了就踢错人
后端·微服务·面试·架构·golang
充值内卷2 小时前
把 Visio 和 Axure 塞进一个 1.5 MB 的 exe:Visioxure 的架构与取舍
架构·axure
QianhangQianping2 小时前
一块大屏的数据在浏览器里怎么变成画面:背景、子屏、前景三层结构
前端·低代码·架构·编辑器·数据可视化·大屏可视化
时间售卖铺3 小时前
MySQL事务ACID与隔离级别深度解析
运维·数据库·mysql·云原生
每天一道题4 小时前
微服务为什么会发生级联故障?从 Timeout、Retry 到熔断、限流与 Backpressure
微服务·云原生·架构
无心水4 小时前
15、云原生架构设计理论与实践 核心考点与学习笔记
笔记·学习·云原生·架构师·软考·系统架构设计师·软考系统架构设计师
ESDWAN4 小时前
SD-WAN 是什么?工作原理、适用场景与选型要点
开发语言·网络·数据库·架构·php
闲云野鹤在人间5 小时前
头秃修炼手册:StorageClass+StatefulSet+ETCD 全套渡劫笔记
linux·数据库·笔记·云原生·kubernetes·etcd