前面我们学习了微服务的超时、重试、熔断、限流和 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 到任务调度与故障恢复,看看长期运行和异步执行的任务究竟应该如何管理。