Maison >Java >javaDidacticiel >Créer des API résilientes : les erreurs que j'ai commises et comment je les ai surmontées

Créer des API résilientes : les erreurs que j'ai commises et comment je les ai surmontées

Mary-Kate Olsen
Mary-Kate Olsenoriginal
2025-01-04 15:48:40620parcourir

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

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.

Erreur 1 : ignorer les configurations de délai d'attente

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.

Erreur 2 : ne pas mettre en œuvre de disjoncteurs

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.

Erreur 3 : faible gestion des erreurs

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.

Erreur 4 : négliger la limitation du taux

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.

Erreur 5 : négliger l'observabilité

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.

Points à retenir

Construire des API résilientes est un voyage, et les erreurs font partie du processus. Voici les principales leçons que j'ai apprises :

  1. Toujours configurer des délais d'attente pour les appels externes.
  2. Utilisez des disjoncteurs pour éviter les pannes en cascade.
  3. Centralisez la gestion des erreurs pour la rendre claire et sécurisée.
  4. Mettre en œuvre une limitation du débit pour gérer les pics de trafic.

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!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn