220|接口 RT 突增排查:依赖、慢 SQL、GC、线程池、网络与流量变化

性能工程、可观测性与故障排查约 12 分钟阅读7,099 字符
知识库文档 221 / 251

考点 220:接口 RT 突增排查——依赖、慢 SQL、GC、线程池、网络与流量变化阅读时间:约 40-50 分钟 | 难度:中高级


一、引言:RT 突增,生产环境的午夜凶铃

凌晨两点,告警电话响起:核心接口 P99 延迟从 200ms 飙升至 8 秒,用户投诉暴涨。你从床上弹起来,打开监控大盘,CPU、内存、网络看起来都正常——问题出在哪?

RT(Response Time,响应时间) 是衡量接口性能最核心的指标。它不是你调优出来的"最佳值",而是系统当前状态的真实投影。当 RT 突然飙升,意味着系统某个环节的平衡被打破了。

本文将从六个维度系统拆解 RT 突增的排查思路:依赖服务、慢 SQL、GC、线程池、网络、流量变化。每个维度都配有排查工具、典型案例和可落地的解决方案。

先建立认知:RT 的构成

code
客户端 → [DNS] → [负载均衡] → [应用服务器] → [依赖服务/数据库/缓存]
         |←──────────────── RT(端到端)─────────────────→|

一个典型的 Java 微服务接口 RT 由以下部分组成:

环节 典型耗时 突增特征
网络传输 1-5ms 跨机房/跨地域时暴涨
容器/框架开销 <1ms 基本稳定
业务逻辑 1-50ms 随数据量/复杂度变化
下游依赖调用 10-200ms 最常见突增来源
数据库查询 5-100ms 慢 SQL 是主要元凶
GC 停顿 0-200ms 突刺型,间歇性
线程等待 0-∞ 线程池满时指数级增长

二、排查方法论:先定位,再深入

RT 突增排查最大的陷阱是凭直觉直接跳到某个环节。经验丰富的工程师会遵循一套系统的方法论。

2.1 黄金排查顺序

code
第一步:确认范围(单机/集群?单接口/全站?)
第二步:检查流量变化(QPS、并发数、请求体大小)
第三步:检查依赖调用(下游 RT、超时、熔断状态)
第四步:检查数据库(慢 SQL、锁等待、连接池)
第五步:检查 JVM(GC、线程池、堆内存)
第六步:检查网络(跨机房延迟、丢包、带宽)

2.2 关键监控指标速查表

指标 工具 异常阈值
接口 RT(P50/P90/P99) Prometheus + Grafana 超过基线 2 倍
QPS 同上 突增/突降均需关注
错误率 同上 >1%
下游调用 RT Skywalking/Jaeger 超过超时阈值 50%
慢 SQL 数量 Druid/Skywalking >10 条/分钟
GC 停顿时间 JVM 监控 >100ms/次
线程池活跃线程 Micrometer 接近 corePoolSize
网络延迟 ping/tcpping RTT > 10ms(内网)

三、维度一:依赖服务——RT 突增的头号嫌疑犯

3.1 为什么依赖服务最容易出问题

在微服务架构中,一个接口通常需要调用多个下游服务。假设你的接口依赖 5 个下游服务,每个超时 2 秒,最坏情况下(串行调用)RT 就是 10 秒。这就是依赖放大效应。

更隐蔽的是级联雪崩:下游服务 A 变慢 → 你的服务线程堆积 → 线程池耗尽 → 你的服务变慢 → 上游超时 → 整个链路崩溃。

3.2 排查步骤

第一步:看链路追踪

Spring Cloud Sleuth + Zipkin 或 Skywalking 是主力工具。关键看:

code
traceId: xxxxx
├── /api/order/create [8000ms] ← 你的接口
│   ├── userService.getUserInfo [50ms]   ← 正常
│   ├── inventoryService.checkStock [5000ms] ← 异常!通常 20ms
│   ├── couponService.queryCoupon [2000ms] ← 异常!通常 30ms
│   └── paymentService.createPayment [500ms] ← 正常

一眼定位:inventoryService 和 couponService 是主要耗时。

第二步:检查下游服务状态

bash
# 查看下游服务的 RT 监控
# 关注:是否全集群变慢?还是个别实例慢?
# 个别实例慢 → 可能是宿主机问题、实例 Full GC、网络抖动
# 全集群慢 → 可能是数据库慢、上游流量冲击、配置变更

第三步:检查熔断和限流状态

java
// Sentinel Dashboard 或 Hystrix Dashboard 查看
// 关键指标:
// - circuitBreakerOpen:熔断器是否打开
// - flowControlRejected:是否被限流
// - threadPoolRejected:线程池是否拒绝

3.3 常见根因与解决方案

根因 表现 解决方案
下游慢 SQL 依赖 RT 周期性飙升 推动下游优化 SQL
下游 GC 依赖 RT 呈毛刺状 推动下游 JVM 调优
下游线程池满 依赖 RT 指数增长 下游扩容或限流
超时设置不合理 请求堆积 设置合理的 connectTimeout/readTimeout
未使用连接池 每次新建连接 使用 HttpClient 连接池
序列化开销大 响应体过大 按需返回字段、启用压缩

3.4 代码级防御

java
// 不好的做法:无限等待
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(url, String.class); // 无超时

// 推荐做法:设置合理的超时和重试
@Bean
public RestTemplate restTemplate() {
    HttpComponentsClientHttpRequestFactory factory = 
        new HttpComponentsClientHttpRequestFactory();
    factory.setConnectTimeout(2000);     // 连接超时 2s
    factory.setReadTimeout(5000);        // 读取超时 5s
    factory.setConnectionRequestTimeout(1000); // 从连接池获取连接超时 1s
    return new RestTemplate(factory);
}

// 更好的做法:使用 Resilience4j 增加熔断保护
@CircuitBreaker(name = "inventoryService", fallbackMethod = "inventoryFallback")
public InventoryDTO checkStock(String skuId) {
    // 实际调用
}

四、维度二:慢 SQL——数据库层面的性能杀手

4.1 慢 SQL 为什么会导致 RT 突增

数据库是绝大多数业务系统的瓶颈点。一条 SQL 从 20ms 变成 2000ms,不是逐渐变慢——它往往是一个突变。常见触发点:

  • 数据量越过阈值:索引失效、全表扫描
  • 锁竞争:大事务持有行锁/表锁
  • 连接池耗尽:慢查询占满连接
  • 统计信息过期:优化器选错执行计划

4.2 排查流程

第一步:确认慢 SQL 来源

sql
-- MySQL:查看当前正在执行的慢查询
SHOW FULL PROCESSLIST;

-- 查看慢查询日志
-- my.cnf: slow_query_log=1, long_query_time=0.5
-- 或用 pt-query-digest 分析慢日志
pt-query-digest /var/log/mysql/slow.log

-- Druid 监控(Spring Boot 集成)
-- 访问 /druid/sql.html 查看 SQL 监控面板

第二步:分析执行计划

sql
EXPLAIN SELECT o.*, u.name FROM orders o 
JOIN users u ON o.user_id = u.id
WHERE o.create_time > '2024-01-01' AND o.status = 1;

-- 关键字段解读:
-- type: ALL(全表扫描)= 最差,ref/index = 较好,const = 最优
-- rows: 预估扫描行数,过大说明需要优化
-- Extra: Using filesort/Using temporary = 需要优化
-- key: 实际使用的索引,NULL = 未使用索引

第三步:检查锁等待

sql
-- MySQL 8.0 查看锁等待
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

4.3 常见慢 SQL 模式与优化

模式 示例 优化
缺失索引 WHERE user&amp;#95;id = 123 无索引 添加 INDEX idx&amp;#95;user&amp;#95;id(user&amp;#95;id)
索引失效 WHERE DATE(create&amp;#95;time) = '2024-01-01' 改为 WHERE create&amp;#95;time &amp;gt;= '2024-01-01' AND create&amp;#95;time &amp;lt; '2024-01-02'
隐式类型转换 WHERE phone = 13800138000(phone 是 VARCHAR) 改为 WHERE phone = '13800138000'
大分页 LIMIT 100000, 20 游标分页:WHERE id &amp;gt; last&amp;#95;id LIMIT 20
N+1 查询 循环中逐条查询 批量查询或 JOIN
大事务 事务中夹杂 RPC 调用 拆小事务,RPC 放事务外

4.4 连接池排查

java
// 连接池是 RT 突增的常见放大器
// 慢 SQL → 连接持有时间变长 → 连接池耗尽 → 新请求等待连接 → RT 进一步恶化

// Druid 监控关键指标:
// ActiveCount:当前活跃连接数
// WaitThreadCount:等待获取连接的线程数 ← 关注这个
// 如果 WaitThreadCount > 0,说明连接池不够用
yaml
# 连接池配置建议
spring:
  datasource:
    druid:
      initial-size: 10
      min-idle: 10
      max-active: 50          # 不宜过大,MySQL 连接数有限
      max-wait: 3000          # 获取连接最大等待时间
      test-on-borrow: true    # 验证连接有效性
      validation-query: SELECT 1
      # 关键:慢 SQL 监控
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 1000

五、维度三:GC——JVM 内部的"暂停风暴"

5.1 GC 如何影响 RT

GC(Garbage Collection)是 JVM 自动内存管理的核心机制,但它有一个致命副作用:Stop-The-World(STW)。在 STW 期间,所有应用线程暂停,RT 因此增加。

RT 突增与 GC 的关联模式:

code
正常:RT 基线 200ms,GC 偶尔 50ms STW,P99 约 300ms
异常:Full GC 频繁,每次 2s STW,P99 飙升至 2s+

5.2 排查 GC 问题

关键 GC 指标:

bash
# 查看 GC 日志(JDK 8)

- XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log


# 查看 GC 日志(JDK 11+)

- Xlog:gc*:file=/path/to/gc.log:time,level,tags


# 使用 jstat 实时查看
jstat -gc <pid> 1000 10   # 每秒打印一次,共 10 次

# 关键输出列:
# YGC/Full GC 次数
# YGCT/FGCT 总耗时
# GCT 总 GC 时间

分析 GC 日志的工具:

  • GCViewer:可视化 GC 日志,直观看到停顿分布
  • gceasy.io:在线分析,给出优化建议
  • Arthas:dashboard 命令查看 GC 情况

5.3 常见 GC 问题与排查

问题 表现 排查方向
Young GC 频繁 每秒多次,每次 10-30ms 年轻代太小 / 对象分配速率过高
Full GC 频繁 几分钟一次,每次 1-5s 老年代太小 / 内存泄漏 / 大对象直接进入老年代
单次 GC 时间过长 偶尔出现,每次 5s+ 堆内存过大 / 碎片化严重
晋升失败 Full GC 后内存仍高 老年代空间不足,考虑扩容

5.4 GC 调优实战

bash
# 典型 4C8G 应用的 GC 参数

- Xms4g -Xmx4g                    # 堆 4G,避免动态调整


- Xmn2g                           # 年轻代 2G(堆的一半)


- XX:+UseG1GC                     # 使用 G1(JDK 9+ 默认)


- XX:MaxGCPauseMillis=200         # 期望最大停顿 200ms


- XX:G1HeapRegionSize=4m          # Region 大小


- XX:InitiatingHeapOccupancyPercent=45  # 并发标记开始的堆占用阈值


- XX:+PrintGCDetails


- XX:+PrintGCDateStamps


- Xloggc:/var/log/app/gc.log


- XX:+UseGCLogFileRotation


- XX:NumberOfGCLogFiles=10


- XX:GCLogFileSize=100M


- XX:+HeapDumpOnOutOfMemoryError


- XX:HeapDumpPath=/var/log/app/heapdump.hprof

G1 调优核心思路:

  1. 设定合理的停顿目标:MaxGCPauseMillis 不是越短越好,太短反而导致 GC 频率增加
  2. 监控 Mixed GC:如果 Mixed GC 频繁且效果差,考虑增大 G1MixedGCLiveThresholdPercent
  3. 关注 Humongous 对象:G1 对大对象(超过 Region 50%)不友好,避免创建超大对象

5.5 内存泄漏排查

bash
# Step 1: 使用 jmap 导出堆转储
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

# Step 2: 使用 MAT 或 JProfiler 分析
# 重点关注:Dominator Tree(支配树)中的大对象
# 常见泄漏模式:
# - ThreadLocal 未清理
# - 静态集合无限增长
# - 连接未关闭
# - 监听器未取消注册

六、维度四:线程池——并发能力的"隐形天花板"

6.1 线程池如何导致 RT 突增

线程池是 Java 并发编程的基石,但配置不当会直接导致 RT 飙升:

code
请求进入 → 提交到线程池 
  → 核心线程有空闲 → 立即执行(RT 正常)
  → 核心线程忙 → 进入队列等待(RT 增加队列等待时间)
  → 队列满 → 创建新线程到 maxPoolSize(RT 增加创建开销)
  → 线程也满 → 执行拒绝策略(RT = 失败或降级)

线程池的"死亡螺旋":

code
某个请求变慢 → 线程占用时间变长 → 可用线程减少
→ 新请求排队等待 → 队列堆积 → 请求超时
→ 调用方重试 → 更多请求涌入 → 线程池彻底崩溃

6.2 排查线程池问题

bash
# Arthas 查看线程池状态
thread -b                    # 找出阻塞线程
thread -n 5                  # 最忙的 5 个线程
thread --state WAITING       # 查看 WAITING 线程

# 查看线程池 MBean
# 通过 jconsole 或 jmxterm 查看
# java.util.concurrent:type=ThreadPoolExecutor,name=*
# 关键属性:
# - ActiveCount:当前活跃线程
# - PoolSize:当前线程数
# - QueueSize:队列中等待的任务数 ← 最关键的指标
# - CompletedTaskCount:已完成任务数
# - RejectedCount:被拒绝的任务数

6.3 线程池配置原则

java
// 线程池配置:没有银弹,但有一套计算逻辑

// CPU 密集型任务
int cpuCore = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
    cpuCore + 1,           // corePoolSize
    cpuCore + 1,           // maxPoolSize(CPU 密集型不建超出核心数)
    60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadPoolExecutor.CallerRunsPolicy() // 让调用线程执行,防止丢任务
);

// IO 密集型任务
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
    cpuCore * 2,           // corePoolSize
    cpuCore * 4,           // maxPoolSize
    60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(5000),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

// 更精确的公式(考虑阻塞系数):
// 线程数 = CPU核心数 * (1 + 平均等待时间 / 平均计算时间)
// 例如:8核CPU,平均等待时间 100ms,平均计算时间 20ms
// 线程数 = 8 * (1 + 100/20) = 48

6.4 线程池监控最佳实践

java
@Component
public class ThreadPoolMonitor {
    
    @Scheduled(fixedRate = 10000)
    public void monitorThreadPools() {
        for (Map.Entry<String, ThreadPoolExecutor> entry : pools.entrySet()) {
            ThreadPoolExecutor pool = entry.getValue();
            String name = entry.getKey();
            
            int active = pool.getActiveCount();
            int queue = pool.getQueue().size();
            long completed = pool.getCompletedTaskCount();
            int poolSize = pool.getPoolSize();
            
            // 告警条件
            if (active >= pool.getCorePoolSize() * 0.8) {
                log.warn("线程池[{}]活跃线程接近上限: active={}, core={}", 
                    name, active, pool.getCorePoolSize());
            }
            if (queue > 100) {
                log.warn("线程池[{}]队列堆积: queue={}", name, queue);
            }
            
            // 上报到监控系统
            meterRegistry.gauge(name + ".active", active);
            meterRegistry.gauge(name + ".queue", queue);
        }
    }
}

七、维度五:网络——最容易被忽视的因子

7.1 网络问题为什么难排查

网络问题就像幽灵——时有时无,难以复现,且监控通常不覆盖网络层。但网络延迟对 RT 的影响是实打实的:

  • 内网延迟 1-5ms:正常
  • 跨可用区延迟 3-10ms:可接受
  • 跨地域延迟 30-100ms:对 RT 影响显著
  • 丢包或抖动:RT 出现毛刺,重试加剧问题

7.2 排查方法

bash
# 基础连通性
ping -c 100 <目标IP>     # 查看 RTT 和丢包率

# TCP 层延迟(避免 ICMP 被限速)
tcpping <目标IP> <port>

# 路径追踪
traceroute <目标IP>

# 抓包分析(终极手段)
tcpdump -i eth0 -w /tmp/capture.pcap host <目标IP> and port <port>
# 然后用 Wireshark 分析:
# - TCP 重传率
# - TCP 窗口大小
# - 三次握手耗时
# - TLS 握手耗时

# 查看网络统计
netstat -s | grep -i retransmit   # TCP 重传统计
ss -ti                              # 查看每个连接的 TCP 信息(RTT、重传等)

7.3 网络优化策略

策略 说明 效果
同机房部署 依赖服务部署在同一机房 延迟降至 1-3ms
连接池复用 避免频繁 TCP 握手 减少 1-3 次 RTT
HTTP/2 或 gRPC 多路复用,减少连接数 减少连接开销
减少序列化体积 启用 gzip、使用 protobuf 减少传输时间
TCP 参数调优 tcp_nodelay、keepalive 减少小包延迟

7.4 网络问题的典型特征

code
正常 RT 分布:  [200ms, 210ms, 205ms, 198ms, 220ms, 215ms]  ← 稳定
CPU 问题 RT 分布:[200ms, 300ms, 280ms, 250ms, 350ms, 310ms]  ← 整体上移
GC 问题 RT 分布: [200ms, 200ms, 2000ms, 200ms, 200ms, 1800ms] ← 毛刺
网络问题 RT 分布:[200ms, 500ms, 200ms, 800ms, 200ms, 1000ms] ← 随机毛刺 + 部分超时

经验法则:如果你的 P50 很低但 P99 很高,且毛刺不规律,
不伴随 CPU/GC 变化,大概率是网络问题。

八、维度六:流量变化——最直接的触发因素

8.1 流量变化如何影响 RT

流量变化是 RT 突增最直接的触发因素,但常被误认为是"代码问题":

流量变化类型 对 RT 的影响 典型场景
QPS 突增 线程池/连接池打满,请求排队 秒杀、大促、热点事件
请求体增大 序列化/反序列化耗时增加,带宽占用 批量导入、数据迁移
慢请求占比增加 少量慢请求拖垮线程池 大客户查询、报表导出
调用链变化 上游调用方式改变 从异步改为同步、增加了重试

8.2 排查方法

bash
# 1. 查看 QPS 变化
# Prometheus: rate(http_requests_total[5m])

# 2. 查看请求体大小
# Nginx access log 中的 $body_bytes_sent
# 或通过 Skywalking 的请求体大小监控

# 3. 分析请求分布
# 是否有某个用户/某个接口的请求量突然大增?
# 是否有某个入参值导致慢查询?

8.3 流量防护体系

java
// 多层次流量防护

// 第一层:负载均衡层限流(Nginx)
// limit_req_zone $binary_remote_addr zone=perip:10m rate=100r/s;
// limit_req zone=perip burst=20 nodelay;

// 第二层:网关层限流(Spring Cloud Gateway)
@Bean
public KeyResolver ipKeyResolver() {
    return exchange -> Mono.just(
        exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
    );
}

// 第三层:应用层限流(Sentinel)
@SentinelResource(value = "createOrder", 
    blockHandler = "createOrderBlockHandler")
public OrderDTO createOrder(CreateOrderRequest request) {
    // 业务逻辑
}

// 第四层:线程池隔离(Hystrix 线程池隔离 / Sentinel 线程隔离)
// 将不同接口分配到不同线程池,避免慢接口拖垮快接口

九、实战案例:一次完整的 RT 突增排查

9.1 背景

某电商系统,订单创建接口 /api/order/create 在 14:00 突然出现 RT 飙涨:

  • 基线 P99:200ms
  • 异常 P99:5000ms
  • QPS 无明显变化(约 500/s)
  • 错误率从 0.01% 升至 3%

9.2 排查过程

Step 1:看监控大盘

code
接口 RT  ↑ 从 14:00 开始飙升
CPU 使用率 ↑ 从 30% 升至 70%
GC 频率 ↑ Young GC 从 5次/min 升至 20次/min
线程池活跃线程 ↑ 接近 maxPoolSize

Step 2:看链路追踪

code
traceId: abc123
├── /api/order/create [5000ms]
│   ├── userService.getUserInfo [50ms]     ← 正常
│   ├── inventoryService.lockStock [3000ms] ← 异常!基线 50ms
│   ├── priceService.calculatePrice [800ms] ← 异常!基线 30ms
│   └── orderService.saveOrder [100ms]     ← 正常

Step 3:分析 inventoryService 慢的原因

进入 inventoryService 的链路追踪:

code
├── inventoryService.lockStock [3000ms]
│   ├── db.queryStock [30ms]        ← 正常
│   ├── db.lockRow [2500ms]         ← 异常!行锁等待
│   └── cache.updateStock [10ms]    ← 正常

Step 4:找到根因

db.lockRow 慢是因为当天上午上线了一个批量操作,该操作在事务中持有大批行锁,导致订单创建的库存锁定发生锁等待。

Step 5:解决

  1. 临时:将批量操作的事务隔离级别调整为 READ COMMITTED
  2. 长期:将批量操作拆分为小批量 + 每批独立事务

9.3 排查时间线

code
14:00 - 告警触发
14:05 - 确认影响范围,排除网络和流量变化
14:10 - 通过链路追踪定位到 inventoryService
14:15 - 深入 inventoryService 发现锁等待
14:20 - 联系相关团队确认变更
14:25 - 确认根因并回滚变更
14:30 - RT 恢复正常
总耗时:30 分钟

十、排查工具速查表

场景 工具 命令/入口
链路追踪 Skywalking / Jaeger Dashboard 查看 trace
慢 SQL Druid / 慢查询日志 /druid/sql.html
JVM 实时诊断 Arthas dashboard / thread / jad
GC 分析 GCViewer / gceasy.io 上传 GC 日志
线程堆栈 jstack jstack &amp;lt;pid&amp;gt; &amp;gt; thread.dump
堆内存分析 MAT / JProfiler 分析 heap dump
网络诊断 tcpdump + Wireshark 抓包分析
实时监控 Prometheus + Grafana 自定义 Dashboard
日志聚合 ELK / SLS 关键字搜索 + 时间聚合

十一、总结:RT 突增排查的"心法"

11.1 排查优先级口诀

code
流量先看,依赖紧追,
SQL 慢查,GC 别漏,
线程池满,网络兜底。

11.2 建立事前防御体系

层次 措施 目的
代码层 超时设置、熔断降级、重试策略 防止依赖故障扩散
框架层 线程池监控、连接池监控、限流 感知系统容量
JVM 层 GC 日志、堆转储、JMX 监控 快速定位内存问题
基础设施 网络监控、数据库慢查询告警 覆盖底层问题
流程层 变更记录、灰度发布、回滚预案 快速止损

11.3 一句话总结

RT 突增排查的核心不是"找到问题",而是用最短时间缩小排查范围。建立完善的监控体系,让 90% 的问题在 5 分钟内定位到具体环节,剩下的 10% 才是真正需要深度排查的疑难杂症。


文章配图
文章配图

文章配图
文章配图

文章配图
文章配图

文章配图
文章配图

文章配图
文章配图

文章配图
文章配图

文章配图
文章配图

文章配图
文章配图


十二、面试问答

以下问题从面试官视角出发,考察对 RT 排查的系统性理解和实战经验。

Q1:线上某个接口的 P99 从 200ms 飙升至 5s,但 P50 几乎没变,最可能的原因是什么?

P50 不变但 P99 飙升,说明大部分请求正常,但存在少量长尾请求。最可能的原因:

  1. GC 停顿:Full GC 导致部分请求被 STW 停顿,P99 出现毛刺
  2. 网络抖动:部分 TCP 连接出现重传,导致个别请求延迟高
  3. 锁竞争:少量请求在数据库行锁或 JVM 锁上等待
  4. 线程池排队:峰值时刻线程池打满,少量请求排队等待

排查思路:先看 GC 日志(排除 GC),再看 P99 是否有规律(周期性 = GC,随机 = 网络),最后通过链路追踪看具体慢在哪个环节。

Q2:为什么说"下游服务变慢"是 RT 突增最常见的根因?请从架构角度解释依赖放大效应。

微服务架构中,一个接口通常需要调用多个下游服务。依赖放大效应体现在:

  1. 串行累积:如果接口串行调用 5 个下游,每个 200ms,RT 就是 1s。如果其中一个变慢至 5s,RT 就是 5.5s
  2. 线程池放大:下游变慢 → 线程占用时间变长 → 可用线程减少 → 新请求排队 → 排队时间叠加到 RT
  3. 重试放大:下游超时触发重试 → 请求量翻倍 → 下游压力更大 → 更慢 → 恶性循环
  4. 级联雪崩:你的服务变慢 → 上游超时 → 上游线程池满 → 上游也变慢 → 整个链路崩溃

这就是为什么一个底层服务的慢 SQL 可能导致整个产品页面不可用。

Q3:线程池的核心线程数你一般怎么设置?给你一个 8 核 CPU 的服务器,处理一个平均耗时 50ms(其中 I/O 等待 40ms)的接口,你会设置多大的线程池?

使用阻塞系数公式:

code
最优线程数 = CPU核心数 × (1 + 平均等待时间 / 平均计算时间)

本题:8 核,总耗时 50ms,IO 等待 40ms,计算时间 = 50 - 40 = 10ms

code
最优线程数 = 8 × (1 + 40/10) = 8 × 5 = 40

所以 corePoolSize 设为 40 左右。但实际还需要考虑:

  • 数据库连接池大小:如果只有 20 个连接,40 个线程大部分在等连接
  • 下游服务承载能力:40 个并发是否超过下游限制
  • 内存开销:每个线程 1MB 栈空间,40 个线程 = 40MB

实际配置建议:corePoolSize=40, maxPoolSize=60, queue=1000,同时监控队列堆积情况动态调整。

Q4:G1 和 CMS 选哪个?什么场景下 G1 可能不如 CMS?

优先选 G1(JDK 9+ 默认),G1 的优势:

  • 可预测的停顿时间(MaxGCPauseMillis)
  • 自动碎片整理
  • 大堆(>4G)表现优秀

G1 不如 CMS 的场景:

  • 堆较小(<4G):G1 的 Remember Set 开销占比大,CMS 更轻量
  • 低延迟要求极高(<10ms):G1 的 Mixed GC 可能仍有百毫秒级停顿,CMS 的并发标记+并发清除停顿更短(但 CMS 有碎片化问题和 Concurrent Mode Failure 风险)
  • 大对象密集:G1 对超过 Region 50% 的 Humongous 对象处理笨拙

实际建议: JDK 11+ 用 G1 基本不会错,CMS 在 JDK 14 已被移除。如果 G1 满足不了延迟要求,考虑 ZGC 或 Shenandoah。

Q5:线上数据库突然出现大量慢查询,但 SQL 本身没有变化,执行计划也没变,可能是什么原因?

SQL 和执行计划都没变,但突然变慢,可能原因:

  1. 数据量变化:表数据量增长导致索引树层数增加,或扫描行数增大
  2. 锁竞争:其他事务持有行锁/间隙锁,导致当前查询等待
  3. Buffer Pool 命中率下降:其他查询占满 Buffer Pool,当前查询需要大量磁盘 I/O
  4. 连接数打满:数据库连接数接近上限,新查询需要排队
  5. 磁盘 I/O 瓶颈:宿主机其他进程抢占磁盘 I/O
  6. 统计信息与实际数据分布偏差:虽然执行计划看起来一样,但 optimizer 基于统计信息做的决策可能已经不准确

排查:先看 SHOW PROCESSLIST 确认是否有锁等待,再看 SHOW ENGINE INNODB STATUS 检查 Buffer Pool 命中率和锁情况,最后检查磁盘 I/O 和连接数。

Q6:你们的系统做了哪些 RT 突增的预防措施?如果让你从零搭建一套保障体系,你会怎么做?

已有措施:

  1. 全链路追踪(Skywalking),关键接口设置 RT 告警阈值
  2. 依赖服务熔断降级(Sentinel),核心链路有 fallback
  3. 慢 SQL 监控(Druid + 告警),超过 500ms 自动通知
  4. JVM 监控(GC 频率、堆内存、线程池),接入 Prometheus + Grafana
  5. 限流(Nginx + Sentinel),防止突发流量打垮系统
  6. 灰度发布,每次变更只影响 10% 流量,快速回滚

从零搭建的思路:

  1. 第一优先级:监控 —— 没有监控就没有排查依据。先上 APM(Skywalking),再补基础设施监控(Prometheus)
  2. 第二优先级:告警 —— 接口 RT 告警、错误率告警、慢 SQL 告警、GC 告警。告警要分级,避免告警疲劳
  3. 第三优先级:防御 —— 超时、熔断、限流、降级,防止故障扩散
  4. 第四优先级:演练 —— 定期做故障注入和混沌工程,验证防御体系是否有效
  5. 第五优先级:文档 —— 每次故障的排查过程形成文档,沉淀排查手册

📌 本文是"Java 技术专家与架构师面试:250 个高频考点"系列的第 220 篇。 下一篇预告:考点 221|分布式事务:2PC、TCC、Saga、消息最终一致性。