第481集重试为什么可能放大故障?怎么避免雪崩?
|字数总计:4.8k|阅读时长:19分钟|阅读量:
重试为什么可能放大故障?怎么避免雪崩?
1. 概述
1.1 重试机制的双刃剑
重试机制是提高系统可靠性的重要手段,但如果使用不当,可能放大故障,甚至导致系统雪崩。
重试的风险:
- 放大故障:重试可能将小故障放大为大故障
- 雪崩效应:重试风暴可能导致整个系统崩溃
- 资源耗尽:大量重试可能耗尽系统资源
- 级联故障:一个服务的故障可能传播到整个系统
1.2 雪崩效应
雪崩效应:当一个服务出现故障时,由于重试机制,大量请求堆积,导致服务完全不可用,进而影响依赖它的其他服务,最终导致整个系统崩溃。
雪崩的特点:
- 快速传播:故障快速传播到整个系统
- 难以恢复:一旦发生,难以快速恢复
- 影响范围大:影响整个系统的可用性
1.3 本文内容结构
本文将从以下几个方面全面解析重试机制的风险和避免雪崩的方法:
- 重试放大故障的原因:为什么重试可能放大故障
- 雪崩效应原理:雪崩是如何发生的
- 避免雪崩的方案:如何设计重试策略避免雪崩
- 重试策略优化:指数退避、重试限制、熔断等
- 实战案例:实际项目中的重试优化
2. 重试放大故障的原因
2.1 重试风暴
2.1.1 什么是重试风暴
重试风暴:当服务出现故障时,大量客户端同时重试,导致请求量急剧增加,进一步加重服务负担,形成恶性循环。
场景示例:
1 2 3 4 5 6
| 正常情况: - 服务A每秒接收100个请求 - 服务A出现故障,响应变慢 - 100个客户端同时重试 - 服务A每秒接收200个请求(100个新请求 + 100个重试请求) - 服务A负载翻倍,故障加剧
|
2.1.2 重试风暴的危害
危害:
- 请求量翻倍:重试导致请求量成倍增加
- 资源耗尽:大量重试耗尽服务资源(CPU、内存、连接数等)
- 故障加剧:服务负载增加,故障更加严重
- 级联故障:故障传播到依赖服务
2.1.3 错误的重试实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Service public class OrderService { @Autowired private PaymentClient paymentClient;
public PaymentResult payOrder(PaymentRequest request) { while (true) { try { return paymentClient.pay(request); } catch (Exception e) { log.warn("Payment failed, retrying...", e); } } } }
|
问题:
- 无限重试,可能导致服务完全不可用
- 没有延迟,立即重试,加重服务负担
- 没有重试次数限制,可能永远重试
2.2 同步重试放大故障
2.2.1 同步重试的问题
同步重试:在同一个线程中重试,阻塞线程资源。
问题:
- 线程阻塞:重试期间线程被阻塞,无法处理其他请求
- 线程池耗尽:大量线程被重试占用,线程池耗尽
- 请求堆积:新请求无法处理,请求堆积
2.2.2 错误示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Service public class OrderService { @Autowired private InventoryClient inventoryClient;
public InventoryResult deductStock(Long skuId, Integer quantity) { int maxRetries = 10; for (int i = 0; i < maxRetries; i++) { try { return inventoryClient.deductStock(skuId, quantity); } catch (Exception e) { if (i < maxRetries - 1) { try { Thread.sleep(100); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new BusinessException("重试被中断", ie); } } else { throw new BusinessException("库存服务调用失败", e); } } } throw new BusinessException("库存服务调用失败"); } }
|
问题:
- 同步重试,阻塞线程
- 固定延迟,没有指数退避
- 重试次数过多(10次)
2.3 无延迟重试放大故障
2.3.1 无延迟重试的问题
无延迟重试:失败后立即重试,没有延迟。
问题:
- 请求突增:大量请求同时重试,请求量突增
- 服务过载:服务还未恢复,又收到大量重试请求
- 故障持续:服务无法恢复,故障持续
2.3.2 错误示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Service public class OrderService { @Autowired private PaymentClient paymentClient;
public PaymentResult payOrder(PaymentRequest request) { int maxRetries = 5; for (int i = 0; i < maxRetries; i++) { try { return paymentClient.pay(request); } catch (Exception e) { if (i < maxRetries - 1) { } else { throw new BusinessException("支付失败", e); } } } throw new BusinessException("支付失败"); } }
|
2.4 重试范围过大
2.4.1 重试范围过大的问题
重试范围过大:对所有异常都重试,包括不应该重试的异常。
问题:
- 无效重试:对业务异常重试,浪费资源
- 数据不一致:对幂等性操作重试,可能导致数据不一致
- 资源浪费:对不可恢复的错误重试,浪费资源
2.4.2 错误示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Service public class OrderService { @Autowired private PaymentClient paymentClient;
@Retryable(value = Exception.class) public PaymentResult payOrder(PaymentRequest request) { return paymentClient.pay(request); } }
|
问题:
- 对业务异常(如余额不足)也重试,浪费资源
- 对参数错误也重试,没有意义
- 对权限错误也重试,不会成功
3. 雪崩效应原理
3.1 雪崩的发生过程
3.1.1 雪崩的触发
雪崩触发条件:
- 服务故障:某个服务出现故障(响应慢、超时、异常等)
- 大量重试:客户端大量重试,请求量急剧增加
- 资源耗尽:服务资源(线程、连接、内存等)耗尽
- 故障传播:故障传播到依赖服务
3.1.2 雪崩的传播
雪崩传播路径:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 服务A故障 ↓ 客户端重试(请求量翻倍) ↓ 服务A资源耗尽(线程池、连接池等) ↓ 服务A完全不可用 ↓ 依赖服务A的服务B也受影响 ↓ 服务B也开始重试 ↓ 整个系统雪崩
|
3.2 雪崩的数学模型
3.2.1 请求量计算
请求量计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 总请求量 = 新请求量 + 重试请求量
重试请求量 = 新请求量 × 重试次数 × 重试比例
其中: - 重试次数:每个请求最多重试的次数 - 重试比例:失败请求的比例
示例: - 新请求量:100 QPS - 失败率:50% - 重试次数:3次 - 重试请求量 = 100 × 50% × 3 = 150 QPS - 总请求量 = 100 + 150 = 250 QPS(增加2.5倍)
|
3.2.2 资源消耗计算
资源消耗:
1 2 3 4 5 6 7 8 9 10
| 线程消耗 = 并发请求数 × 平均处理时间
如果: - 并发请求数:250(包括重试) - 平均处理时间:2秒(服务变慢) - 线程消耗 = 250 × 2 = 500个线程
如果线程池大小只有100,则: - 500个线程 > 100个线程池大小 - 线程池耗尽,新请求无法处理
|
3.3 雪崩的典型案例
3.3.1 案例:支付服务雪崩
场景:
- 支付服务响应变慢(从100ms增加到2秒)
- 订单服务调用支付服务,超时后重试
- 100个订单同时创建,每个重试3次
- 支付服务收到400个请求(100个新请求 + 300个重试请求)
- 支付服务线程池耗尽,完全不可用
- 依赖支付服务的其他服务也受影响
4. 避免雪崩的方案
4.1 指数退避(Exponential Backoff)
4.1.1 指数退避原理
指数退避:重试间隔逐渐增加,避免大量请求同时重试。
公式:
1 2 3 4 5 6 7 8
| 延迟时间 = 初始延迟 × 2^重试次数
示例: - 初始延迟:100ms - 第1次重试:100ms - 第2次重试:200ms - 第3次重试:400ms - 第4次重试:800ms
|
4.1.2 指数退避实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @Service public class OrderService { @Autowired private PaymentClient paymentClient;
public PaymentResult payOrder(PaymentRequest request) { int maxRetries = 3; long initialDelay = 100; long maxDelay = 5000; for (int i = 0; i < maxRetries; i++) { try { return paymentClient.pay(request); } catch (Exception e) { if (i < maxRetries - 1) { long delay = Math.min(initialDelay * (long) Math.pow(2, i), maxDelay); try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new BusinessException("重试被中断", ie); } } else { throw new BusinessException("支付失败", e); } } } throw new BusinessException("支付失败"); } }
|
4.1.3 带抖动的指数退避
抖动(Jitter):在指数退避的基础上添加随机抖动,避免大量请求同时重试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public PaymentResult payOrderWithJitter(PaymentRequest request) { int maxRetries = 3; long initialDelay = 100; long maxDelay = 5000; Random random = new Random(); for (int i = 0; i < maxRetries; i++) { try { return paymentClient.pay(request); } catch (Exception e) { if (i < maxRetries - 1) { long baseDelay = Math.min(initialDelay * (long) Math.pow(2, i), maxDelay); long jitter = (long) (baseDelay * 0.1 * random.nextDouble()); long delay = baseDelay + jitter; try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new BusinessException("重试被中断", ie); } } else { throw new BusinessException("支付失败", e); } } } throw new BusinessException("支付失败"); }
|
4.2 限制重试次数
4.2.1 重试次数限制
重试次数限制:限制最大重试次数,避免无限重试。
最佳实践:
- 网络错误:重试3-5次
- 超时错误:重试2-3次
- 5xx错误:重试1-2次
- 4xx错误:不重试(客户端错误)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| @Service public class OrderService { @Autowired private PaymentClient paymentClient;
public PaymentResult payOrder(PaymentRequest request) { int maxRetries = getMaxRetries(request); for (int i = 0; i < maxRetries; i++) { try { return paymentClient.pay(request); } catch (TimeoutException e) { if (i < 2) { sleepWithBackoff(i); } else { throw new BusinessException("支付超时", e); } } catch (ConnectException e) { if (i < 4) { sleepWithBackoff(i); } else { throw new BusinessException("支付服务连接失败", e); } } catch (HttpServerException e) { if (i < 1) { sleepWithBackoff(i); } else { throw new BusinessException("支付服务错误", e); } } catch (HttpClientException e) { throw new BusinessException("支付请求错误", e); } } throw new BusinessException("支付失败"); } private int getMaxRetries(PaymentRequest request) { return 3; } private void sleepWithBackoff(int retryCount) { long delay = 100 * (long) Math.pow(2, retryCount); try { Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
|
4.3 熔断器保护
4.3.1 熔断器防止重试风暴
熔断器:当服务故障率超过阈值时,快速失败,不进行重试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Service public class OrderService { @Autowired private PaymentClient paymentClient; private final CircuitBreaker circuitBreaker; public OrderService() { CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(10)) .slidingWindowSize(10) .build(); this.circuitBreaker = CircuitBreaker.of("payment", config); }
public PaymentResult payOrder(PaymentRequest request) { return circuitBreaker.executeSupplier(() -> { return retryWithBackoff(() -> paymentClient.pay(request)); }); } private PaymentResult retryWithBackoff(Supplier<PaymentResult> supplier) { int maxRetries = 3; long initialDelay = 100; for (int i = 0; i < maxRetries; i++) { try { return supplier.get(); } catch (Exception e) { if (i < maxRetries - 1) { long delay = initialDelay * (long) Math.pow(2, i); try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new BusinessException("重试被中断", ie); } } else { throw new BusinessException("支付失败", e); } } } throw new BusinessException("支付失败"); } }
|
4.4 异步重试
4.4.1 异步重试的优势
异步重试:将重试任务放入队列,异步执行,不阻塞主线程。
优势:
- 不阻塞线程:主线程不被阻塞
- 控制重试速率:通过队列控制重试速率
- 批量处理:可以批量处理重试任务
4.4.2 异步重试实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| @Service public class OrderService { @Autowired private PaymentClient paymentClient; @Autowired private KafkaTemplate<String, String> kafkaTemplate;
public PaymentResult payOrder(PaymentRequest request) { try { return paymentClient.pay(request); } catch (Exception e) { log.warn("Payment failed, sending to retry queue: request={}", request, e); RetryMessage retryMessage = new RetryMessage(); retryMessage.setRequest(request); retryMessage.setRetryCount(0); retryMessage.setMaxRetries(3); retryMessage.setNextRetryTime(System.currentTimeMillis() + 100); kafkaTemplate.send("payment-retry", JSON.toJSONString(retryMessage)); return PaymentResult.processing("支付处理中,请稍后查询"); } } }
@Component public class PaymentRetryConsumer { @Autowired private PaymentClient paymentClient; @KafkaListener(topics = "payment-retry", groupId = "payment-retry-group") public void handleRetry(String message) { RetryMessage retryMessage = JSON.parseObject(message, RetryMessage.class); if (System.currentTimeMillis() < retryMessage.getNextRetryTime()) { kafkaTemplate.send("payment-retry", message); return; } try { PaymentResult result = paymentClient.pay(retryMessage.getRequest()); log.info("Payment retry success: request={}", retryMessage.getRequest()); } catch (Exception e) { log.warn("Payment retry failed: request={}, retryCount={}", retryMessage.getRequest(), retryMessage.getRetryCount(), e); if (retryMessage.getRetryCount() < retryMessage.getMaxRetries()) { long delay = 100 * (long) Math.pow(2, retryMessage.getRetryCount()); retryMessage.setRetryCount(retryMessage.getRetryCount() + 1); retryMessage.setNextRetryTime(System.currentTimeMillis() + delay); kafkaTemplate.send("payment-retry", JSON.toJSONString(retryMessage)); } else { log.error("Payment retry exhausted: request={}", retryMessage.getRequest()); kafkaTemplate.send("payment-dlq", JSON.toJSONString(retryMessage)); } } } }
|
4.5 重试限流
4.5.1 重试限流原理
重试限流:限制重试请求的速率,避免重试风暴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| @Service public class OrderService { @Autowired private PaymentClient paymentClient; @Autowired private RedisTemplate<String, String> redisTemplate;
public PaymentResult payOrder(PaymentRequest request) { int maxRetries = 3; long initialDelay = 100; for (int i = 0; i < maxRetries; i++) { try { return paymentClient.pay(request); } catch (Exception e) { if (i < maxRetries - 1) { if (!checkRetryRateLimit("payment-retry")) { throw new BusinessException("重试过于频繁,请稍后再试"); } long delay = initialDelay * (long) Math.pow(2, i); try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new BusinessException("重试被中断", ie); } } else { throw new BusinessException("支付失败", e); } } } throw new BusinessException("支付失败"); }
private boolean checkRetryRateLimit(String key) { String cacheKey = "retry_rate_limit:" + key; String countStr = redisTemplate.opsForValue().get(cacheKey); int count = countStr == null ? 0 : Integer.parseInt(countStr); if (count >= 10) { return false; } redisTemplate.opsForValue().increment(cacheKey); redisTemplate.expire(cacheKey, 1, TimeUnit.SECONDS); return true; } }
|
4.6 选择性重试
4.6.1 选择性重试原理
选择性重试:只对特定异常重试,不对所有异常重试。
应该重试的异常:
- 网络错误:连接超时、读取超时等
- 5xx错误:服务器错误
- 临时故障:服务暂时不可用
不应该重试的异常:
- 4xx错误:客户端错误(参数错误、权限错误等)
- 业务异常:余额不足、库存不足等
- 幂等性错误:重复请求等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @Service public class OrderService { @Autowired private PaymentClient paymentClient;
public PaymentResult payOrder(PaymentRequest request) { int maxRetries = 3; long initialDelay = 100; for (int i = 0; i < maxRetries; i++) { try { return paymentClient.pay(request); } catch (TimeoutException e) { if (i < maxRetries - 1) { sleepWithBackoff(i, initialDelay); } else { throw new BusinessException("支付超时", e); } } catch (ConnectException e) { if (i < maxRetries - 1) { sleepWithBackoff(i, initialDelay); } else { throw new BusinessException("支付服务连接失败", e); } } catch (HttpServerException e) { if (i < maxRetries - 1) { sleepWithBackoff(i, initialDelay); } else { throw new BusinessException("支付服务错误", e); } } catch (HttpClientException e) { throw new BusinessException("支付请求错误", e); } catch (BusinessException e) { throw e; } } throw new BusinessException("支付失败"); } private void sleepWithBackoff(int retryCount, long initialDelay) { long delay = initialDelay * (long) Math.pow(2, retryCount); try { Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
|
5. 综合方案
5.1 完整的重试策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| @Service public class OrderService { @Autowired private PaymentClient paymentClient; private final CircuitBreaker circuitBreaker; private final RateLimiter retryRateLimiter; public OrderService() { CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(10)) .slidingWindowSize(10) .build(); this.circuitBreaker = CircuitBreaker.of("payment", circuitBreakerConfig); this.retryRateLimiter = RateLimiter.of("payment-retry", RateLimiterConfig.custom() .limitForPeriod(10) .limitRefreshPeriod(Duration.ofSeconds(1)) .build()); }
public PaymentResult payOrder(PaymentRequest request) { return circuitBreaker.executeSupplier(() -> { if (!retryRateLimiter.acquirePermission()) { throw new BusinessException("重试过于频繁,请稍后再试"); } return retryWithStrategy(() -> paymentClient.pay(request)); }); }
private PaymentResult retryWithStrategy(Supplier<PaymentResult> supplier) { int maxRetries = 3; long initialDelay = 100; long maxDelay = 5000; Random random = new Random(); for (int i = 0; i < maxRetries; i++) { try { return supplier.get(); } catch (TimeoutException | ConnectException | HttpServerException e) { if (i < maxRetries - 1) { long baseDelay = Math.min(initialDelay * (long) Math.pow(2, i), maxDelay); long jitter = (long) (baseDelay * 0.1 * random.nextDouble()); long delay = baseDelay + jitter; try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new BusinessException("重试被中断", ie); } } else { throw new BusinessException("支付失败", e); } } catch (HttpClientException | BusinessException e) { throw new BusinessException("支付失败", e); } } throw new BusinessException("支付失败"); } }
|
6. 监控和告警
6.1 重试监控
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Component public class RetryMetrics { private final MeterRegistry meterRegistry; public RetryMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; }
public void recordRetry(String service, int retryCount, boolean success) { meterRegistry.counter("retry_count", "service", service, "retry_count", String.valueOf(retryCount), "success", String.valueOf(success)) .increment(); }
public void recordRetryDelay(String service, long delay) { meterRegistry.timer("retry_delay", "service", service) .record(delay, TimeUnit.MILLISECONDS); }
public void recordRetryStorm(String service) { meterRegistry.counter("retry_storm", "service", service).increment(); } }
|
7. 总结
7.1 核心要点
- 重试放大故障的原因:重试风暴、同步重试、无延迟重试、重试范围过大
- 雪崩效应原理:服务故障 → 大量重试 → 资源耗尽 → 故障传播
- 避免雪崩的方案:指数退避、限制重试次数、熔断器保护、异步重试、重试限流、选择性重试
- 最佳实践:组合使用多种方案,形成完整的重试策略
7.2 关键理解
- 重试是双刃剑:合理使用提高可靠性,不当使用放大故障
- 指数退避是关键:避免大量请求同时重试
- 熔断器是保护:快速失败,防止重试风暴
- 选择性重试:只对特定异常重试,不对所有异常重试
7.3 最佳实践
- 指数退避 + 抖动:避免大量请求同时重试
- 限制重试次数:避免无限重试
- 熔断器保护:快速失败,防止重试风暴
- 异步重试:不阻塞主线程
- 重试限流:限制重试请求的速率
- 选择性重试:只对特定异常重试
- 监控告警:实时监控重试情况,及时发现问题
相关文章: