Heim >Java >javaLernprogramm >Aufbau widerstandsfähiger APIs: Fehler, die ich gemacht habe und wie ich sie überwunden habe
APIs sind das Rückgrat moderner Anwendungen. Als ich anfing, APIs mit Spring Boot zu erstellen, war ich so auf die Bereitstellung von Funktionen konzentriert, dass ich einen entscheidenden Aspekt übersehen habe: die Ausfallsicherheit. Ich habe auf die harte Tour gelernt, dass die Fähigkeit einer API, Fehler reibungslos zu verarbeiten und sich an unterschiedliche Bedingungen anzupassen, sie wirklich zuverlässig macht. Lassen Sie mich Ihnen einige Fehler erklären, die ich dabei gemacht habe, und wie ich sie behoben habe. Hoffentlich können Sie diese Fallstricke auf Ihrer eigenen Reise vermeiden.
Was geschah: In einem meiner frühen Projekte habe ich eine API erstellt, die externe Aufrufe an Dienste von Drittanbietern durchführte. Ich ging davon aus, dass diese Dienste immer schnell reagieren würden und machte mir nicht die Mühe, Zeitüberschreitungen festzulegen. Alles schien in Ordnung zu sein, bis der Verkehr zunahm und die Dienste von Drittanbietern langsamer wurden. Meine API blieb einfach auf unbestimmte Zeit hängen und wartete auf eine Antwort.
Auswirkungen: Die Reaktionsfähigkeit der API nahm einen Sturzflug hin. Abhängige Dienste begannen auszufallen, und die Benutzer mussten mit langen Verzögerungen rechnen – einige bekamen sogar den gefürchteten 500 Internal Server Error.
Wie ich das Problem behoben habe: Da wurde mir klar, wie wichtig Timeout-Konfigurationen sind. So habe ich es mit Spring Boot behoben:
@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; } } }
Diese Konfiguration legt nicht nur geeignete Zeitüberschreitungen fest, sondern umfasst auch die Protokollierung, um die Leistung externer Dienste zu überwachen.
Was geschah: Es gab eine Zeit, in der ein interner Dienst, auf den wir angewiesen waren, für mehrere Stunden ausfiel. Meine API hat die Situation nicht ordnungsgemäß gemeistert. Stattdessen wurden diese fehlgeschlagenen Anfragen immer wieder wiederholt, was die Belastung des ohnehin schon überlasteten Systems zusätzlich erhöhte.
Kaskadierende Ausfälle sind eines der größten Probleme in verteilten Systemen. Wenn ein Dienst ausfällt, kann es zu einem Dominoeffekt kommen, der das gesamte System zum Absturz bringt.
Auswirkungen: Die wiederholten Wiederholungsversuche überlasteten das System, verlangsamten andere Teile der Anwendung und beeinträchtigten alle Benutzer.
Wie ich das Problem behoben habe: Da habe ich das Schutzschaltermuster entdeckt. Mit Spring Cloud Resilience4j konnte ich den Teufelskreis durchbrechen.
@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(); } }
Diese einfache Ergänzung verhinderte, dass meine API sich selbst, den internen Dienst oder den Drittanbieterdienst überlastete, und sorgte so für Systemstabilität.
Was geschah: Anfangs habe ich mir nicht viele Gedanken über die Fehlerbehandlung gemacht. Meine API hat entweder generische Fehler ausgegeben (wie HTTP 500 für alles) oder vertrauliche interne Details in Stack-Traces offengelegt.
Auswirkungen: Benutzer waren verwirrt darüber, was schief gelaufen ist, und die Offenlegung interner Details führte zu potenziellen Sicherheitsrisiken.
Wie ich das Problem behoben habe: Ich habe beschlossen, die Fehlerbehandlung mithilfe der @ControllerAdvice-Annotation von Spring zu zentralisieren. Folgendes habe ich getan:
@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; } } }
Dadurch wurden Fehlermeldungen klar und sicher, was sowohl Benutzern als auch Entwicklern hilft.
Was geschah: Eines schönen Tages starteten wir eine Werbekampagne und der Verkehr zu unserer API schoss in die Höhe. Obwohl dies eine großartige Nachricht für das Unternehmen war, begannen einige Benutzer, die API mit Anfragen zu überschwemmen, wodurch anderen Ressourcen entzogen wurden.
Auswirkungen: Die Leistung aller ging zurück und wir erhielten eine Flut von Beschwerden.
Wie ich das Problem behoben habe: Um dieses Problem zu lösen, habe ich eine Ratenbegrenzung mithilfe von Bucket4j mit Redis implementiert. Hier ist ein Beispiel:
@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(); } }
Dies gewährleistete eine faire Nutzung und schützte die API vor Missbrauch.
Was geschah: Immer wenn in der Produktion etwas schief ging, war es wie die Suche nach der Nadel im Heuhaufen. Ich verfügte weder über eine ordnungsgemäße Protokollierung noch über Metriken, sodass die Diagnose von Problemen viel länger dauerte, als es hätte sein sollen.
Auswirkungen: Die Fehlerbehebung wurde zu einem Albtraum, der die Problemlösung verzögerte und die Benutzer frustrierte.
Wie ich das Problem behoben habe: Ich habe Spring Boot Actuator für Zustandsprüfungen hinzugefügt und Prometheus mit Grafana für die Metrikvisualisierung integriert:
@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]"); } }
Ich habe auch eine strukturierte Protokollierung mithilfe des ELK-Stacks (Elasticsearch, Logstash, Kibana) implementiert. Dadurch wurden Protokolle weitaus umsetzbarer.
Der Aufbau widerstandsfähiger APIs ist eine Reise, und Fehler sind Teil des Prozesses. Hier sind die wichtigsten Lektionen, die ich gelernt habe:
Diese Änderungen haben meine Herangehensweise an die API-Entwicklung verändert. Wenn Sie vor ähnlichen Herausforderungen standen oder andere Tipps haben, würde ich gerne Ihre Geschichten hören!
Endbemerkung: Denken Sie daran, dass Resilienz keine Funktion ist, die Sie hinzufügen – es ist eine Eigenschaft, die Sie von Grund auf in Ihr System einbauen. Jede dieser Komponenten spielt eine entscheidende Rolle bei der Erstellung von APIs, die nicht nur funktionieren, sondern auch unter Belastung zuverlässig funktionieren.
Das obige ist der detaillierte Inhalt vonAufbau widerstandsfähiger APIs: Fehler, die ich gemacht habe und wie ich sie überwunden habe. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!