[JAVA] À propos de la tolérance aux pannes MicroProfile

Lors de la création d'un service avec un micro-service, il est très important de tenir compte de la tolérance aux pannes lors de sa mise en œuvre. Les stratégies de nouvelle tentative, les cloisons, les disjoncteurs, etc. sont des concepts très importants qui sont également définis dans le modèle de conception de microservice.

La tolérance aux pannes de MicroProfile fournit les fonctionnalités dont vous avez besoin pour créer ces services tolérants aux pannes. L'implémentation est facile à développer sur la base des annotations CDI et fonctionne avec les intercepteurs CDI (les classes doivent être implémentées comme des beans CDI). Cela sépare le code redondant pour la logique métier et la correction des pannes et facilite sa mise en œuvre.

La politique de tolérance aux pannes de MicroProfile peut être gérée en dehors des paramètres externes, et la gestion des politiques peut également être effectuée à l'aide de MicroProfile Config.

Principales fonctionnalités incluses dans la spécification Fault Tolerance

Fonctionnalités fournies par Fault Tolerance Annotations à utiliser et brève description
1.temps libre: @Utilisez l'annotation Timeout. Définit le temps maximum requis pour le traitement
2.recommencez: @Utilisez l'annotation Réessayer. Définir l'opération de nouvelle tentative en cas d'échec du traitement
3.Se retirer: @Utilisez l'annotation de secours. Fournit une méthode alternative (appelant une autre méthode) lorsque le traitement échoue
4.Cloison(Cloison): @Utilisez l'annotation Bulkhead. Limitez le nombre d'exécutions simultanées. Par conséquent, lorsque la charge est élevée, la charge est concentrée sur un seul processus et la réponse est réduite, ce qui évite les pannes de chaîne sur l'ensemble du système.
5.Disjoncteur: @Utilisez l'annotation CircuitBreaker. Fait échouer automatiquement l'appel de processus immédiatement si le processus échoue à plusieurs reprises
6.asynchrone: @Utilisez l'annotation asynchrone. Rendre le processus asynchrone

Fondamentalement, si vous souhaitez appliquer l'une des politiques ci-dessus (plusieurs spécifications sont possibles), vous pouvez la définir en ajoutant simplement une annotation à la classe ou à la méthode à implémenter.


1. Politique de temporisation (@Timeout)

En définissant un délai d'attente, vous pouvez éviter d'attendre la fin du traitement. Si vous ne définissez pas de délai d'expiration, en cas de défaillance du réseau ou si la destination de connexion est surchargée et que vous ne pouvez pas renvoyer une réponse immédiatement, les threads de travail du pool de connexions de l'appelant seront épuisés, ce qui peut également mettre une charge sur l'appelant. ne pas. Par conséquent, lors de l'implémentation de plusieurs microservices ou de l'appel d'un service externe, définissez un délai d'expiration pour la coopération entre chaque service.

@Timeout(400) //Valeur du délai de connexion 400 ms(0.4 sec)
public Connection getConnectionForServiceA() {
   Connection conn = connectionService();
   return conn;
}

L'annotation @Timeout peut être ajoutée au niveau de la classe ou de la méthode. Si la valeur du délai d'expiration est atteinte, une exception TimeoutException sera levée.

2. Stratégie de nouvelle tentative (@Retry)

Si vous rencontrez une panne réseau mineure ou si vous n'obtenez pas de réponse de la destination, vous pouvez utiliser l'annotation @Retry pour réessayer l'appel de processus.

Vous pouvez configurer les éléments suivants dans la stratégie de nouvelle tentative:

Paramètres La description
maxRetries: Nombre maximum de tentatives
delay: Intervalle de réessai
delayUnit: unité de retard
maxDuration: Durée maximale de la nouvelle tentative
durationUnit: unité de durée
jitter: Changement aléatoire du délai de nouvelle tentative(Écart de synchronisation (ou cycle) du signal d'horloge)
jitterDelayUnit: unité de gigue
retryOn: Échec de la nouvelle tentative(Exception, Error)Spécifier
abortOn: Défaut d'annuler(Exception, Error)Spécifier

L'annotation @Retry peut être ajoutée au niveau de la classe ou de la méthode, et lorsqu'elle est ajoutée à une classe, elle sera appliquée à toutes les méthodes existantes dans la classe. Lorsqu'elle est ajoutée à une méthode, seule la méthode spécifiée est ciblée. Si vous ajoutez une annotation dans la classe et également dans la méthode, les paramètres spécifiés dans la méthode prendront effet.

  1. Si le processus se termine normalement, le résultat est renvoyé normalement.
  2. Si l'exception levée est spécifiée avec abortOn, l'exception levée sera retransmise.
  3. Si vous spécifiez l'exception levée avec retryOn, l'appel de méthode sera retenté.
  4. Sinon, renvoyez l'exception levée

Il peut également être utilisé avec d'autres annotations de tolérance aux pannes.

    /**
     * serviceA()Lorsqu'une exception est levée dans un appel de méthode
     *Si l'exception n'est pas IOException, réessayez.
     */
    @Retry(retryOn = Exception.class, abortOn = IOException.class)
    public void invokeService() {
        callServiceA();
    }

    /**
     *Définissez le nombre maximal de tentatives sur 90 et la durée maximale des tentatives sur 1000 millisecondes
     *Lorsque le nombre maximum de tentatives est atteint, aucune tentative n'est effectuée même si le nombre maximum de tentatives n'a pas été atteint.
     */
    @Retry(maxRetries = 90, maxDuration= 1000)
    public void serviceB() {
        callServiceB();
    }

    /**
    *Décalage de fréquence d'horloge(jitter)En supposant 400 ms-400 ms à 400 ms
    * 0 (delay - jitter) 〜 800ms (delay + jitter )On s'attend à ce que les tentatives soient effectuées avec la différence de.
    *3200, en supposant que le délai maximum se produit/800=À 4, le nombre minimum d'essais est de 4 ou plus,
    *Définissez le nombre maximum de tentatives pour ne pas dépasser 10
    */
    @Retry(delay = 400, maxDuration= 3200, jitter= 400, maxRetries = 10)
    public Connection serviceA() {
        return getConnectionForServiceA();
    }

3. Politique de secours (@Fallback)

L'annotation @Fallback peut être spécifiée au niveau de la méthode. Si une méthode annotée entraîne une exception et se ferme, la méthode spécifiée dans la méthode de secours est appelée.

L'annotation @Fallback peut être utilisée seule ou avec d'autres annotations Fault Tolerance. Lorsqu'il est utilisé avec d'autres annotations, le repli est appelé une fois que toutes les autres opérations de tolérance aux pannes ont été effectuées.

Par exemple, si @Retry est défini, le traitement de secours sera effectué si le nombre maximal de tentatives est dépassé. De plus, si @CircuitBreaker est défini ensemble, il sera appelé immédiatement si l'appel de méthode échoue. Et chaque fois que le circuit est ouvert, la méthode de secours est appelée.

3.1 Exemple d'implémentation du traitement de secours en implémentant FallbackHandler

Définissez une classe (ServiceInvocationAFallbackHandler) pour FallbackHandler qui implémente l'interface FallbackHandler. Ensuite, implémentez le traitement alternatif dans la méthode handle.

Ici, il est implémenté pour répondre à la chaîne de caractères définie par la propriété de app.serviceinvokeA.FallbackReplyMessage ou la variable d'environnement à l'aide de MicroProfile Config.

@Dependent
public class ServiceInvocationAFallbackHandler implements FallbackHandler<String> {

    @ConfigProperty(name="app.serviceinvokeA.FallbackReplyMessage", defaultValue = "Unconfigured Default Reply")
    private String replyString;

    @Override
    public String handle(ExecutionContext ec) {
        return replyString;
    }
}

Lorsque la méthode BusinessLogicServiceBean # invokeServiceA () ci-dessous est appelée, une RuntimeException est déclenchée en interne ici, mais le processus est retenté trois fois. Une fois toutes les tentatives échouées, ServiceInvocationAFallbackHandler # handle () est appelé. Sera.

@RequestScoped
public class BusinessLogicServiceBean {

    //Spécifiez la classe d'implémentation de FallbackHandler@Ajouter une annotation de secours
    //Nombre maximum de tentatives(3 fois)S'il dépasse, handle de FallbackHandler()La méthode s'appelle
    @Retry(maxRetries = 3)
    @Fallback(ServiceInvocationAFallbackHandler.class)
    public String invokeServiceA() {
        throw new RuntimeException("Connection failed");
        return null;
    }   
}

3.2 Exemple d'implémentation de traitement de secours avec méthode de secours spécifiée

Décrivez le nom de la méthode à appeler comme alternative directement dans l'annotation @Fallback.

Ici, la méthode fallbackForServiceB () est définie comme une méthode alternative.

@RequestScoped
public class BusinessLogicServiceBean {

    @Retry(maxRetries = 3)
    @Fallback(fallbackMethod= "fallbackForServiceB")
    public String invokeServiceB() {
        counterForInvokingServiceB++;
       return nameService();
    }


    @ConfigProperty(name="app.serviceinvokeB.FallbackReplyMessage", defaultValue = "Unconfigured Default Reply")
    private String replyString;

    private String fallbackForInvokeServiceB() {
        return replyString;
    }

4. Politique de cloison (@Bulkhead)

Le modèle de cloison est utilisé pour empêcher certaines défaillances du système de se propager dans tout le système et de provoquer une panne de l'ensemble du système. L'implémentation MicroProfile limite le nombre de requêtes simultanées qui accèdent à une instance.

Le modèle Bulkhead est efficace lorsqu'il est appliqué aux composants qui peuvent être appelés en grand nombre ou aux services qui entraînent une mauvaise réponse sous une charge élevée.

L'annotation @Bulkhead peut être ajoutée au niveau de la classe ou au niveau de la méthode, et lorsqu'elle est ajoutée à une classe, elle sera appliquée à toutes les méthodes existantes dans la classe. Lorsqu'elle est ajoutée à une méthode, seule la méthode spécifiée est ciblée. Si vous ajoutez une annotation dans la classe et également dans la méthode, les paramètres spécifiés dans la méthode prendront effet.

La cloison peut être réglée des deux manières suivantes.

  1. Isolation du pool de threads: (lorsqu'il est utilisé avec l'annotation @Asynchronous) Définit le nombre maximal de demandes simultanées pour la taille de la file d'attente dans le pool de threads.
  2. Séparation des sémaphos: (lorsqu'il n'est pas utilisé avec l'annotation @Asynchronous) Seul le réglage du nombre de demandes simultanées est autorisé.

4.1 Exemple de séparation par pool de threads

Lorsqu'il est utilisé avec l'annotation @Asynchronous, l'isolation du pool de threads est appliquée. Dans l'exemple ci-dessous, jusqu'à 5 demandes simultanées sont autorisées et 8 demandes sont conservées dans la file d'attente.

//Jusqu'à 5 demandes simultanées autorisées, jusqu'à 8 demandes autorisées dans la file d'attente
@Asynchronous
@Bulkhead(value = 5, waitingTaskQueue = 8)
public Future<Connection> invokeServiceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return CompletableFuture.completedFuture(conn);
}

4.2 Exemple de séparation par sémapho

Si vous n'utilisez pas l'annotation @Asynchronous, définissez simplement le nombre de requêtes simultanées.

@Bulkhead(5) //Jusqu'à 5 demandes simultanées sont autorisées
public Connection invokeServiceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return conn;
}

5. Politique de disjoncteur (@CircuitBreaker)

Le disjoncteur empêche les appels répétés au service en échec de sorte que le service ou l'appel API échoué échoue immédiatement. Si un appel de service échoue fréquemment, le disjoncteur s'ouvre et aucun appel à ce service n'est tenté avant un certain laps de temps.

L'annotation @CircuitBreaker peut être ajoutée au niveau de la classe ou au niveau de la méthode, et lorsqu'elle est ajoutée à une classe, elle sera appliquée à toutes les méthodes existantes dans la classe. Lorsqu'elle est ajoutée à une méthode, seule la méthode spécifiée est ciblée. Si vous ajoutez une annotation dans la classe et également dans la méthode, les paramètres spécifiés dans la méthode prendront effet.

Trois états du disjoncteur

Fermé: (heure normale)

Normalement, le disjoncteur est fermé. Le disjoncteur garde une trace de la réussite ou de l'échec de chaque appel et garde la trace des derniers résultats. Lorsque le taux de défaillance dépasse le taux de défaillance, le disjoncteur s'ouvre.

Ouvert: (en cas de panne)

Si le disjoncteur est ouvert, les appels aux services exécutés sur le disjoncteur échoueront immédiatement avec une exception CircuitBreakerOpenException. Après un certain temps (configurable), le disjoncteur passera à l'état semi-ouvert.

Semi-ouvert: (Confirmation de la reprise après sinistre)

Dans l'état semi-ouvert, les tentatives d'appel de service commencent (numéro configurable). Si l'un ou l'autre des appels échoue, le disjoncteur reviendra à l'état ouvert. Si toutes les tentatives réussissent, le disjoncteur passera à l'état fermé.

Exemple de mise en œuvre du disjoncteur 1

@CircuitBreaker(successThreshold = 10, requestVolumeThreshold = 4, failureRatio=0.5, delay = 1000)
public Connection serviceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return conn;
}
Paramètres La description
requestVolumeThreshold: Fenêtre roulante à utiliser lorsque le disjoncteur est «fermé»(Nombre de dénominateurs pour le calcul du taux de défaut)Taille
failureRatio: Rapport de défaut dans la fenêtre déroulante pour "ouvrir" le disjoncteur
successThreshold: Nombre de tentatives pour passer à fermé lorsque le disjoncteur est «à moitié ouvert»
delay et delayUnit: Il est temps de garder le disjoncteur «ouvert»

Dans ce qui précède, le circuit «s'ouvre» lorsque deux (4 x 0,5) échecs se produisent pendant quatre appels consécutifs, qui est le nombre de fenêtres glissantes spécifié par requestVolumeThreshold. Le circuit reste «ouvert» pendant 1 000 millisecondes avant de passer à «semi-ouvert». Après 10 appels réussis en "Semi-Ouvert", le circuit sera à nouveau "Fermé".

Demande 1-Succès
Requête 2-Échec
Demande 3-Succès
Requête 4-Succès
Requête 5-Échec
Demande 6-CircuitBreakerOpenException

Pour la demande ci-dessus, deux des quatre dernières demandes échoueront et le failureRatio atteindra 0,5, donc "Request 5" ouvrira le circuit et lèvera une CircuitBreakerOpenException.

Ajout d'une définition d'exception pour prendre en compte le succès / l'échec

Les paramètres failOn et skipOn sont utilisés pour déterminer quelles exceptions sont considérées comme échouant car ils déterminent si le disjoncteur doit être "ouvert".

@CircuitBreaker(successThreshold = 10, requestVolumeThreshold = 4, failureRatio=0.5, delay = 1000,failOn = {ExceptionA.class, ExceptionB.class}, skipOn = ExceptionBSub.class))
public Connection serviceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return conn;
}

Si l'exception spécifiée pour failOn se produit, elle est considérée comme un échec. Si l'exception spécifiée pour skipOn se produit, elle est considérée comme réussie.

6. Stratégie asynchrone (@ asynchrone)

Les principales fonctionnalités de Fault Tolerance sont décrites dans Architecture , Voici les fonctions répertoriées en 1-5 ci-dessus. Le traitement asynchrone n'est donc pas directement lié à la tolérance aux pannes. Cependant, le traitement asynchrone est très important dans le traitement distribué, et en le combinant avec diverses fonctions de tolérance aux pannes, il a été intégré dans les spécifications pour fonctionner plus efficacement.

Source: Architecture

As mentioned above, the Fault Tolerance specification is to focus on the following aspects:

  • Timeout: Define a duration for timeout
  • Retry: Define a criteria on when to retry
  • Fallback: provide an alternative solution for a failed execution.
  • CircuitBreaker: offer a way of fail fast by automatically failing execution to prevent the system overloading and indefinite wait or timeout by the clients.
  • Bulkhead: isolate failures in part of the system while the rest part of the system can still function.

L'annotation @Asynchronous peut être ajoutée au niveau de la classe ou de la méthode, et lorsqu'elle est ajoutée à une classe, elle sera appliquée à toutes les méthodes existantes dans la classe. Lorsqu'elle est ajoutée à une méthode, seule la méthode spécifiée est ciblée. Si vous ajoutez une annotation dans la classe et également dans la méthode, les paramètres spécifiés dans la méthode prendront effet.

Dès qu'une méthode avec l'annotation @Asynchronous est appelée, elle renvoie Future ou CompletionStage. Le reste du traitement du corps de méthode est exécuté dans un thread séparé. Le Future ou CompletionStage retourné n'aura pas la valeur correcte tant que le processus asynchrone ne sera pas terminé. Si une exception se produit pendant le traitement, Future ou CompletionStage se terminera avec cette exception. Si le processus se termine avec succès, Future ou CompletionStage renvoie une valeur de retour (elle-même Future ou CompletionStage).

@Asynchronous
public CompletionStage <Connection> serviceA(){
   Connection conn = null;
   counterForInvokingServiceA ++;
   conn = connectionService();
   return CompletableFuture.completedFuture(conn);
}

Dans l'exemple ci-dessus, l'appel à la méthode serviceA est asynchrone. L'appel à serviceA renvoie CompletionStage et le corps de la méthode est exécuté dans un thread séparé.

Mise en garde: Lors de l'appel de @Asynchronous à partir d'un RequestScope CDI, le RequestScope doit être actif lors de l'appel de méthode asynchrone. Les méthodes avec l'annotation @Asynchronous doivent renvoyer le Future ou CompletionStage du package java.util.concurrent. Sinon, vous obtiendrez une FaultToleranceDefinitionException.

Comment écraser la valeur de réglage décrite dans le code source

Comme nous l'avons vu dans chaque section, les stratégies de tolérance aux pannes peuvent être appliquées à l'aide d'annotations dans la plupart des cas, à quelques exceptions près. Après avoir implémenté le code source, si vous souhaitez modifier la valeur implémentée par l'annotation, vous pouvez également utiliser MicroProfile Config pour écraser la valeur du paramètre.

Les paramètres de l'annotation peuvent être remplacés dans les propriétés de configuration à l'aide de la convention de dénomination suivante:

<classname>/<methodname>/<annotation>/<parameter>

Par exemple, si vous souhaitez remplacer en externe les paramètres spécifiés dans le délai d'expiration spécifié dans une méthode spécifique ou dans l'annotation Réessayer, écrivez comme suit dans MicroProfile Config.

com.yoshio3.FaultToleranceService.resilient.ResilienceController/checkTimeout/Timeout/value=2000
com.yoshio3.FaultToleranceService.resilient.ResilienceController/checkTimeout/Retry/maxDuration=3000

Si vous souhaitez l'appliquer à toute la classe, vous pouvez supprimer la partie du nom de la méthode et l'appliquer à toute la classe comme indiqué ci-dessous.

com.yoshio3.FaultToleranceService.resilient.ResilienceController/Timeout/value=2000
com.yoshio3.FaultToleranceService.resilient.ResilienceController/Retry/maxDuration=3000

Et si vous souhaitez appliquer les mêmes règles à tout le code de votre projet, vous pouvez simplement lister les annotations et les paramètres.

Timeout/value=2000
Retry/maxDuration=3000

à la fin

Voici le code pour créer une application tolérante aux pannes qui exploite MicroProfile Fault Tolerance. Ensuite, j'aimerais créer une application qui utilise Fault Tolerance et lier plusieurs services tolérants aux pannes sur Azure.

Recommended Posts

À propos de la tolérance aux pannes MicroProfile
À propos d'Eclipse MicroProfile Config
À propos =