Maison >Java >javaDidacticiel >Créer des API résilientes : les erreurs que j'ai commises et comment je les ai surmontées
Les API sont l'épine dorsale des applications modernes. Lorsque j’ai commencé à créer des API avec Spring Boot, j’étais tellement concentré sur la fourniture de fonctionnalités que j’ai négligé un aspect crucial : la résilience. J’ai appris à mes dépens que la capacité d’une API à gérer les pannes avec élégance et à s’adapter à différentes conditions est ce qui la rend vraiment fiable. Laissez-moi vous expliquer quelques erreurs que j'ai commises en cours de route et comment je les ai corrigées. J'espère que vous pourrez éviter ces pièges au cours de votre propre voyage.
Que s'est-il passé : Dans l'un de mes premiers projets, j'ai construit une API qui effectuait des appels externes vers des services tiers. J’ai supposé que ces services répondraient toujours rapidement et ne prenaient pas la peine de définir des délais d’attente. Tout semblait bien jusqu'à ce que le trafic augmente et que les services tiers commencent à ralentir. Mon API se bloquerait indéfiniment, en attendant une réponse.
Impact : La réactivité de l’API a plongé. Les services dépendants ont commencé à échouer et les utilisateurs ont été confrontés à de longs retards : certains ont même eu la redoutable erreur de serveur interne 500.
Comment je l'ai résolu : C'est à ce moment-là que j'ai réalisé l'importance des configurations de délai d'attente. Voici comment je l'ai corrigé avec 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; } } }
Cette configuration définit non seulement des délais d'attente appropriés, mais inclut également la journalisation pour aider à surveiller les performances des services externes.
Que s'est-il passé : Il fut un temps où notre service interne sur lequel nous comptions était tombé en panne pendant plusieurs heures. Mon API n’a pas géré la situation avec élégance. Au lieu de cela, il réessayait sans cesse les requêtes qui échouaient, ajoutant ainsi une charge supplémentaire au système déjà sollicité.
Les pannes en cascade sont l'un des problèmes les plus difficiles dans les systèmes distribués. Lorsqu'un service tombe en panne, cela peut créer un effet domino qui fait tomber l'ensemble du système.
Impact : Les tentatives répétées ont submergé le système, ralentissant d'autres parties de l'application et affectant tous les utilisateurs.
Comment je l'ai réparé : C'est à ce moment-là que j'ai découvert le modèle du disjoncteur. Grâce à Spring Cloud Resilience4j, j'ai pu briser le cycle.
@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(); } }
Ce simple ajout a empêché mon API de se submerger, le service interne ou le service tiers, garantissant ainsi la stabilité du système.
Que s'est-il passé : Au début, je n'ai pas beaucoup réfléchi à la gestion des erreurs. Mon API a soit généré des erreurs génériques (comme HTTP 500 pour tout), soit exposé des détails internes sensibles dans les traces de pile.
Impact : Les utilisateurs ne comprenaient pas ce qui n'allait pas, et la divulgation de détails internes créait des risques de sécurité potentiels.
Comment je l'ai corrigé : J'ai décidé de centraliser la gestion des erreurs à l'aide de l'annotation @ControllerAdvice de Spring. Voici ce que j'ai fait :
@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; } } }
Cela rend les messages d'erreur clairs et sécurisés, aidant à la fois les utilisateurs et les développeurs.
Que s'est-il passé : Un beau jour, nous avons lancé une campagne promotionnelle et le trafic vers notre API est monté en flèche. Bien qu'il s'agisse d'une excellente nouvelle pour l'entreprise, certains utilisateurs ont commencé à envoyer des requêtes à l'API, privant ainsi les autres de ressources.
Impact : Performances dégradées pour tout le monde, et nous avons reçu un flot de plaintes.
Comment je l'ai résolu : Pour gérer cela, j'ai implémenté une limitation de débit à l'aide de Bucket4j avec Redis. Voici un exemple :
@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(); } }
Cela garantissait une utilisation équitable et protégeait l'API contre les abus.
Ce qui s'est passé : Chaque fois que quelque chose n'allait pas dans la production, c'était comme chercher une aiguille dans une botte de foin. Je n'avais pas mis en place de journalisation ou de métriques appropriées, donc le diagnostic des problèmes a pris beaucoup plus de temps que prévu.
Impact : Le dépannage est devenu un cauchemar, retardant la résolution des problèmes et frustrant les utilisateurs.
Comment je l'ai corrigé : J'ai ajouté Spring Boot Actuator pour les contrôles de santé et intégré Prometheus à Grafana pour la visualisation des métriques :
@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]"); } }
J'ai également implémenté une journalisation structurée à l'aide de la pile ELK (Elasticsearch, Logstash, Kibana). Cela a rendu les journaux beaucoup plus exploitables.
Construire des API résilientes est un voyage, et les erreurs font partie du processus. Voici les principales leçons que j'ai apprises :
Ces changements ont transformé ma façon d'aborder le développement d'API. Si vous avez été confronté à des défis similaires ou si vous avez d’autres conseils, j’aimerais entendre vos histoires !
Remarque de fin : n'oubliez pas que la résilience n'est pas une fonctionnalité que vous ajoutez, c'est une caractéristique que vous intégrez à votre système à partir de zéro. Chacun de ces composants joue un rôle crucial dans la création d'API qui non seulement fonctionnent, mais continuent de fonctionner de manière fiable sous pression.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!