API 是现代应用程序的支柱。当我第一次开始使用 Spring Boot 构建 API 时,我过于专注于提供功能,而忽略了一个关键方面:弹性。我经历了惨痛的教训才明白,API 能够优雅地处理故障并适应不同条件的能力才是它真正可靠的原因。让我向您介绍我一路上犯的一些错误以及我是如何纠正这些错误的。希望您能够在自己的旅程中避免这些陷阱。
发生了什么:在我的一个早期项目中,我构建了一个 API,可以对第三方服务进行外部调用。我认为这些服务总是会快速响应,并且不会费心设置超时。一切看起来都很好,直到流量增加,第三方服务开始变慢。我的 API 将无限期挂起,等待响应。
影响: API 的响应能力急剧下降。相关服务开始出现故障,用户面临长时间的延迟,有些甚至遇到了可怕的 500 内部服务器错误。
我是如何修复它的:那时我意识到超时配置的重要性。以下是我使用 Spring Boot 修复该问题的方法:
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder .setConnectTimeout(Duration.ofSeconds(5)) .setReadTimeout(Duration.ofSeconds(5)) .additionalInterceptors(new RestTemplateLoggingInterceptor()) .build(); } // Custom interceptor to log request/response details @RequiredArgsConstructor public class RestTemplateLoggingInterceptor implements ClientHttpRequestInterceptor { private static final Logger log = LoggerFactory.getLogger(RestTemplateLoggingInterceptor.class); @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { long startTime = System.currentTimeMillis(); log.info("Making request to: {}", request.getURI()); ClientHttpResponse response = execution.execute(request, body); long duration = System.currentTimeMillis() - startTime; log.info("Request completed in {}ms with status: {}", duration, response.getStatusCode()); return response; } } }
此配置不仅设置适当的超时,还包括日志记录以帮助监控外部服务性能。
发生了什么:曾经有一段时间,我们所依赖的内部服务宕机了几个小时。我的 API 没有很好地处理这种情况。相反,它不断重试那些失败的请求,给已经紧张的系统增加了更多的负载。
级联故障是分布式系统中最具挑战性的问题之一。当一项服务出现故障时,可能会产生多米诺骨牌效应,导致整个系统瘫痪。
影响:重复的重试使系统不堪重负,减慢了应用程序的其他部分并影响了所有用户。
我是如何修复它的:就在那时我发现了断路器模式。使用 Spring Cloud Resilience4j,我能够打破这个循环。
@Configuration public class Resilience4jConfig { @Bean public CircuitBreakerConfig circuitBreakerConfig() { return CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(60)) .permittedNumberOfCallsInHalfOpenState(2) .slidingWindowSize(2) .build(); } @Bean public RetryConfig retryConfig() { return RetryConfig.custom() .maxAttempts(3) .waitDuration(Duration.ofSeconds(2)) .build(); } } @Service @Slf4j public class ResilientService { private final CircuitBreaker circuitBreaker; private final RestTemplate restTemplate; public ResilientService(CircuitBreakerRegistry registry, RestTemplate restTemplate) { this.circuitBreaker = registry.circuitBreaker("internalService"); this.restTemplate = restTemplate; } @CircuitBreaker(name = "internalService", fallbackMethod = "fallbackResponse") @Retry(name = "internalService") public String callInternalService() { return restTemplate.getForObject("https://internal-service.com/data", String.class); } public String fallbackResponse(Exception ex) { log.warn("Circuit breaker activated, returning fallback response", ex); return new FallbackResponse("Service temporarily unavailable", getBackupData()).toJson(); } private Object getBackupData() { // Implement cache or default data strategy return new CachedDataService().getLatestValidData(); } }
这个简单的添加可以防止我的 API 压垮自身、内部服务或第三方服务,确保系统稳定性。
发生了什么: 早期,我没有对错误处理投入太多考虑。我的 API 要么抛出一般错误(例如所有内容的 HTTP 500),要么在堆栈跟踪中暴露敏感的内部详细信息。
影响:用户对出了什么问题感到困惑,内部细节的暴露造成了潜在的安全风险。
我是如何修复它的:我决定使用 Spring 的 @ControllerAdvice 注释来集中处理错误。这就是我所做的:
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder .setConnectTimeout(Duration.ofSeconds(5)) .setReadTimeout(Duration.ofSeconds(5)) .additionalInterceptors(new RestTemplateLoggingInterceptor()) .build(); } // Custom interceptor to log request/response details @RequiredArgsConstructor public class RestTemplateLoggingInterceptor implements ClientHttpRequestInterceptor { private static final Logger log = LoggerFactory.getLogger(RestTemplateLoggingInterceptor.class); @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { long startTime = System.currentTimeMillis(); log.info("Making request to: {}", request.getURI()); ClientHttpResponse response = execution.execute(request, body); long duration = System.currentTimeMillis() - startTime; log.info("Request completed in {}ms with status: {}", duration, response.getStatusCode()); return response; } } }
这使得错误消息清晰且安全,为用户和开发人员提供帮助。
发生了什么:在一个晴朗的日子,我们发起了一项促销活动,我们的 API 流量猛增。虽然这对企业来说是个好消息,但一些用户开始向 API 发送垃圾邮件请求,导致其他人资源匮乏。
影响:每个人的性能都下降了,我们收到了大量投诉。
我如何修复它:为了解决这个问题,我使用 Bucket4j 和 Redis 实现了速率限制。这是一个例子:
@Configuration public class Resilience4jConfig { @Bean public CircuitBreakerConfig circuitBreakerConfig() { return CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(60)) .permittedNumberOfCallsInHalfOpenState(2) .slidingWindowSize(2) .build(); } @Bean public RetryConfig retryConfig() { return RetryConfig.custom() .maxAttempts(3) .waitDuration(Duration.ofSeconds(2)) .build(); } } @Service @Slf4j public class ResilientService { private final CircuitBreaker circuitBreaker; private final RestTemplate restTemplate; public ResilientService(CircuitBreakerRegistry registry, RestTemplate restTemplate) { this.circuitBreaker = registry.circuitBreaker("internalService"); this.restTemplate = restTemplate; } @CircuitBreaker(name = "internalService", fallbackMethod = "fallbackResponse") @Retry(name = "internalService") public String callInternalService() { return restTemplate.getForObject("https://internal-service.com/data", String.class); } public String fallbackResponse(Exception ex) { log.warn("Circuit breaker activated, returning fallback response", ex); return new FallbackResponse("Service temporarily unavailable", getBackupData()).toJson(); } private Object getBackupData() { // Implement cache or default data strategy return new CachedDataService().getLatestValidData(); } }
这确保了公平使用并保护 API 免遭滥用。
发生了什么:每当生产中出现问题时,就像大海捞针一样。我没有适当的日志记录或指标,因此诊断问题花费的时间比应有的时间要长。
影响:故障排除变成了一场噩梦,延迟了问题解决并使用户感到沮丧。
我是如何解决这个问题的:我添加了 Spring Boot Actuator 来进行健康检查,并将 Prometheus 与 Grafana 集成起来以实现指标可视化:
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(HttpClientErrorException.class) public ResponseEntity<ErrorResponse> handleHttpClientError(HttpClientErrorException ex, WebRequest request) { log.error("Client error occurred", ex); ErrorResponse error = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(ex.getStatusCode().value()) .message(sanitizeErrorMessage(ex.getMessage())) .path(((ServletWebRequest) request).getRequest().getRequestURI()) .build(); return ResponseEntity.status(ex.getStatusCode()).body(error); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex, WebRequest request) { log.error("Unexpected error occurred", ex); ErrorResponse error = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) .message("An unexpected error occurred. Please try again later.") .path(((ServletWebRequest) request).getRequest().getRequestURI()) .build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } private String sanitizeErrorMessage(String message) { // Remove sensitive information from error messages return message.replaceAll("(password|secret|key)=\[.*?\]", "=[REDACTED]"); } }
我还使用 ELK Stack(Elasticsearch、Logstash、Kibana)实现了结构化日志记录。这使得日志更具可操作性。
构建弹性 API 是一个旅程,错误是这个过程的一部分。以下是我学到的主要经验教训:
这些变化改变了我进行 API 开发的方式。如果您遇到过类似的挑战或有其他建议,我很想听听您的故事!
尾注:请记住,弹性不是您添加的功能,而是您从头开始构建到系统中的特性。这些组件中的每一个在创建 API 方面都发挥着至关重要的作用,这些 API 不仅可以工作,而且可以在压力下继续可靠地工作。
以上是构建弹性 API:我犯的错误以及我如何克服这些错误的详细内容。更多信息请关注PHP中文网其他相关文章!