JAVA系统QPS突然下降的排查:限流、熔断与线程池链路诊断
大家好,今天我们来聊聊Java系统QPS突然下降的排查思路,重点关注限流、熔断以及线程池这三个关键环节。QPS下降是一个常见问题,原因可能很多,但从这三个角度入手,往往能快速定位并解决大部分问题。
一、QPS下降的常见原因与诊断流程
首先,我们需要了解QPS下降可能的原因,然后制定一个清晰的诊断流程。
-
常见原因:
- 资源瓶颈: CPU、内存、磁盘IO、网络带宽等资源达到瓶颈。
- 数据库瓶颈: 数据库查询缓慢、连接数不足等。
- 代码问题: 死循环、资源泄漏、低效算法等。
- 外部依赖: 依赖的外部服务响应缓慢或者不可用。
- 并发问题: 锁竞争激烈、死锁等。
- 限流、熔断: 系统主动触发了限流或者熔断机制。
- 缓存失效: 大量缓存同时失效,导致请求直接打到数据库。
- 垃圾回收: 频繁的Full GC导致系统停顿。
-
诊断流程:
- 监控报警: 查看监控系统,确认QPS下降的程度和持续时间。关注CPU、内存、磁盘IO、网络带宽、数据库连接数、JVM GC等关键指标。
- 日志分析: 分析系统日志,查找错误信息、异常堆栈等。
- 链路追踪: 使用链路追踪工具(如SkyWalking、Jaeger、Zipkin)分析请求链路,找出耗时较长的环节。
- 线程Dump: 获取线程Dump,分析线程状态,查找死锁、锁竞争等问题。
- 内存Dump: 获取内存Dump,分析内存占用情况,查找内存泄漏等问题。
- 逐步排查: 从最可能的原因入手,逐步排查,缩小范围。
二、限流排查
限流是为了保护系统免受过载的攻击,防止雪崩效应。但如果限流策略配置不当,或者系统误判,也可能导致QPS下降。
-
常见限流算法:
-
计数器法: 在单位时间内,记录请求数量,超过阈值则拒绝请求。
public class CounterRateLimiter { private final int limit; // 单位时间内允许的请求数量 private final long period; // 单位时间,单位毫秒 private long counter; // 当前计数器 private long lastRefillTimestamp; // 上次重置计数器的时间戳 public CounterRateLimiter(int limit, long period) { this.limit = limit; this.period = period; this.counter = 0; this.lastRefillTimestamp = System.currentTimeMillis(); } public synchronized boolean tryAcquire() { long now = System.currentTimeMillis(); if (now - lastRefillTimestamp > period) { // 重置计数器 counter = 0; lastRefillTimestamp = now; } if (counter < limit) { counter++; return true; // 允许请求 } else { return false; // 拒绝请求 } } } -
令牌桶算法: 系统以恒定速率向桶中放入令牌,每个请求需要从桶中获取一个令牌,如果桶中没有令牌,则拒绝请求。
public class TokenBucketRateLimiter { private final int capacity; // 令牌桶容量 private final double rate; // 令牌生成速率,单位:令牌/毫秒 private double tokens; // 当前令牌数量 private long lastRefillTimestamp; // 上次填充令牌的时间戳 public TokenBucketRateLimiter(int capacity, double rate) { this.capacity = capacity; this.rate = rate; this.tokens = capacity; // 初始令牌数量为桶容量 this.lastRefillTimestamp = System.currentTimeMillis(); } public synchronized boolean tryAcquire() { refill(); // 先填充令牌 if (tokens >= 1) { tokens--; return true; // 允许请求 } else { return false; // 拒绝请求 } } private void refill() { long now = System.currentTimeMillis(); double elapsedTime = (now - lastRefillTimestamp) / 1000.0; // 毫秒转秒 double newTokens = elapsedTime * rate; tokens = Math.min(capacity, tokens + newTokens); lastRefillTimestamp = now; } } -
漏桶算法: 请求进入漏桶,漏桶以恒定速率流出请求,如果请求速度超过漏桶流出速度,则请求溢出,被拒绝。
// 简易实现,实际生产中更复杂,需要考虑并发等问题 public class LeakyBucketRateLimiter { private final int capacity; // 漏桶容量 private final double rate; // 漏水速率,单位:请求/毫秒 private int water; // 当前水量 private long lastOutflowTimestamp; // 上次漏水的时间戳 public LeakyBucketRateLimiter(int capacity, double rate) { this.capacity = capacity; this.rate = rate; this.water = 0; this.lastOutflowTimestamp = System.currentTimeMillis(); } public synchronized boolean tryAcquire() { long now = System.currentTimeMillis(); double elapsedTime = (now - lastOutflowTimestamp) / 1000.0; double outflow = elapsedTime * rate; // 计算流出的水量 water = Math.max(0, water - (int)outflow); lastOutflowTimestamp = now; if (water < capacity) { water++; return true; } else { return false; } } }
-
-
排查步骤:
- 检查限流配置: 确认限流策略是否过于严格,例如QPS阈值设置过低。
- 查看限流日志: 分析限流日志,确认是否有大量请求被限流。
- 监控限流指标: 监控限流器的状态,例如计数器值、令牌桶剩余令牌数等。
- 动态调整限流策略: 根据实际情况,动态调整限流策略,例如增加QPS阈值、调整令牌生成速率等。
-
代码示例:
假设我们使用Guava RateLimiter进行限流,排查代码如下:
import com.google.common.util.concurrent.RateLimiter; public class RateLimiterExample { private static final RateLimiter rateLimiter = RateLimiter.create(100); // 每秒允许100个请求 public static void main(String[] args) { for (int i = 0; i < 200; i++) { if (rateLimiter.tryAcquire()) { // 处理请求 System.out.println("处理请求:" + i); } else { // 请求被限流 System.out.println("请求被限流:" + i); } } } }在排查QPS下降时,需要关注
rateLimiter.tryAcquire()的返回值,如果大量请求返回false,则说明系统正在进行限流。可以通过调整RateLimiter.create(100)的参数来调整限流策略。
三、熔断排查
熔断是为了防止系统因外部依赖故障而导致雪崩效应。当外部依赖出现故障时,熔断器会切断对该依赖的调用,避免故障蔓延。但如果熔断器误判,或者熔断策略配置不当,也可能导致QPS下降。
-
常见熔断器实现:
- Hystrix: Netflix开源的熔断器,功能强大,但已停止维护。
- Resilience4j: 轻量级的熔断器,功能完善,且仍在积极维护。
- Spring Cloud CircuitBreaker: Spring Cloud提供的熔断器,与Spring Cloud生态集成良好。
-
排查步骤:
- 检查熔断器状态: 确认熔断器是否处于打开状态。
- 查看熔断器指标: 监控熔断器的状态指标,例如错误率、请求数量、熔断时间等。
- 分析熔断原因: 分析熔断日志,确认熔断的原因,例如外部依赖超时、异常等。
- 调整熔断策略: 根据实际情况,调整熔断策略,例如调整错误率阈值、熔断时间等。
- 检查依赖服务: 确认被熔断的依赖服务是否正常运行。
-
代码示例:
假设我们使用Resilience4j进行熔断,排查代码如下:
import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import java.time.Duration; import java.util.function.Supplier; public class CircuitBreakerExample { public static void main(String[] args) { // 配置熔断器 CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() .failureRateThreshold(50) // 错误率超过50%则熔断 .waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断10秒后尝试恢复 .slidingWindowSize(10) // 统计最近10次请求 .build(); CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(circuitBreakerConfig); CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("myCircuitBreaker"); // 定义需要熔断保护的方法 Supplier<String> backendService = () -> { // 模拟调用外部服务 if (Math.random() < 0.6) { throw new RuntimeException("调用外部服务失败"); } return "调用外部服务成功"; }; // 使用熔断器包装方法 Supplier<String> decoratedSupplier = CircuitBreaker.decorateSupplier(circuitBreaker, backendService); // 调用方法 for (int i = 0; i < 20; i++) { try { String result = decoratedSupplier.get(); System.out.println("调用结果:" + result + ", 状态:" + circuitBreaker.getState()); } catch (Exception e) { System.out.println("调用失败:" + e.getMessage() + ", 状态:" + circuitBreaker.getState()); } } } }在排查QPS下降时,需要关注
circuitBreaker.getState()的状态,如果状态为OPEN,则说明熔断器处于打开状态。可以通过调整CircuitBreakerConfig的参数来调整熔断策略,或者检查被熔断的依赖服务是否正常运行。
四、线程池排查
线程池是管理和复用线程的机制,可以提高系统性能。但如果线程池配置不当,例如线程池过小、任务队列过长等,也可能导致QPS下降。
-
常见线程池参数:
- corePoolSize: 核心线程数,即使线程空闲也会保持的线程数量。
- maximumPoolSize: 最大线程数,线程池允许的最大线程数量。
- keepAliveTime: 线程空闲时间,超过该时间的空闲线程会被回收。
- workQueue: 任务队列,用于存放等待执行的任务。
- RejectedExecutionHandler: 拒绝策略,当任务队列已满且线程池已达到最大线程数时,用于处理新提交的任务。
-
排查步骤:
- 监控线程池状态: 监控线程池的活跃线程数、任务队列长度、已完成任务数等指标。
- 分析线程Dump: 获取线程Dump,分析线程状态,查找线程是否处于阻塞状态。
- 检查线程池配置: 确认线程池的配置是否合理,例如线程池大小是否足够、任务队列是否过长等。
- 调整线程池配置: 根据实际情况,调整线程池配置,例如增加线程池大小、缩短线程空闲时间等。
- 分析任务执行时间: 分析任务的执行时间,找出耗时较长的任务,并进行优化。
-
代码示例:
假设我们使用
ThreadPoolExecutor创建线程池,排查代码如下:import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolExample { private static final int CORE_POOL_SIZE = 5; private static final int MAX_POOL_SIZE = 10; private static final long KEEP_ALIVE_TIME = 60L; private static final int QUEUE_CAPACITY = 100; private static final ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY), new ThreadPoolExecutor.CallerRunsPolicy()); // 使用CallerRunsPolicy拒绝策略 public static void main(String[] args) { for (int i = 0; i < 200; i++) { final int taskId = i; executor.execute(() -> { try { // 模拟执行任务 Thread.sleep((long) (Math.random() * 100)); // 模拟任务执行时间 System.out.println("执行任务:" + taskId + ", 线程:" + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } }); } // 关闭线程池 executor.shutdown(); try { executor.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } }在排查QPS下降时,需要关注线程池的活跃线程数、任务队列长度等指标。可以使用
jconsole或者jvisualvm等工具来监控线程池的状态。如果任务队列过长,或者活跃线程数接近最大线程数,则说明线程池可能存在瓶颈。可以尝试增加线程池大小或者优化任务执行时间来解决问题。同时,关注拒绝策略的执行情况,如果大量任务被拒绝,则说明线程池无法处理所有的请求。
五、链路诊断工具的使用
链路追踪工具,如 SkyWalking, Zipkin, Jaeger 等,可以帮助我们分析请求在各个服务之间的调用链,找出耗时较长的环节,从而快速定位问题。
-
使用步骤:
- 引入依赖: 在项目中引入链路追踪工具的依赖。
- 配置参数: 配置链路追踪工具的参数,例如采样率、上报地址等。
- 代码埋点: 在关键代码处进行埋点,例如在Controller层、Service层、DAO层等。
- 查看追踪信息: 通过链路追踪工具的UI界面,查看请求的调用链、耗时信息等。
-
示例(以SkyWalking为例):
- 引入依赖 (Maven):
<dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-trace</artifactId> <version>8.9.0</version> </dependency>- 配置 Agent: 下载 SkyWalking Agent, 并配置
agent.config文件. 主要配置collector.servers指向 SkyWalking OAP 服务器. - 启动 Agent: 在 JVM 启动参数中添加
-javaagent:/path/to/skywalking-agent/skywalking-agent.jar. - 查看链路信息: 访问 SkyWalking UI, 查看 trace 信息.
链路追踪工具能够提供清晰的调用链,帮助我们快速定位瓶颈点,例如数据库查询缓慢、外部服务响应慢等。
六、实际案例分析
假设一个电商系统在双十一期间QPS突然下降,经过初步排查,发现CPU利用率不高,数据库连接数也正常。
- 查看限流日志: 发现大量请求被限流,原因是秒杀接口的限流策略过于严格。
- 调整限流策略: 临时调整秒杀接口的限流阈值,增加允许通过的请求数量。
- 查看熔断器状态: 发现支付服务被熔断,原因是支付服务在高峰期出现了一些故障。
- 检查支付服务: 检查支付服务,发现数据库连接池耗尽,导致服务响应缓慢。
- 调整数据库连接池: 增加支付服务的数据库连接池大小,解决数据库连接耗尽的问题。
- 恢复熔断器: 等待支付服务恢复正常后,手动恢复熔断器。
- 监控线程池: 发现订单服务的线程池任务队列过长,导致订单处理速度缓慢。
- 调整线程池: 增加订单服务的线程池大小,提高订单处理速度。
通过以上排查和调整,电商系统的QPS逐渐恢复正常。
七、总结与建议
QPS下降的排查是一个复杂的过程,需要综合考虑各种因素。限流、熔断和线程池是三个关键环节,需要重点关注。
- 监控是关键: 完善的监控系统可以帮助我们及时发现问题。
- 日志是线索: 详细的日志可以帮助我们定位问题。
- 链路追踪是利器: 链路追踪工具可以帮助我们分析请求链路,找出耗时较长的环节。
- 逐步排查: 从最可能的原因入手,逐步排查,缩小范围。
希望今天的分享能帮助大家更好地排查Java系统QPS下降的问题,谢谢大家。
核心思路的回顾
本文阐述了QPS下降的排查思路,重点介绍了限流、熔断和线程池的排查方法,并结合代码示例和实际案例进行了说明,强调了监控、日志和链路追踪的重要性。