Skip to content

十五、线上故障排查(10 题)

15.1 ★★★★线上服务 CPU 飙升到 100%,你的排查思路和步骤是什么?(结合 top + top -Hp + jstack 分析线程栈)

📖 点击查看答案

排查步骤(经典三件套)

1. 定位进程top 找出 CPU 最高的 Java 进程

bash
top
# 假设找到 PID = 12345 的 Java 进程 CPU 100%

2. 定位线程top -Hp <pid> 查看进程内线程 CPU 占用

bash
top -Hp 12345
# 找到 CPU 最高的线程 TID = 12378(十进制)

3. 线程 ID 转十六进制:jstack 输出的 nid 是十六进制

bash
printf "%x\n" 12378
# 输出 305a

4. dump 线程栈并分析

bash
jstack 12345 > /tmp/thread_dump.txt
grep "nid=0x305a" -A 30 /tmp/thread_dump.txt
# 查看该线程在执行什么代码

常见 CPU 飙高原因

  1. 死循环 / 无限递归:业务逻辑 bug
  2. 频繁 Full GC:jstat 查看 GC 情况
  3. 正则回溯:恶意输入导致正则灾难
  4. 序列化/反序列化:大对象循环引用
  5. 加密/哈希计算:如大量密码加密
  6. 线程上下文切换过多:线程数过多
bash
# 辅助命令
# 查看 GC 情况
jstat -gcutil 12345 1000 5

# 查看线程数
jstack 12345 | grep "java.lang.Thread.State" | wc -l

# 查看死锁
jstack -l 12345 | grep -A 20 "Found .* deadlock"

# 一键排查脚本
#!/bin/bash
pid=$1
tid=$(top -Hp $pid -n 1 | awk 'NR>7 {print $1, $9}' | sort -k2 -rn | head -1 | awk '{print $1}')
hex=$(printf "%x\n" $tid)
jstack $pid | grep "nid=0x$hex" -A 30

其他工具

  • Arthasthread -n 3 直接看 CPU 最高的 3 个线程
  • async-profiler:生成火焰图,定位方法级 CPU 消耗
  • JProfiler / VisualVM:GUI 工具实时监控

Arthas 一键排查

bash
# 启动 arthas
java -jar arthas-boot.jar <pid>
# 查看 CPU 占用最高的线程
thread -n 3
# 查看具体方法耗时
trace com.example.OrderService createOrder

15.2 ★★★★线上服务频繁 Full GC / OOM 如何快速定位问题?可能的原因有哪些?如何从 GC 日志中定位问题?Heap Dump 的分析思路是什么?

📖 点击查看答案

频繁 Full GC / OOM 常见原因

  1. 内存泄漏:静态集合持有对象不释放、ThreadLocal 未 remove
  2. 大对象:大 SQL 结果集、大文件加载、大 List
  3. 内存分配过快:高并发创建对象,Young GC 跟不上
  4. 堆配置不合理:堆太小、年轻代/老年代比例不当
  5. Metaspace 溢出:动态加载类过多(如反射、Groovy)
  6. Direct Memory 溢出:NIO ByteBuffer 堆外内存泄漏
  7. System.gc() 被频繁调用:代码或框架主动触发

排查步骤

1. 查看 GC 日志

bash
# JVM 参数建议
-Xms4g -Xmx4g -Xmn2g
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/path/gc.log
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

# 分析 GC 日志
# Full GC 频率、耗时、回收后老年代大小
# 如 Full GC 后老年代没怎么下降 → 内存泄漏

2. jstat 实时监控

bash
jstat -gcutil <pid> 1000 10
# 观察 O(老年代使用率)是否持续增长不回收

3. Dump 堆内存分析

bash
# 手动 dump
jmap -dump:format=b,file=heap.hprof <pid>

# OOM 自动 dump(推荐 JVM 参数)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/dump

# MAT 分析
# 1. 打开 hprof 文件
# 2. 查看 Dominator Tree(支配树)找最大对象
# 3. 查看 Histogram 按类统计对象数量
# 4. Leak Suspects 报告自动定位泄漏点
# 5. 查看对象的 GC Root 引用链

4. 常见内存泄漏场景与排查

java
// 场景1:静态集合泄漏
static Map<String, Object> cache = new HashMap<>();
// 缓存无限增长,未设置淘汰策略 → 用 Caffeine/Guava 设置 maxSize/expire

// 场景2:ThreadLocal 泄漏
ThreadLocal<byte[]> tl = new ThreadLocal<>();
tl.set(new byte[1024 * 1024]);
// 线程池中线程复用,未 remove → finally 中 tl.remove()

// 场景3:监听器未移除
eventBus.register(listener);
// 忘记 unregister → 在 destroy 方法中移除

// 场景4:数据库连接/IO 未关闭
Connection conn = dataSource.getConnection();
// 异常时未关闭 → try-with-resources

MAT 分析要点

  1. Leak Suspects:自动报告最可能的泄漏点
  2. Dominator Tree:按对象持有内存大小排序
  3. Histogram:按类查看实例数和内存占用
  4. Path to GC Roots:查看对象为何无法被回收(引用链)
  5. OQL 查询SELECT * FROM java.lang.String s WHERE s.count > 1000

OOM 类型

  • Java heap space:堆内存不足
  • GC overhead limit exceeded:GC 花费超过 98% 时间但回收少于 2%
  • Metaspace:元空间不足
  • Direct buffer memory:堆外内存不足
  • unable to create new native thread:线程数过多

15.3 ★★线上服务出现线程死锁如何排查?jstack 的使用方法是什么?如何分析线程堆栈?

📖 点击查看答案

排查步骤

1. 确认死锁jstack 会自动检测死锁并打印

bash
jstack <pid> > /tmp/jstack.txt
grep -A 20 "Found .* deadlock" /tmp/jstack.txt
# 或直接
jstack -l <pid> | grep -A 30 "deadlock"

2. 分析死锁信息

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8b0c0063e8 (object 0x000000076b6a3e98, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f8b0c003ed8 (object 0x000000076b6a3ea8, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
    at com.example.Service.methodB(Service.java:20)
    - waiting to lock <0x000000076b6a3e98>
    - locked <0x000000076b6a3ea8>
"Thread-0":
    at com.example.Service.methodA(Service.java:10)
    - waiting to lock <0x000000076b6a3ea8>
    - locked <0x000000076b6a3e98>

3. 关键信息解读

  • waiting to lock <0x...>:等待获取的锁
  • locked <0x...>:已持有的锁
  • 锁对象地址对应,即可确认死锁环路

经典死锁示例

java
class Service {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void methodA() {
        synchronized (lock1) {       // 持有 lock1
            synchronized (lock2) {   // 等待 lock2
                // ...
            }
        }
    }
    public void methodB() {
        synchronized (lock2) {       // 持有 lock2
            synchronized (lock1) {   // 等待 lock1
                // ...
            }
        }
    }
}

死锁排查工具

  • jstack:命令行,最常用
  • Arthasthread -b 查找阻塞其他线程的线程
  • VisualVM / JConsole:可视化查看线程状态和死锁
  • JProfiler / MAT:高级分析

Arthas 一键排查

bash
# 查找阻塞其他线程的线程(死锁根源)
thread -b
# 查看所有线程状态
thread
# 查看特定线程
thread <thread_id>

死锁预防

  1. 固定锁顺序:所有线程按相同顺序获取锁
  2. 尝试获取锁带超时tryLock(timeout) 避免永久阻塞
  3. 减少锁粒度:使用细粒度锁
  4. 无锁编程:CAS、ConcurrentHashMap、队列
  5. 避免嵌套锁:尽量减少同步块嵌套
java
// 预防方案1:tryLock 超时
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
    try {
        if (lock2.tryLock(1, TimeUnit.SECONDS)) {
            try { /* ... */ } finally { lock2.unlock(); }
        }
    } finally { lock1.unlock(); }
}

// 预防方案2:按 hash 排序加锁
void transfer(Account from, Account to) {
    Account first = from.hashCode() < to.hashCode() ? from : to;
    Account second = from.hashCode() < to.hashCode() ? to : from;
    synchronized (first) {
        synchronized (second) { /* ... */ }
    }
}

15.4 ★★★某接口响应突然变慢(从 50ms 到 5s),如何排查?从哪些层面入手分析?

📖 点击查看答案

分层排查思路

1. 应用层

  • 是否有慢方法/循环(APM 链路追踪:SkyWalking/Pinpoint)
  • 是否 Full GC 导致 STW(jstat 查看)
  • 线程池是否打满(jstack 查看 BLOCKED 线程)
  • 锁竞争是否激烈

2. 数据库层

  • 是否有慢 SQL(慢查询日志)
  • 是否索引失效(EXPLAIN 分析执行计划)
  • 是否锁等待(SHOW PROCESSLISTinnodb_lock_waits
  • 连接池是否耗尽

3. 中间件层

  • Redis 是否慢查询(SLOWLOG GET)、大 key、热点 key
  • MQ 是否消费堆积、消费速度跟不上
  • 网络是否抖动(ping/telnet)

4. 系统层

  • CPU/内存/磁盘 IO/网络是否异常(top/iostat/vmstat)
  • 是否被其他进程影响

5. 外部依赖

  • 下游服务是否变慢(RPC 超时配置)
  • 第三方 API 是否限流/超时

排查工具链

bash
# 1. 查看接口监控(APM)
# SkyWalking 链路追踪,定位耗时在哪个环节

# 2. 查看应用状态
jstat -gcutil <pid> 1000 5    # GC 情况
jstack <pid> | grep -A 5 "BLOCKED"  # 阻塞线程

# 3. 数据库排查
SHOW PROCESSLIST;             # 当前执行的 SQL
SHOW ENGINE INNODB STATUS;    # 锁等待
# 慢查询日志
pt-query-digest /var/log/mysql/slow.log

# 4. Redis 排查
redis-cli SLOWLOG GET 10      # 慢查询
redis-cli --bigkeys           # 大 key
redis-cli INFO clients        # 连接数

# 5. 系统资源
top                           # CPU
iostat -x 1                   # 磁盘 IO
vmstat 1                      # 系统状态

常见原因与解决

原因现象解决方案
慢 SQL数据库耗时高加索引、优化 SQL、分页
Full GC间歇性卡顿调整 JVM 参数、排查内存泄漏
锁等待线程 BLOCKED优化锁粒度、避免长事务
连接池耗尽获取连接超时增大连接池、优化连接释放
Redis 大 keyRedis 耗时高拆分大 key、pipeline
网络抖动RPC 超时重试、超时降级
下游变慢链路耗时增加熔断降级、异步化

Arthas 逐层定位

bash
# 监控方法耗时
trace com.example.OrderService createOrder -n 5
# 查看方法入参出参
watch com.example.OrderService createOrder "{params, returnObj}" -x 2
# 反编译确认线上代码
jad com.example.OrderService

15.5 ★★如何排查 MySQL 慢查询?慢查询日志如何分析?pt-query-digest / EXPLAIN 怎么使用?数据库 CPU 飙高如何定位到具体 SQL?

📖 点击查看答案

1. 开启慢查询日志

sql
-- 动态开启
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;        -- 超过 1s 记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL log_queries_not_using_indexes = ON;  -- 记录未用索引的查询

-- 查看配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

2. pt-query-digest 分析慢日志

bash
# 基本用法
pt-query-digest /var/log/mysql/slow.log > slow_report.txt

# 输出包含:
# - 统计概览:总查询数、唯一查询数、总耗时
# - 按耗时排序的 TOP 查询
# - 每个查询的执行次数、平均耗时、返回行数
# - 示例 SQL

# 实时分析
tail -f /var/log/mysql/slow.log | pt-query-digest --

# 按数据库过滤
pt-query-digest --filter '$event->{db} =~ /order_db/' slow.log

3. EXPLAIN 分析执行计划

sql
EXPLAIN SELECT * FROM t_order WHERE user_id = 1001;

-- 关注字段:
-- type:访问类型(system > const > eq_ref > ref > range > index > ALL)
--       ALL 全表扫描需优化,ref/range 较好
-- key:实际使用的索引
-- rows:预估扫描行数(越少越好)
-- Extra:
--   Using index:覆盖索引,好
--   Using filesort:额外排序,需优化
--   Using temporary:临时表,需优化
--   Using where:使用 WHERE 过滤

-- 查看详细成本
EXPLAIN FORMAT=JSON SELECT ...;
-- 或
EXPLAIN ANALYZE SELECT ...;  -- MySQL 8.0+

4. 数据库 CPU 飙高定位 SQL

sql
-- 查看当前执行的 SQL
SHOW PROCESSLIST;
SHOW FULL PROCESSLIST;

-- 查看耗时最长的查询
SELECT * FROM information_schema.processlist
WHERE time > 10 ORDER BY time DESC;

-- 查看正在执行的 SQL 详情
SELECT * FROM performance_schema.events_statements_current;

-- InnoDB 引擎状态(锁等待、死锁)
SHOW ENGINE INNODB STATUS\G

-- 查看锁等待
SELECT * FROM information_schema.innodb_lock_waits;
SELECT * FROM information_schema.innodb_trx;

-- 性能分析(MySQL 5.7+ performance_schema)
-- 按耗时 TOP SQL
SELECT digest_text, count_star, avg_timer_wait/1000000000 avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY avg_timer_wait DESC LIMIT 10;

5. 常见慢查询原因与优化

sql
-- 原因1:未走索引
-- 优化:添加合适索引
ALTER TABLE t_order ADD INDEX idx_user_id (user_id);

-- 原因2:索引失效
-- 函数操作:WHERE DATE(create_time) = '2024-01-01' → 改为范围查询
-- 隐式转换:WHERE varchar_col = 123 → WHERE varchar_col = '123'
-- 前导模糊:WHERE name LIKE '%abc' → 全文索引或后缀模糊
-- OR 连接:WHERE a=1 OR b=2(b无索引)→ UNION ALL 或都加索引

-- 原因3:返回数据量大
SELECT * FROM t_order WHERE ... → SELECT id, name FROM ...
-- 分页:LIMIT 0, 10 而非全量

-- 原因4:JOIN 关联过多
-- 小表驱动大表,被驱动表的关联字段加索引

-- 原因5:深分页
SELECT * FROM t_order LIMIT 1000000, 10
-- 优化:游标分页
SELECT * FROM t_order WHERE id > 1000000 LIMIT 10

15.6 ★线上 Redis 内存使用率接近 maxmemory,如何排查和解决?

📖 点击查看答案

排查步骤

1. 查看内存概况

bash
redis-cli INFO memory
# 关注:
# used_memory:已用内存
# used_memory_rss:操作系统分配的内存
# maxmemory:最大内存限制
# mem_fragmentation_ratio:内存碎片率(>1.5 需关注)
# evicted_keys:因内存淘汰的 key 数量

redis-cli INFO stats | grep evicted  # 查看是否在淘汰 key

2. 分析 key 分布

bash
# 大 key 扫描
redis-cli --bigkeys
# 或
redis-cli --memkeys  # Redis 6.0+ 按内存分析

# 扫描指定前缀的 key 数量
redis-cli --scan --pattern 'session:*' | wc -l

# 查看单个 key 占用内存
MEMORY USAGE <key> SAMPLES 1000

3. 定位大 key

bash
# 离线分析 RDB 文件
rdb-tools --command memory dump.rdb > memory.csv
# 或
redis-rdb-tools dump.rdb -f memory.csv

# 实时分析(redis 4.0+)
MEMORY STATS

常见原因与解决方案

原因解决方案
大 key(如大 List/Hash)拆分为多个小 key,或迁移到 DB
未设置过期时间的 key 堆积设置 TTL,定期清理
缓存无淘汰策略或策略不当配置 maxmemory-policy allkeys-lru
热点 key 占用大分片存储、压缩 value
内存碎片过多重启 Redis 或 MEMORY PURGE(jemalloc)

解决方案

bash
# 1. 设置淘汰策略
CONFIG SET maxmemory-policy allkeys-lru
# 可选策略:
#   noeviction:不淘汰(默认,OOM 时拒绝写入)
#   allkeys-lru:所有 key 中 LRU 淘汰
#   volatile-lru:设了 TTL 的 key 中 LRU 淘汰
#   allkeys-random:随机淘汰
#   volatile-ttl:淘汰即将过期的

# 2. 批量删除大 key(避免阻塞)
redis-cli --scan --pattern 'temp:*' | xargs redis-cli DEL
# Redis 4.0+ 异步删除
UNLINK key1 key2 key3

# 3. 设置 TTL
EXPIRE key 3600

# 4. 大 key 拆分
# 原:HSET user:1001 {name, age, addr, history(大)...}
# 拆:HSET user:1001:basic {name, age}
#     HSET user:1001:addr {addr}
#     LIST user:1001:history ...

预防措施

  1. 单 key 大小不超过 10KB(Hash/List 不超过 5000 元素)
  2. 所有缓存必须设置 TTL
  3. 监控内存使用率(告警阈值 80%)
  4. 定期巡检大 key
  5. 使用 Redis Cluster 分散压力

15.7 ★★消息队列消息堆积、消费失败如何处理?MQ 消息大量堆积来不及消费,如何应急处理?

📖 点击查看答案

消息堆积排查

  1. 查看消费者状态:是否存活、是否在消费
  2. 查看消费速度(TPS)vs 生产速度
  3. 查看是否有消费失败导致重试堆积
  4. 查看消费者线程数是否足够
  5. 查看下游依赖(DB/Redis)是否变慢

应急处理方案

1. 临时扩容消费者

  • 增加消费者实例数(注意:Kafka 分区数限制了并发度)
  • 增加消费者线程池大小
  • 临时机器扩容

2. 快速消费跳过非核心逻辑

java
// 临时降级:跳过非核心处理,仅记录日志
@RocketMQMessageListener(topic = "order_topic")
public class OrderConsumer implements RocketMQListener<String> {
    public void onMessage(String msg) {
        try {
            // 临时降级:仅入库,不调用下游
            orderMapper.insert(parse(msg));
            // downstreamService.notify(...);  // 暂时跳过
        } catch (Exception e) {
            log.error("消费失败: {}", msg, e);
            // 临时改为本地存储,后续补偿
            failedMsgMapper.insert(msg);
        }
    }
}

3. 消息转移 + 离线消费

java
// 方案A:新建一个临时 Topic,用快速消费者把消息搬运过去
// 原消费者只做转发,新消费者慢慢处理
public void onMessage(String msg) {
    producer.send("order_topic_backup", msg);  // 转发
}

// 方案B:多个消费者分组并行处理(Kafka 增加分区)

4. Kafka 专项方案

bash
# 增加分区数(注意:会破坏顺序性)
kafka-topics.sh --alter --topic order_topic --partitions 64

# 增加消费者实例到与分区数相同
# 重置消费位点(跳过堆积,谨慎使用)
kafka-consumer-groups.sh --reset-offsets --group order_group \
    --topic order_topic --to-latest --execute

消费失败处理

java
// 1. 重试 + 死信队列
public void onMessage(String msg) {
    try {
        process(msg);
    } catch (Exception e) {
        int retryCount = getRetryCount(msg);
        if (retryCount < 3) {
            // 延迟重试
            producer.send("order_retry_topic", msg, retryCount + 1);
        } else {
            // 进入死信队列,人工处理
            producer.send("order_dlt_topic", msg);
            alertService.alert("消息进入死信: " + msg);
        }
    }
}

// 2. 本地存储 + 定时补偿
public void onMessage(String msg) {
    try {
        process(msg);
    } catch (Exception e) {
        failedMsgMapper.insert(msg, e.getMessage());  // 落库
        // 定时任务重试 failedMsgMapper 中未处理的消息
    }
}

预防措施

  1. 监控消费滞后(Lag)告警
  2. 设置合理的重试次数和死信队列
  3. 消费者幂等设计(避免重试导致重复消费)
  4. 消费者批量处理提高吞吐
  5. 合理设置 prefetch(Kafka fetch.min.bytes、RocketMQ pullBatchSize

15.8 ★线上服务出现连接池耗尽(数据库/Redis/HTTP)如何排查?如何合理设置连接池参数?

📖 点击查看答案

排查步骤

1. 数据库连接池耗尽

bash
# 现象:获取连接超时 "Cannot get a connection, pool error timeout"
# 查看 Druid 状态
curl http://app:port/druid/datasource.json
# 关注:activeCount、poolingCount、waitThreadCount

# MySQL 侧查看连接
SHOW PROCESSLIST;
SELECT * FROM information_schema.processlist WHERE db = 'order_db';

2. Redis 连接池耗尽

bash
# 现象:JedisException: Could not get a resource from the pool
redis-cli INFO clients          # 当前连接数
redis-cli CONFIG GET maxclients # 最大连接数

3. HTTP 连接池耗尽

bash
# 现象:Timeout waiting for connection from pool
# 查看 HttpClient 连接池状态(需埋点)
# 或 jstack 查看线程是否在等待连接
jstack <pid> | grep -A 5 "POOL"

常见原因

  1. 连接泄漏:获取连接后未关闭(异常未走 finally)
  2. 慢查询/慢调用:连接占用时间长
  3. 并发突增:连接数不足
  4. 连接池参数配置不当:maxActive 太小、超时太长

合理参数配置

yaml
# HikariCP(推荐,性能最好)
spring:
  datasource:
    hikari:
      maximum-pool-size: 20          # 最大连接数(公式:核心数 * 2 + 有效磁盘数)
      minimum-idle: 10               # 最小空闲连接
      connection-timeout: 30000      # 获取连接超时(30s)
      idle-timeout: 600000           # 空闲连接超时(10min)
      max-lifetime: 1800000          # 连接最大生命周期(30min)
      leak-detection-threshold: 60000 # 连接泄漏检测(60s)

# Druid
spring:
  datasource:
    druid:
      initial-size: 5
      min-idle: 10
      max-active: 20
      max-wait: 60000                # 获取连接超时
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      remove-abandoned: true         # 回收泄漏连接
      remove-abandoned-timeout: 300  # 泄漏超时
      log-abandoned: true            # 记录泄漏堆栈

# Redis 连接池(Lettuce)
spring:
  redis:
    lettuce:
      pool:
        max-active: 50
        max-idle: 20
        min-idle: 5
        max-wait: 3000ms

连接数计算公式

  • 数据库连接池大小 ≠ 越大越好
  • PostgreSQL 官方建议:connections = ((core_count * 2) + effective_spindle_count)
  • 一般 20-50 即可满足大多数场景
  • 过大反而增加 DB 负载(上下文切换、锁竞争)

代码层面防泄漏

java
// 正确:try-with-resources
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    // ...
}  // 自动关闭

// 错误:手动关闭易遗漏
Connection conn = dataSource.getConnection();
// ... 异常时未关闭 conn → 泄漏

// ThreadLocal 连接需在 finally 中 remove
ThreadLocal<Connection> tl = new ThreadLocal<>();
try {
    tl.set(dataSource.getConnection());
    // ...
} finally {
    if (tl.get() != null) {
        tl.get().close();
        tl.remove();  // 必须 remove
    }
}

预防措施

  1. 使用 try-with-resources 自动释放
  2. 开启连接泄漏检测(Druid removeAbandoned
  3. 监控连接池使用率(活跃连接/最大连接 > 80% 告警)
  4. 合理设置超时(获取连接、SQL 执行、HTTP 调用)
  5. 排查慢 SQL/慢调用,减少连接占用时间

15.9 ★线上服务突然出现大量 502 / 503 错误,排查思路是什么?

📖 点击查看答案

502 vs 503 区别

  • 502 Bad Gateway:网关/代理收到上游无效响应(上游服务挂了/重启/无响应)
  • 503 Service Unavailable:服务暂时不可用(过载、限流、熔断、健康检查失败)

排查思路

1. 确认影响范围

  • 是单个服务还是多个服务?
  • 是部分实例还是全部实例?
  • 是持续还是间歇?

2. 502 排查(网关层)

bash
# Nginx 502:后端服务无响应
# 查看 Nginx error.log
tail -f /var/log/nginx/error.log
# 常见:connect() failed (111: Connection refused)
#   → 后端服务挂了/未启动/端口不对
# 常见:upstream timed out
#   → 后端响应慢,调整 proxy_read_timeout

# 检查后端服务健康
curl http://backend:8080/health
# 查看服务是否存活
ps -ef | grep java
# 查看服务日志
tail -f /app/logs/app.log

3. 503 排查(服务层)

bash
# 服务过载
# 查看负载
top                          # CPU
free -m                      # 内存
# 查看线程数
jstack <pid> | grep "java.lang.Thread.State" | wc -l
# 查看连接数
netstat -an | grep 8080 | wc -l

# 限流/熔断触发
# 查看限流配置(Sentinel)
curl http://app:port/sentinel/dashboard
# 查看熔断器状态(Hystrix/Resilience4j)

# 健康检查失败导致摘除
# 查看注册中心(Nacos/Eureka)
curl http://nacos:8848/nacos/v1/ns/instance/list?serviceName=xxx

4. 分层定位

用户 → CDN → LB(Nginx) → Gateway → Service → DB/Redis/MQ
  • LB 层:Nginx 日志、upstream 健康状态
  • Gateway 层:网关日志、限流熔断状态
  • Service 层:应用日志、GC、线程池、连接池
  • 下游层:DB/Redis 是否可用

常见原因

错误码原因解决
502后端服务挂掉重启服务、查看 OOM/崩溃日志
502后端重启中滚动发布等待启动
502连接超时调整超时、优化慢接口
503限流触发扩容、调整限流阈值
503熔断触发修复下游、调整熔断策略
503健康检查失败修复健康检查逻辑
503线程池满增大线程池、优化慢调用

应急处理

  1. 确认是否在发布(发布中 503 正常)
  2. 重启异常实例
  3. 扩容实例数
  4. 降级非核心功能
  5. 临时调大限流/超时阈值
  6. 切换备用机房

15.10 ★服务发布之后出现大量异常,在不重启服务的情况下如何快速止血?如何设计一套完善的线上监控告警体系?

📖 点击查看答案

不重启的快速止血方案

1. 回滚(最有效):

bash
# 回滚到上一版本(K8s)
kubectl rollout undo deployment/order-service
# 或指定版本
kubectl rollout undo deployment/order-service --to-revision=3

# 查看发布历史
kubectl rollout history deployment/order-service

2. 动态配置降级

java
// 通过配置中心(Nacos/Apollo)动态关闭新功能
@Value("${feature.new_order_flow.enabled:false}")
private boolean newOrderFlowEnabled;

// 热更新配置,无需重启
// Nacos 控制台修改 feature.new_order_flow.enabled = false

3. 限流/熔断保护

java
// Sentinel 动态规则,在控制台实时调整
// 1. 对异常接口加限流规则
// 2. 对下游服务加熔断规则
// 3. 临时关闭异常接口的访问

// Arthas 动态修改日志级别
logger --name com.example --level DEBUG
// 动态查看方法调用
watch com.example.OrderService createOrder "{params, returnObj, throwExp}"

4. 流量切换

  • 在网关层将异常实例摘除(修改权重为 0)
  • 切换到稳定版本的集群
  • 灰度发布:先切少量流量验证

监控告警体系设计

1. 监控分层

基础设施 → 中间件 → 应用 → 业务
层级指标工具
基础设施CPU、内存、磁盘、网络Prometheus + Node Exporter
中间件DB 连接数、慢SQL、Redis 内存、MQ 堆积Prometheus + Exporter
应用QPS、RT、错误率、GC、线程池Micrometer + Prometheus
业务订单量、支付成功率、库存预警自定义指标 + Grafana

2. 黄金信号(Google SRE)

  • 延迟(Latency):P50/P95/P99 响应时间
  • 流量(Traffic):QPS、并发数
  • 错误(Errors):错误率、5xx 状态码
  • 饱和度(Saturation):CPU、内存、连接池使用率

3. 告警规则示例

yaml
# Prometheus Alertmanager 规则
groups:
- name: app_alerts
  rules:
  - alert: HighErrorRate
    expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "错误率超过 5%"

  - alert: HighLatency
    expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 1
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "P99 延迟超过 1s"

  - alert: JVM_Heap_High
    expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85
    for: 5m
    labels:
      severity: warning

  - alert: Full_GC_Frequent
    expr: increase(jvm_gc_seconds_count{action="end of major GC"}[5m]) > 3
    for: 1m
    labels:
      severity: critical

4. 告警分级

  • P0(致命):核心服务不可用 → 电话+短信+IM,立即响应
  • P1(严重):错误率飙升/RT 翻倍 → 短信+IM,5 分钟响应
  • P2(警告):资源使用率高 → IM,30 分钟处理
  • P3(提示):慢查询增多 → 邮件,工作时间处理

5. 监控告警原则

  1. 告警要有可操作性:收到告警知道该做什么
  2. 避免告警风暴:合理设置阈值、静默规则、聚合
  3. 告警收敛:同源告警合并、关联告警聚合
  4. 故障自愈:自动重启、自动扩容(高级)
  5. 故障复盘:每次故障记录 RCA(根因分析)

基于 4 份主流面试题库整合精选,共 200 题