首页 > Java > java教程 > 构建弹性 API:我犯的错误以及我如何克服这些错误

构建弹性 API:我犯的错误以及我如何克服这些错误

Mary-Kate Olsen
发布: 2025-01-04 15:48:40
原创
620 人浏览过

Building Resilient APIs: Mistakes I Made and How I Overcame Them

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;
        }
    }
}
登录后复制
登录后复制

此配置不仅设置适当的超时,还包括日志记录以帮助监控外部服务性能。

错误2:没有实施断路器

发生了什么:曾经有一段时间,我们所依赖的内部服务宕机了几个小时。我的 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 压垮自身、内部服务或第三方服务,确保系统稳定性。

错误 3:错误处理能力弱

发生了什么: 早期,我没有对错误处理投入太多考虑。我的 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 免遭滥用。

错误5:忽视可观察性

发生了什么:每当生产中出现问题时,就像大海捞针一样。我没有适当的日志记录或指标,因此诊断问题花费的时间比应有的时间要长。

影响:故障排除变成了一场噩梦,延迟了问题解决并使用户感到沮丧。

我是如何解决这个问题的:我添加了 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 是一个旅程,错误是这个过程的一部分。以下是我学到的主要经验教训:

  1. 始终为外部调用配置超时。
  2. 使用断路器来防止级联故障。
  3. 集中错误处理,使其清晰且安全。
  4. 实施速率限制以管理流量峰值。

这些变化改变了我进行 API 开发的方式。如果您遇到过类似的挑战或有其他建议,我很想听听您的故事!

尾注:请记住,弹性不是您添加的功能,而是您从头开始构建到系统中的特性。这些组件中的每一个在创建 API 方面都发挥着至关重要的作用,这些 API 不仅可以工作,而且可以在压力下继续可靠地工作。

以上是构建弹性 API:我犯的错误以及我如何克服这些错误的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板