Je suis passé du synchrone à l’asynchrone. Les performances se sont détériorées. Voici ce que j’ai mal compris à propos du parallélisme
Plus de threads ≠ plus de vitesse, surcharge de changement de contexte et pourquoi le blocage des E/S sur 8 cœurs peut battre l’asynchrone sur 32 si vous ne savez pas ce que vous faites
Le message Slack de mon CTO était bref :
“Le temps de réponse de l’API est passé de 450 ms à 2,8 secondes après votre ‘optimisation’.” Reculez maintenant.”
Je regardais l’écran. C’est impossible.
Je viens de passer 3 semaines à refactoriser l’intégralité de notre service de traitement des paiements, depuis les E/S bloquantes synchrones vers une belle architecture asynchrone non bloquante moderne.
Les CompleteableFutures sont partout. Sujets virtuels. Courants-jets. Travaux.
Tous les blogs techniques disaient que l’asynchrone était plus rapide. Chaque conférence a montré que les tests asynchrones tuent les performances de synchronisation.
Alors pourquoi diable notre API de production est-elle désormais 6x LENTE ?
L'”optimisation” qui a tout cassé
Il y a deux mois, notre vice-président de l’ingénierie est revenu d’une conférence.
Vous connaissez ce regard. “Je viens de découvrir que nous faisons les choses mal” énergie.
« Nous devons nous synchroniser », a-t-il annoncé dans notre planning de sprint. “Le blocage des E/S réduit notre débit. Jetez un œil à ce test.”
Il nous a montré une diapositive. Certaines entreprises sont passées de 100 requêtes/s à 10 000 requêtes/s en passant à l’asynchrone.
Notre responsable technique a tenté de répondre : “Notre goulot d’étranglement réside dans les requêtes de base de données, pas dans les threads…”
« Êtes-vous en train de dire que nous ne devrions pas améliorer les performances ?
La réunion est terminée. C’est asynchrone.
Je me suis porté volontaire parce que je voulais avoir l’air intelligent. Je lirais sur CompletableFuture. J’ai vu le Manifeste Réactif. J’étais prêt à être un héros.
Ce que j’ai construit (à la manière “moderne”)
J’ai réécrit notre point de terminaison de traitement des paiements. L’ancienne version synchrone ressemblait à ceci :
@PostMapping("/process-payment")
public PaymentResult processPayment(PaymentRequest request) {
// Simple. Boring. Works.
User user = userService.getUser(request.getUserId());
Account account = accountService.getAccount(user.getAccountId());
PaymentResult result = paymentGateway.charge(account, request.getAmount());
auditLog.record(result);
return result;
}
Temps d’exécution : ~450 ms en moyenne (base de données 300 ms, passerelle de paiement 100 ms, audit 50 ms)
Ma version asynchrone “optimisée”:
@PostMapping("/process-payment")
public CompletableFuture<PaymentResult> processPaymentAsync(PaymentRequest request) {
return CompletableFuture.supplyAsync(() ->
userService.getUserAsync(request.getUserId())
).thenComposeAsync(user ->
accountService.getAccountAsync(user.getAccountId())
).thenComposeAsync(account ->
paymentGateway.chargeAsync(account, request.getAmount())
).thenComposeAsync(result ->
auditLog.recordAsync(result).thenApply(v -> result)
);
}
Magnifique, non ? Non bloquant. Pliant. Réactif.
Je l’ai déployé vendredi après-midi (oui, je sais) après que des tests locaux ont montré qu’il fonctionnait.
Tout est collecté. Tests réussis.
Lundi matin : échec de production.
Des chiffres qui ne mentent pas
Auparavant (synchronisation) :
- Temps de réponse moyen : 450 ms
- P95 : 680 ms
- P99 : 920 ms
- Utilisation du processeur : 35 %
- Nombre de fils : 50 (stable)
- Bande passante : 180 requêtes/sec
Après (mon “optimisation”) :
- Temps de réponse moyen : 2800 ms
- P95 : 4200 ms
- P99 : 8 500 ms
- Utilisation du processeur : 87 %
- Nombre de threads : 250+ (instable)
- Bande passante : 42 requêtes/sec
je l’ai fait 6 fois plus lent et bande passante réduite 76%.
Le directeur technique était… mécontent.
Ce que je me suis trompé (et pourquoi c’est important)
Voici ce que tous les tutoriels asynchrones ne diront pas :
1. L’épuisement du pool de threads est réel
Mon code asynchrone a créé un CompletableFuture pour chaque opération. Chacun d’eux a récupéré un fil de discussion du ForkJoinPool partagé.
En charge nous avions :
- 200 demandes de paiement simultanées
- Chacun crée 4 CompleteableFutures
- 800 tâches en compétition pour 32 cœurs de processeur
Les tâches passaient plus de temps à attendre les threads qu’à s’exécuter réellement.
Coût du changement de contexte : Chaque changement de fil prend 1 à 2 microsecondes. Avec 800 tâches en compétition pour 32 cœurs, nous avons passé environ 60 % du temps CPU à changer de contexte.
Version synchrone ? 50 fils font le vrai travail. Aucune lutte. Pas de commutation.
2. Les frais généraux asynchrones ne sont pas gratuits
Chaque CompleteableFuture a des frais généraux :
- Allocation de tas pour le futur objet
- Créer une fermeture lambda
- Gestion de la chaîne de rappel
- Coordination du pool de threads
Dans notre cas d’utilisation (principalement les appels de base de données liés aux E/S), cette surcharge était PLUS IMPORTANTE que les « économies » réalisées grâce aux E/S non bloquantes.
Mathématiques:
- Surcharge de synchronisation : ~2 ms (création de thread + pile)
- Overhead asynchrone : ~15 ms (création future + rappels + coordination)
Si vos E/S prennent 300 ms, ajouter 15 ms de surcharge asynchrone pour bénéficier des avantages du « non-blocage » est idiot.
3. La base de données ne se soucie pas de vos threads
C’était la partie la plus stupide.
Notre base de données PostgreSQL disposait d’un pool de connexions de 50 connexions.
Approche synchrone : 50 threads, 50 connexions. Un match parfait.
Approche asynchrone : 250 threads tentent de partager 50 connexions.
Le résultat ? Les threads attendent de se connecter à la base de données. Le mode asynchrone était censé résoudre exactement le problème.
J’ai “optimisé” le mauvais calque. Le goulot d’étranglement était le temps de requête PostgreSQL, et non le blocage des threads.
4. La gestion des erreurs est devenue un cauchemar
Gestion des erreurs synchrone :
try {
result = processPayment();
} catch (PaymentException e) {
log.error("Payment failed", e);
return error;
}
Gestion des erreurs asynchrones :
future
.exceptionally(e -> {
log.error("Failed at... which step?", e);
return fallback; // But which fallback?
})
.thenApply(...)
.exceptionally(e -> {
// Is this the same error? A new one?
return anotherFallback;
});
Lorsque les paiements échouaient, le débogage prenait 4 fois plus de temps, car les traces de la pile d’exceptions étaient des déchets asynchrones incompréhensibles.
L’affaire qui m’a tout appris
Au troisième jour de la catastrophe asynchrone, nous avons eu un véritable incident de production.
La passerelle de paiement est en panne. Notre code asynchrone a continué à itérer sur tous les CompletableFutures.
Ce qui s’est passé:
- 200 demandes en vol
- Chacun a créé 4 tâches asynchrones
- Les 800 tâches réessayent la passerelle défaillante
- Le pool de threads est complètement bloqué
- L’API ne répond plus
- La vérification de l’état a échoué
- Instance d’équilibreur de charge supprimée
- Tout le trafic a été redirigé vers d’autres instances
- Échec en cascade à l’échelle du cluster
La version synchronisée aura :
- Crash rapide avec timeout
- 50 threads bloqués (et non 800)
- D’autres demandes sont toujours en cours de traitement
- Dégradation raffinée
Temps d’arrêt :
- Version asynchrone : 47 minutes
- Ancienne version de synchronisation (lors de la restauration) : 4 minutes
Cela fonctionne réellement
Après un rollback et un post-mortem très inconfortable, voici ce que j’ai appris :
Utilisez le mode asynchrone si :
- Vous disposez d’un travail parallèle véritablement lié au processeur
- Votre goulot d’étranglement bloque le thread, n’attendant pas les E/S
- Vous comprenez la taille de votre pool de threads
- Votre équipe peut déboguer les traces de pile asynchrones
N’utilisez pas async si :
- Votre goulot d’étranglement est constitué d’E/S externes (base de données, API)
- Vous disposez de workflows demande-réponse simples
- Votre pool de connexions est le facteur limitant
- Tu veux juste avoir l’air moderne
Correction:
- Code synchrone enregistré
- Goulet d’étranglement réel optimisé (requêtes de base de données d’indexation)
- La taille du pool de connexions a été augmentée en conséquence
- Ajout d’une mise en cache appropriée
Résultats après optimisation réelle :
- Temps de réponse : 180 ms (au lieu de 450 ms)
- Ce code très simple
- Pas de complexité asynchrone
- Pas de sessions de débogage à minuit
La checklist que personne ne vous donne
Avant de « optimiser » pour le mode asynchrone, répondez aux questions suivantes :
[ ] D’abord le profil: Le thread bloque-t-il votre goulot d’étranglement ?
[ ] Vérifiez vos E/S: Attendez-vous des bases de données/API qui ont des limites de connexion ?
[ ] Mesurer les frais généraux: La surcharge asynchrone est-elle inférieure à votre temps d’E/S ?
[ ] Mathématiques du pool de threads: Avez-vous plus de threads que ce que votre aval peut gérer ?
[ ] Gestion des erreurs: Votre équipe peut-elle déboguer les exceptions asynchrones à 3 heures du matin ?
[ ] Test de charge: Avez-vous réellement testé sous une charge de type production ?
Si vous avez répondu non à l’une de ces questions, l’asynchronisation risque d’aggraver les choses.
J’ai dressé une liste de contrôle des performances du backend qui couvre les véritables goulots d’étranglement qui tuent les API en production. C’est quelque chose que j’aimerais vérifier avant de passer 3 semaines à laisser les choses ralentir.
Une vérité qui dérange sur l’architecture « moderne »
LinkedIn regorge de personnes présentant des tests asynchrones.
Ce qu’ils ne montrent pas :
- 6 semaines passées à résoudre des problèmes de production
- Incidents d’épuisement du pool de threads
- Retour en arrière à 2 heures du matin
- Les performances se détériorent
L’async n’est pas magique. C’est un outil. Et comme tout outil, il peut empirer les choses s’il est mal utilisé.
Code synchrone ennuyeux avec une indexation appropriée > Code asynchrone sophistiqué avec un parallélisme mal compris
Si ça casse toujours
Même avec la bonne architecture, la production interrompt le parcours créatif.
Désormais, lorsque des incidents surviennent dans notre système de paiement, nous utilisons ProdRescue AI pour générer une analyse des causes profondes à partir de nos journaux Slack et du chaos. Parce que je ne veux pas passer encore 47 minutes à déboguer manuellement la trace de la pile asynchrone jusqu’à ce que les clients puissent payer.
Pour les défis spécifiques du parallélisme Java et la préparation aux entretiens, j’ai compilé 120 véritables questions d’entretien qui sont réellement posées aux ingénieurs seniors. Le parallélisme est toujours là. Parce que les enquêteurs savent que la plupart des gens ne comprennent pas vraiment.
Une vraie leçon
J’ai passé 3 semaines à rendre notre API 6x plus lente parce que j’ai mal optimisé.
Le vrai problème ? L’index de la base de données est manquant.
La « solution » que j’ai créée ? Architecture asynchrone complexe qui ajoute de la latence et de la fragilité.
Qu’est-ce qui fonctionnerait :
- Exécutez EXPLAIN pour les requêtes lentes (5 minutes)
- Ajouter l’index manquant (2 minutes)
- Le temps de réponse tombe à 180 ms
- Pas de complexité asynchrone
- Il n’y a pas d’accidents industriels
Mais ce n’est pas sexuel. Il ne propose pas de conférences. Cela ne prétend pas que vous utilisez une technologie « moderne ».
Cela fonctionne.
Et il arrive qu’il suffit de travailler.
Vous voulez apprendre des désastres de performance des autres ? Je partage les échecs de production réels, les chiffres réels et ce qui les a réellement résolus dans ma sous-pile. Pas de mots à la mode. Il n’y a pas de “performance 10x avec cette astuce incroyable”. Juste des post-mortems honnêtes d’ingénieurs qui ont appris de manière coûteuse.
Après tout, le meilleur professeur est l’événement de production de quelqu’un d’autre.
Je suis passé du synchrone à l’asynchrone. a été initialement publié sur Stackademic on Medium, où les gens poursuivent la conversation en soulignant et en répondant à cette histoire.
PakarPBN
A Private Blog Network (PBN) is a collection of websites that are controlled by a single individual or organization and used primarily to build backlinks to a “money site” in order to influence its ranking in search engines such as Google. The core idea behind a PBN is based on the importance of backlinks in Google’s ranking algorithm. Since Google views backlinks as signals of authority and trust, some website owners attempt to artificially create these signals through a controlled network of sites.
In a typical PBN setup, the owner acquires expired or aged domains that already have existing authority, backlinks, and history. These domains are rebuilt with new content and hosted separately, often using different IP addresses, hosting providers, themes, and ownership details to make them appear unrelated. Within the content published on these sites, links are strategically placed that point to the main website the owner wants to rank higher. By doing this, the owner attempts to pass link equity (also known as “link juice”) from the PBN sites to the target website.
The purpose of a PBN is to give the impression that the target website is naturally earning links from multiple independent sources. If done effectively, this can temporarily improve keyword rankings, increase organic visibility, and drive more traffic from search results.
Jasa Backlink
Download Anime Batch
