| name | error-handling-resilience |
| description | 设计错误处理 / 重试 / 熔断 / 限流 / 降级时使用。覆盖 Resilience4j(Java)/ Polly(.NET)/ opossum(Node)/ tenacity(Python)/ Sentinel(阿里)。融合 Netflix Hystrix 模式 + 故障隔离 + 优雅降级。 |
错误处理与韧性(Error Handling & Resilience)
参考来源:Michael Nygard《Release It!》、Netflix Hystrix Wiki、Resilience4j 文档、Google SRE Workbook、AWS Well-Architected Framework Reliability Pillar。
适用场景
- 第三方 API 调用容错
- 跨服务调用韧性
- 上游故障隔离
- 重试 / 熔断 / 降级 / 限流
- 全局错误处理 / 异常映射
- 故障注入 / Chaos Engineering
核心原则
1. 不信任任何外部依赖
网络 / DB / 第三方都会失败
2. Fail Fast(快速失败)
超时不能无限大,配置合理超时
3. Fail Safe(安全失败)
下游挂了不能拖垮上游
降级返回兜底数据
4. 区分可重试 vs 不可重试错误
网络超时 → 可重试
401 / 400 → 不重试
5. 重试要有边界
最大次数 + 指数退避 + 抖动
6. 熔断防止级联崩溃
下游持续失败 → 主动熔断
恢复后半开探测
7. 错误信息分内外
外部:友好错误码
内部:完整堆栈 + trace
异常分类
1. 业务异常(Business Exception)
- 资源不存在、状态冲突、校验失败
- 不重试,返回 4xx
2. 系统异常(System Exception)
- 数据库挂了、第三方超时、内存溢出
- 可能重试,返回 5xx
3. 编程错误(Programming Error)
- 空指针、数组越界、类型错误
- Bug,不重试,告警
异常分层:
Domain Exception → 业务规则违反
Application Exception → 业务逻辑失败(如订单不存在)
Infrastructure Exception → 基础设施失败(DB / Redis)
System Exception → 不可预期
全局错误处理
Spring Boot
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
return ResponseEntity.status(e.getStatus())
.body(new ErrorResponse(e.getCode(), e.getMessage(), TraceContext.getTraceId()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors().stream()
.map(fe -> new FieldError(fe.getField(), fe.getDefaultMessage()))
.toList();
return ResponseEntity.badRequest()
.body(new ErrorResponse("VALIDATION_ERROR", "Validation failed", fieldErrors));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception e) {
log.error("Unhandled exception", e);
return ResponseEntity.status(500)
.body(new ErrorResponse("INTERNAL_ERROR", "Internal server error"));
}
}
NestJS
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = 500;
let code = 'INTERNAL_ERROR';
let message = 'Internal server error';
if (exception instanceof BusinessException) {
status = exception.status;
code = exception.code;
message = exception.message;
} else if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
} else {
logger.error('Unhandled exception', exception);
}
response.status(status).json({
error: {
code,
message,
request_id: request.id,
timestamp: new Date().toISOString(),
},
});
}
}
超时设置(必须)
HTTP 客户端超时:
- 连接超时(connect):1~5s
- 读超时(read):5~30s
- 总超时:根据业务
数据库:
- 连接超时:1~5s
- 查询超时:根据业务
Redis:
- 连接超时:500ms
- 命令超时:1s
外部服务调用:
- 整体不超过用户可接受等待时间
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(2))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.responseTimeout(Duration.ofSeconds(5))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
))
.build();
const client = axios.create({
timeout: 5000,
baseURL: 'https://api.example.com',
});
const response = await fetch(url, {
signal: AbortSignal.timeout(5000),
});
重试模式
Spring + Resilience4j
@Retry(name = "paymentApi", fallbackMethod = "paymentFallback")
@CircuitBreaker(name = "paymentApi", fallbackMethod = "paymentFallback")
@TimeLimiter(name = "paymentApi")
public CompletableFuture<PaymentResult> charge(ChargeRequest req) {
return CompletableFuture.supplyAsync(() ->
paymentClient.charge(req)
);
}
public CompletableFuture<PaymentResult> paymentFallback(ChargeRequest req, Throwable t) {
log.warn("Payment failed, fallback", t);
return CompletableFuture.completedFuture(PaymentResult.queued(req));
}
resilience4j:
retry:
instances:
paymentApi:
max-attempts: 3
wait-duration: 1s
retry-exceptions:
- java.io.IOException
- org.springframework.web.client.HttpServerErrorException
ignore-exceptions:
- com.example.BusinessException
exponential-backoff-multiplier: 2
random-wait-factor: 0.5
circuitbreaker:
instances:
paymentApi:
sliding-window-size: 100
failure-rate-threshold: 50
slow-call-rate-threshold: 50
slow-call-duration-threshold: 5s
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 3
NestJS + opossum
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(paymentApi, {
timeout: 5000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
breaker.fallback(() => ({ status: 'queued', message: 'Payment queued for retry' }));
breaker.on('open', () => logger.warn('Payment circuit opened'));
breaker.on('halfOpen', () => logger.info('Payment circuit half-open'));
const result = await breaker.fire(req);
Python + tenacity
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type(RetryableError),
reraise=True,
)
def call_payment_api(req):
response = requests.post('https://api.example.com', json=req, timeout=5)
if response.status_code >= 500:
raise RetryableError(...)
return response.json()
Go
import "github.com/sony/gobreaker"
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "paymentApi",
MaxRequests: 3,
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
result, err := cb.Execute(func() (interface{}, error) {
return paymentClient.Charge(ctx, req)
})
熔断器三态
Closed(关闭):正常调用
↓ 失败率超阈值
Open(打开):快速失败,不调用
↓ 等待时间到
Half-Open(半开):少量探测
↓ 成功 → Closed
↓ 失败 → Open
降级策略
1. 静态降级:返回固定数据
user.country → 默认 "Unknown"
2. 缓存降级:返回过期缓存
原本 5 分钟过期 → 出错时用 1 小时内的
3. 简化降级:返回简化版数据
推荐列表 → 热门列表
4. 异步降级:放队列稍后处理
下单 → 支付服务挂了 → 进队列稍后扣款
5. 限流降级:超过容量直接返回
"系统繁忙,请稍后再试"
限流(Rate Limit)
async function rateLimit(key: string, limit: number, windowMs: number): Promise<boolean> {
const now = Date.now();
const windowKey = `rl:${key}`;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(windowKey, 0, now - windowMs);
pipeline.zcard(windowKey);
pipeline.zadd(windowKey, now, `${now}-${randomUUID()}`);
pipeline.expire(windowKey, Math.ceil(windowMs / 1000));
const results = await pipeline.exec();
const count = results[1][1] as number;
return count < limit;
}
@Bean
public Bucket bucket() {
return Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
.build();
}
@RestController
public class ApiController {
@Autowired private Bucket bucket;
@GetMapping("/api/data")
public ResponseEntity<?> getData() {
if (!bucket.tryConsume(1)) {
return ResponseEntity.status(429)
.header("Retry-After", "60")
.body(new ErrorResponse("RATE_LIMITED", "Too many requests"));
}
return ResponseEntity.ok(data);
}
}
舱壁隔离(Bulkhead)
为不同业务分配独立线程池 / 连接池:
- Critical(核心):50 连接
- Normal(普通):30 连接
- Background(后台):20 连接
避免一个业务把池吃完拖死其他业务
@Bulkhead(name = "paymentApi", fallbackMethod = "paymentBulkheadFallback")
public PaymentResult charge(...) { ... }
工作流程
1. 列出所有外部依赖
- 第三方 API
- 数据库
- 缓存
- 消息队列
- 文件存储
2. 每个依赖配置:
- 超时
- 重试策略
- 熔断阈值
- 降级方案
3. 全局错误处理
- 异常分类
- 状态码映射
- 不暴露内部细节
4. 限流策略
- 接入网关层 / 应用层
- 用户级 / IP 级 / 接口级
5. 监控
- 错误率
- 重试次数
- 熔断状态
- 限流触发率
6. 测试
- 故障注入(Chaos)
- 超时模拟
- 连接池耗尽
配套模板
templates/resilience-checklist.md — 韧性配置清单 + 异常映射 + 重试 + 熔断 + 降级 + 限流
质量自检
□ 所有外部调用有超时
□ 重试策略明确(次数 + 退避 + 抖动)
□ 区分可重试 vs 不可重试错误
□ 熔断器配置(阈值 + 恢复)
□ 降级方案存在
□ 全局错误处理器
□ 错误码统一
□ 不暴露内部细节给前端
□ 异常完整记日志
□ 限流配置(接口级 / 用户级)
□ 监控关键指标
□ 故障注入测试
常见坑
- 不设超时——一个慢调用拖垮整个服务
- catch 不抛——隐藏 Bug
- catch all + log——业务异常被吞
- 重试无上限——僵尸请求
- 重试不区分错误类型——4xx 也重试
- 不抖动——惊群效应
- 熔断阈值过严——正常波动也触发
- 熔断没有降级——熔了用户看到错
- 错误信息暴露内部——SQL / 堆栈外泄
- 限流只看 IP——NAT 后多用户共享
- 限流粒度太粗——一接口限流影响全平台
- 不监控错误率——故障扩散无感
- 错误码乱——前端处理混乱
- 不做故障演练——上线才发现
与其他 skill 的协作
上游:
api-implementation → 全局错误处理器
async-jobs → 任务重试策略
caching-strategy → 缓存挂了的降级
下游:
observability → 错误监控
testing-implementation → 故障测试
相关参考
- 项目根
references/backend-tech-stack-guide.md — 韧性库选型