首頁 >Java >java教程 >建立彈性 API:我犯的錯誤以及我如何克服這些錯誤

建立彈性 API:我犯的錯誤以及我如何克服這些錯誤

Mary-Kate Olsen
Mary-Kate Olsen原創
2025-01-04 15:48:40620瀏覽

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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn