Maison > Java > javaDidacticiel > Moderniser les monolithes Java pour de meilleures performances avec des architectures asynchrones et non bloquantes

Moderniser les monolithes Java pour de meilleures performances avec des architectures asynchrones et non bloquantes

Susan Sarandon
Libérer: 2024-11-17 06:13:03
original
536 Les gens l'ont consulté

Modernizing Java Monoliths for Better Performance with Async and Non-Blocking Architectures

Dans un projet récent, j'ai modernisé un service Web Java monolithique vieillissant écrit en Dropwizard. Ce service gérait un certain nombre de dépendances tierces (3P) via les fonctions AWS Lambda, mais les performances étaient à la traîne en raison de la nature synchrone et bloquante de l'architecture. La configuration avait une latence P99 de 20 secondes, bloquant les threads de requête en attendant la fin des fonctions sans serveur. Ce blocage a provoqué une saturation du pool de threads, entraînant des échecs fréquents de requêtes pendant les pics de trafic.

Identifier le goulot d'étranglement des performances

Le nœud du problème était que chaque requête adressée à une fonction Lambda occupait un thread de requête dans le service Java. Étant donné que ces fonctions 3P prenaient souvent un temps considérable, les threads qui les géraient restaient bloqués, consommant des ressources et limitant l'évolutivité. Voici un exemple de ce à quoi ressemble ce comportement de blocage dans le code :

// Blocking code example
public String callLambdaService(String payload) {
    String response = externalLambdaService.invoke(payload);
    return response;
}
Copier après la connexion
Copier après la connexion

Dans cet exemple, la méthode callLambdaService attend que externalLambdaService.invoke() renvoie une réponse. Pendant ce temps, aucune autre tâche ne peut utiliser le fil.

Solution : migration vers des modèles asynchrones et non bloquants

Pour résoudre ces goulots d'étranglement, j'ai réorganisé le service en utilisant des méthodes asynchrones et non bloquantes. Ce changement impliquait l'utilisation d'un client HTTP qui appelait les fonctions Lambda pour utiliser AsyncHttpClient à partir de la bibliothèque org.asynchttpclient, qui utilise en interne un EventLoopGroup pour gérer les requêtes de manière asynchrone.

L'utilisation d'AsyncHttpClient a permis de décharger les opérations de blocage sans consommer de threads du pool. Voici un exemple de ce à quoi ressemble l'appel non bloquant mis à jour :

// Non-blocking code example
public CompletableFuture<String> callLambdaServiceAsync(String payload) {
    return CompletableFuture.supplyAsync(() -> {
        return asyncHttpClient.invoke(payload);
    });
}
Copier après la connexion
Copier après la connexion

Tirer parti de CompletableFuture de Java pour enchaîner les appels asynchrones

En plus de rendre les appels individuels non bloquants, j'ai enchaîné plusieurs appels de dépendances à l'aide de CompletableFuture. Avec des méthodes telles que thenCombine et thenApply, je pouvais récupérer et combiner de manière asynchrone des données provenant de plusieurs sources, augmentant ainsi considérablement le débit.

CompletableFuture<String> future1 = callLambdaServiceAsync(payload1);
CompletableFuture<String> future2 = callLambdaServiceAsync(payload2);

CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> {
    return processResults(result1, result2);
});
Copier après la connexion
Copier après la connexion

Présentation de la sécurité des types avec une classe SafeAsyncResponse personnalisée

Lors de l'implémentation, j'ai observé que l'objet AsyncResponse par défaut de Java manquait de sécurité de type, ce qui permettait de transmettre des objets Java arbitraires. Pour résoudre ce problème, j'ai créé une classe SafeAsyncResponse avec des génériques, qui garantissait que seul le type de réponse spécifié pouvait être renvoyé, favorisant ainsi la maintenabilité et réduisant le risque d'erreurs d'exécution. Cette classe enregistre également les erreurs si une réponse est écrite plusieurs fois.

// Blocking code example
public String callLambdaService(String payload) {
    String response = externalLambdaService.invoke(payload);
    return response;
}
Copier après la connexion
Copier après la connexion

Exemple d'utilisation de SafeAsyncResponse

// Non-blocking code example
public CompletableFuture<String> callLambdaServiceAsync(String payload) {
    return CompletableFuture.supplyAsync(() -> {
        return asyncHttpClient.invoke(payload);
    });
}
Copier après la connexion
Copier après la connexion

Tests et gains de performances

Pour vérifier l'efficacité de ces changements, j'ai écrit des tests de charge à l'aide de threads virtuels pour simuler un débit maximal sur une seule machine. J'ai généré différents niveaux de temps d'exécution de fonctions sans serveur (allant de 1 à 20 secondes) et j'ai constaté que la nouvelle implémentation asynchrone non bloquante augmentait le débit de 8x pour des temps d'exécution inférieurs et d'environ 4x pour des temps d'exécution plus élevés.

En mettant en place ces tests de charge, je me suis assuré d'ajuster les limites de connexion au niveau client pour maximiser le débit, ce qui est essentiel pour éviter les goulots d'étranglement dans les systèmes asynchrones.

Découverte d'un bug caché dans le client HTTP

Lors de l'exécution de ces tests de stress, j'ai découvert un bug caché dans notre client HTTP personnalisé. Le client a utilisé un sémaphore avec un délai d'expiration de connexion défini sur Integer.MAX_VALUE, ce qui signifie que si le client manquait de connexions disponibles, il bloquerait le thread indéfiniment. La résolution de ce bug était cruciale pour éviter les blocages potentiels dans les scénarios à charge élevée.

Le choix entre les threads virtuels et le code asynchrone traditionnel

On pourrait se demander pourquoi nous ne sommes pas simplement passés aux threads virtuels, ce qui peut réduire le besoin de code asynchrone en permettant aux threads de se bloquer sans coût de ressources important. Cependant, il existe actuellement une limitation avec les threads virtuels : ils sont épinglés lors des opérations synchronisées. Cela signifie que lorsqu'un thread virtuel entre dans un bloc synchronisé, il ne peut pas être démonté, bloquant potentiellement les ressources du système d'exploitation jusqu'à ce que l'opération soit terminée.

Par exemple :

CompletableFuture<String> future1 = callLambdaServiceAsync(payload1);
CompletableFuture<String> future2 = callLambdaServiceAsync(payload2);

CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> {
    return processResults(result1, result2);
});
Copier après la connexion
Copier après la connexion

Dans ce code, si read bloque parce qu'il n'y a pas de données disponibles, le thread virtuel est épinglé à un thread du système d'exploitation, l'empêchant de démonter et bloquant également le thread du système d'exploitation.

Heureusement, avec la JEP 491 à l'horizon, les développeurs Java peuvent s'attendre à un comportement amélioré des threads virtuels, où les opérations de blocage dans le code synchronisé peuvent être gérées plus efficacement sans épuiser les threads de la plateforme.

Conclusion

En refactorisant notre service vers une architecture asynchrone non bloquante, nous avons obtenu des améliorations significatives des performances. En implémentant AsyncHttpClient, en introduisant SafeAsyncResponse pour la sécurité des types et en effectuant des tests de charge, nous avons pu optimiser notre service Java et améliorer considérablement le débit. Ce projet a constitué un exercice précieux dans la modernisation des applications monolithiques et a révélé l'importance de pratiques asynchrones appropriées pour l'évolutivité.

À mesure que Java évolue, nous pourrons peut-être exploiter les threads virtuels plus efficacement à l'avenir, mais pour l'instant, l'architecture asynchrone et non bloquante reste une approche essentielle pour l'optimisation des performances dans les services à haute latence et dépendants de tiers.

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!

source:dev.to
Déclaration de ce site Web
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
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal