A propos de l'auteur
Wen est ingénieur senior en développement back-end chez Ctrip, se concentrant sur les performances du système, la stabilité, les systèmes commerciaux et d'autres domaines.
1. Origines
À l'ère de l'information d'aujourd'hui, la technologie des microservices est devenue une solution importante. La technologie des microservices peut rendre l'échelle et le fonctionnement du système plus flexibles, permettant ainsi une évolutivité et une disponibilité plus élevées. Cependant, le problème de délai d’attente qui se produit lors des appels de microservices est également devenu un danger caché majeur pour la disponibilité du système. Les délais d'attente peuvent entraîner une dégradation des performances du client et même l'empêcher de fonctionner correctement. Cet article vise le problème du timeout et propose des méthodes d'optimisation pertinentes pour réduire le risque d'expiration des appels de microservices.
1.1 Malentendus
Lorsque nous rencontrons des problèmes de délai d'attente ou de lenteur d'exécution, nous pensons souvent qu'il y a un problème avec la partie utilisatrice.
Par exemple : l'accès aux interfaces Redis, DB, RPC devient lent ou expire. Trouvez la partie de confiance pour résoudre le problème dès que possible. Les commentaires de l'autre partie sont qu'il n'y a aucun problème de mon côté (serveur). S'il vous plaît vérifiez si votre côté (client) a un problème.
En fait, la dégradation des performances est un problème très complexe qui peut impliquer plusieurs aspects, notamment le serveur et le client. Par exemple, des problèmes tels que la qualité du code, les ressources matérielles et les conditions du réseau entraîneront une dégradation des performances, entraînant une réponse lente, un délai d'attente et d'autres problèmes. Par conséquent, nous devons analyser de manière approfondie le problème et découvrir les différents facteurs qui affectent les performances.
1.2 Objectif du partage
Cet article présentera en détail les problèmes tels que la lenteur d'exécution et les délais d'attente que nous rencontrons dans l'environnement de production, et proposera des méthodes d'optimisation pertinentes pour réduire le risque de ralentissement ou de délai d'attente et améliorer la stabilité du système en optimisant les performances à longue traîne.
2. Classification des temps morts
Il existe généralement deux types de délais d'attente courants :
a. Délai d'attente de connexion (ConnectTimeout) : signifie que le temps nécessaire pour établir une connexion réseau dépasse le temps d'attente défini.
B. Socket timeout (SocketTimeout) : signifie que pendant le processus de transmission des données, le temps pendant lequel le client attend que le serveur réponde dépasse le temps d'attente défini.
Comme le montre la figure ci-dessous, ① est le moment de préoccupation pour le délai d'expiration de la connexion, et ② est le moment de préoccupation pour le délai d'expiration du Socket. Le délai d'attente expliqué dans cet article est le délai d'attente du Socket.
Figure 1 Processus de demande du client
3. Analyse et optimisation des problèmes de délai d'attente
3.1 Définir un délai d'attente raisonnable
Définissez un délai d'expiration raisonnable en fonction de la situation réelle pour éviter un délai d'attente de l'interface causé par des paramètres de délai d'attente déraisonnables.
1) Analyse
Vérifiez si le délai d'attente défini par le client est raisonnable. Par exemple, si le P99.9 du serveur est appelé à 100 ms et que le timeout défini par le client est de 100 ms, 0,1% des requêtes expireront.
2) Plan d'optimisation
Lors de la définition du délai d'attente, nous devons prendre en compte de manière globale le délai du réseau, le temps de réponse du service, le GC, etc.
Prenons l'exemple du moteur de requête d'événement de ticket :
Interface principale : valeur minimale (P99.9*3, temps d'attente acceptable par l'utilisateur), le noyau affectera l'ordre et tentera de produire des résultats dans la plage acceptable par l'utilisateur.
Interface non principale : valeur minimale (P99,9*1,5, temps d'attente acceptable par l'utilisateur), l'interface non principale n'affecte pas les commandes et peu importe si elle n'est pas affichée.
3.2 Limitation de courant
Lorsque le système rencontre un trafic soudain, il utilise la limitation de courant pour contrôler la vitesse d'accès du trafic afin d'éviter les pannes ou les délais d'attente du système.
1) Analyse
Vérifiez s'il y a une augmentation soudaine du nombre de requêtes au point d'expiration. Par exemple, il y a une activité soudaine. À ce stade, l'application n'est pas développée à l'avance. Faire face à l'augmentation soudaine du trafic entraînera une charge de l'application être relativement élevé, ce qui entraîne des problèmes de délai d'attente.
2) Plan d'optimisation
Évaluez le trafic maximum que l'application actuelle peut transporter et configurez la limitation du trafic. La dimension peut être une machine unique + un cluster.
Limitation de courant sur une seule machine : évitez les collisions sur une seule machine face à une augmentation soudaine du trafic.
Limitation actuelle du cluster : fournissez des capacités de service maximales avec des ressources limitées pour garantir la stabilité du système sans panne ni panne.
3.3 Améliorer le taux de réussite du cache
L'amélioration du taux de réussite du cache peut améliorer la vitesse de réponse de l'interface et réduire le temps de réponse de l'interface, réduisant ainsi l'apparition de délais d'attente.
1) Analyse
Analysez le lien appelant, trouvez les zones lentes et optimisez-les pour améliorer la vitesse de réponse du serveur.
Comme le montre la figure ci-dessous, il est évident que le temps d'exécution du serveur dépasse de 200 ms le délai d'attente configuré par le client, ce qui entraîne un délai d'attente.
Figure 2 Le client appelle le lien de délai d'attente du serveur
Continuez à analyser le lien d'exécution du serveur et constatez qu'il est dû à des échecs de cache.
Figure 3 Lien manquant de cache
2) Plan d'optimisation
Pour les systèmes à haute concurrence, il est courant d’utiliser le cache pour améliorer les performances.
La figure ci-dessous montre l'architecture de cache précédente. Cette architecture de cache présente deux risques.
a. Le cache a expiré de manière fixe, ce qui entraînera la pénétration directe d'un grand nombre de pannes de clé à un certain moment dans la base de données.
b. Le mécanisme d'actualisation actif consiste à supprimer le cache et à écouter les messages du journal binaire de la base de données pour supprimer le cache. Si une grande quantité de données est actualisée, un grand nombre de clés deviendront invalides.
Figure 4 Expiration fixe + mode de chargement paresseux
En réponse aux risques ci-dessus, nous avons optimisé l'architecture du cache, en remplaçant l'expiration fixe par un renouvellement actif du cache et en écoutant activement les messages pour actualiser le cache, comme le montre la figure ci-dessous.
Figure 5 Comparaison de l'architecture avant et après la mise en cache
3) Effet
Le taux de réussite du cache est augmenté à plus de 98 % et les performances de l'interface (RT) sont améliorées de plus de 50 %.
Figure 6 Performances de traitement augmentées de 50 %
Cette solution d'optimisation de mise en cache a été publiée dans un article rédigé auparavant par notre équipe, "80 000 tickets vendus en 1 minute !" Il y a une introduction détaillée dans " La réflexion technique derrière la saisie de billets" . Les détails spécifiques ne seront pas développés ici. Les étudiants intéressés peuvent la lire par eux-mêmes.
3.4 Optimiser le pool de threads
Réduisez les threads déraisonnables et réduisez les délais d’attente causés par le changement de thread.
1) Analyse
a. Nombre de threads HTTP
Vérifiez d'abord si le nombre de threads HTTP côté serveur a augmenté de manière significative et si le trafic n'a pas augmenté. Assurez-vous que l'augmentation du nombre de threads HTTP n'est pas causée par l'augmentation du trafic. Comme le montrent les deux figures ci-dessous, lorsque le trafic est normal, le nombre de threads HTTP augmente, indiquant que la réponse du serveur est lente, et il peut être confirmé que le délai d'attente est provoqué par le serveur.
Figure 7 Le trafic du service est stable
Figure 8 Le nombre de threads HTTP augmente soudainement
B. Nombre total de threads
Vérifiez à nouveau si le nombre total de threads a augmenté (à l'exclusion du nombre de threads HTTP). Si tel est le cas, cela signifie que le nombre de threads a augmenté en raison de l'utilisation du multi-threading. À ce stade, vous devez vider les threads pour voir quels threads sont le plus utilisés.
2) Solutions
a. Gestion unifiée du pool de threads : paramètres de configuration dynamiques + capacités de surveillance
Un pool de threads unifié est encapsulé via des classes d'outils, fournissant des paramètres de configuration dynamiques et des capacités de surveillance du pool de threads.
Effet
Le pool de threads a des capacités de surveillance. La figure suivante montre la surveillance de la valeur minimale (nombre de threads principaux), de la valeur maximale (nombre maximum de threads) et du nombre de threads dans le pool de threads actuel. Vous pouvez vous y référer pour ajuster les paramètres du pool de threads.
Figure 9 Surveillance du niveau d’eau de la piscine Thread
b. Asynchrone à synchrone : n'utilisez pas le multi-threading s'il est inférieur à 10 ms
Dans les scénarios à forte concurrence, il y a trop de threads et le temps de planification des threads ne peut pas être garanti. Une tâche nécessite plusieurs tranches de temps CPU et le temps de planification suivant ne peut pas être garanti.
La figure suivante montre le temps d'exécution fastidieux d'un pool de threads. Grâce aux points enfouis, on constate que A s'exécute plus rapidement dans le pool de threads. La ligne moyenne et P95 sont tous deux inférieurs à 10 ms. Il n'est pas nécessaire d'utiliser le thread pool et passage à l’exécution synchrone.
Figure 10 Temps d'exécution avant optimisation
Effet
Les performances de l'interface ont été considérablement améliorées, avec la ligne moyenne réduite de 2,7 ms à 1,6 ms et le P99.9 réduit de 23,7 ms à 1,7 ms.
Dans le passé, le multithreading était utilisé et lorsque le volume des requêtes fluctuait, le nombre de threads augmentait davantage, ce qui faisait que le temps de planification des threads n'était pas garanti et que le P99.9 était très élevé.
Figure 11 Comparaison de la consommation de temps avant et après optimisation
De plus, on voit clairement que le nombre total de threads a été réduit en conséquence.
Figure 12 Le nombre total de threads diminue après un changement asynchrone vers la synchronisation
3.5 Optimiser le GC
Optimisez le GC, réduisez le temps de pause du GC et améliorez les performances de l'interface.
1) Analyse
Vérifiez d'abord s'il y a un Full GC au point d'expiration, puis vérifiez s'il y a des problèmes évidents dans le Yong GC.Comme le montre la figure ci-dessous, vous pouvez voir trois problèmes évidents.
Figure 13 Temps de Yong GC
Si le délai d'expiration (comme indiqué ci-dessous) peut correspondre au moment du problème GC, il peut être confirmé que le problème est causé par Yong GC.
Figure 14 Nombre de délais d'attente lorsque le client appelle le serveur
2) Solutions
A. Réglage des paramètres JVM universels
Vérifiez si les deux valeurs de -Xmx -Xms sont définies de la même manière. Si elles sont différentes, la JVM ajustera dynamiquement la taille du tas en fonction de la situation réelle pendant l'exécution. Cet ajustement entraîne souvent une surcharge de performances, et si le Le tas d'initialisation est petit, le nombre de GC sera relativement faible.
Effet
-Xmx3296m -Xms1977m est remplacé par -Xmx3296m -Xms3296m. L'effet est comme indiqué dans la figure ci-dessous. Il y a une diminution significative de la fréquence et du temps.
Figure 15 Effet après réglage des paramètres JVM universels
B. Réglage des paramètres du garbage collector G1
Contexte : Si les valeurs maximales et minimales de la nouvelle génération ne sont pas définies ou si une seule des valeurs maximales et minimales est définie, alors G1 occupera tout l'espace du tas selon les paramètres G1MaxNewSizePercent (la valeur par défaut est 60) et G1NewSizePercent (la valeur par défaut est 5). Le rapport est utilisé pour calculer les valeurs maximales et minimales, et l'équilibrage dynamique est utilisé pour allouer l'espace de nouvelle génération.
Lorsque la JVM vient de démarrer, l'allocation par défaut de l'espace de nouvelle génération est de 5 % du tas total. À mesure que le trafic augmente, la nouvelle génération peut facilement devenir pleine, ce qui entraîne Yong GC. La prochaine fois, plus d'espace de nouvelle génération sera réaffecté jusqu'à ce que les 5 % par défaut soient atteints. Expansion dynamique et valeurs initiales appropriées. Cette configuration est sujette à des Yong GC fréquents lors de la publication de trafic d'accès ou d'un afflux de trafic important.
Pour résoudre ce type de problème, la solution d'optimisation consiste à augmenter G1NewSizePercent et à augmenter la valeur initiale pour rendre le GC plus stable. Cette valeur doit être définie en fonction du scénario métier et se référer à la distribution de taille initiale d'Eden dans le journal GC. Si elle est trop grande, cela peut provoquer des problèmes de Full GC.
En prenant le moteur de requête comme exemple, selon l'analyse des journaux GC, la proportion de la taille de la nouvelle génération dans le tas est relativement stable après 35 %.Les paramètres définis sont :
-XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=35
Effet
L'effet après optimisation est comme indiqué ci-dessous. Vous pouvez voir qu'après optimisation, le nombre de GC est réduit de 27 fois/min à 11 fois/min et le temps de GC est réduit de 560 ms à 250 ms.
Figure 16 Effet après réglage des paramètres G1
3.6 Changer le thread asynchrone en programmation asynchrone NIO
NIO (IO non bloquant) peut réduire le nombre de threads, améliorer l'utilisation des threads, réduisant ainsi les délais d'attente causés par le changement de thread.
1) Analyse : indicateurs CPU
Analysez les indicateurs liés au processeur. Si l'utilisation du processeur est normale et que la charge du processeur est élevée, vous devez vous concentrer dessus (si l'utilisation du processeur est élevée, cela signifie que le processeur lui-même est très occupé, il est donc normal que le processeur La charge doit être élevée).
Avant l’analyse, introduisons quelques concepts :
a. Tranche de temps CPU
Le CPU divise le temps en plusieurs tranches de temps, et chaque tranche de temps est allouée à un thread. Lorsqu'une tranche de temps est épuisée, le CPU arrêtera l'exécution du thread en cours, effectuera un changement de contexte vers la tâche suivante, et ainsi de suite.
Cela permet d'exécuter plusieurs tâches simultanément en même temps, améliorant ainsi l'efficacité du système et la vitesse de réponse.
La figure ci-dessous simule le processus d'exécution d'un processeur monocœur. Il convient de noter que le changement de contexte nécessite une surcharge, mais le temps réel requis pour un changement de contexte est très court (généralement au niveau de la microseconde).
Figure 17 Processus du thread d'exécution du processeur
B. Utilisation du processeur
Pour le comprendre à partir de la dimension tranche de temps, on suppose que chaque tranche de temps est exactement utilisée.
c. Charge du processeur
D'après l'analyse conceptuelle ci-dessus, si l'utilisation du processeur est normale mais que la charge du processeur est élevée, cela signifie que le processeur a beaucoup de tranches de temps d'inactivité et beaucoup de threads en attente, et que très peu de tranches de temps sont utilisées. , pour réduire la charge CPU, vous devez réduire le nombre de threads en attente.
2 ) Analyse : cas concrets
Nous avons rencontré à plusieurs reprises en production auparavant que la charge du processeur et l'utilisation élevée du processeur étaient normales. Le code n'a pas changé avant et après l'apparition du problème, le changement le plus évident étant l'augmentation du trafic. Après avoir vérifié le code, nous avons constaté que le pool de threads est utilisé pour appeler l'interface simultanément.La méthode d'appel est la suivante.
Figure 18 Modèle d'exécution du pool de threads
Cette méthode ne pose aucun problème lorsque le trafic est faible : à mesure que le trafic augmente, le nombre de threads requis doublera.
Par exemple : une requête pour A nécessite d'appeler 3 interfaces de BCD, alors le nombre de threads requis pour 100 simultanéités est de 100 + 3*100=400 (les 100 premiers sont le thread principal correspondant à A, et les 3*100 suivants sont les 100 requis par le fil BCD).
3) Solutions
L'appel simultané du pool de threads est remplacé par un appel asynchrone NIO, comme indiqué dans la figure ci-dessous.
Par rapport à avant, 100 accès simultanés nécessitent 100 threads (les threads de NIO lui-même ne sont pas pris en compte ici, c'est global, et il s'agit d'un nombre de threads relativement fixe et petit).
Figure 19 Modèle d'exécution d'appel asynchrone NIO
4) Effet
Le problème de timeout ne s'est plus produit et la charge du processeur a diminué de 50 % en moyenne. Avant, la charge dépassait souvent 2 (le nombre de cœurs du processeur était de 2). Après la transformation, la charge est tombée à environ 0,5.
Figure 20 Effet après optimisation de la charge CPU
3.7 Préchauffage pendant la phase de démarrage
Le préchauffage pendant la phase de démarrage peut établir des liaisons à l'avance, réduisant ainsi l'établissement de liaisons lors de la réception du trafic, réduisant ainsi l'apparition de délais d'attente.
1) Analyse
Une fois l'application extraite, un grand nombre de délais d'attente se produisent et une utilisation élevée du processeur par charge CPU est normale, ce qui indique qu'il y a de nombreux threads en attente. Cela signifie qu'il y a un grand nombre de demandes en attente d'être traitées après l'exécution de l'application. arrivé.
Nous avons déjà rencontré en production que nous attendions que Redis établisse un lien. Le processus de création de liens est synchrone. L'afflux instantané de requêtes juste après l'arrivée de l'application entraînera un grand nombre de requêtes à attendre que Redis termine le processus. établissement de liens.
2) Solutions
Pendant la phase de démarrage, préparez et établissez des liens à l'avance, ou configurez le nombre minimum de connexions inactives pour Redis. D'autres préparations de ressources peuvent également être complétées par un préchauffage pendant la phase de démarrage, comme les liens DB, le chargement du cache local, etc.
3.8 Optimiser le JIT
La compilation JIT (Just-In-Time) peut améliorer l'efficacité d'exécution du programme. Le trafic d'accès en niveaux de gris compile le bytecode en code machine local pour éviter tout impact sur les performances de l'interface.
1) Introduction au JIT
JIT est l'abréviation de Just-In-Time, qui signifie compilation juste à temps. JIT est une technologie qui compile le bytecode en code machine local lorsque le programme est en cours d'exécution, ce qui peut améliorer l'efficacité d'exécution du programme.
En Java, un programme est d'abord compilé en bytecode puis interprété et exécuté par la JVM. Cependant, l'efficacité de l'exécution interprétée est faible car le bytecode doit être interprété une fois pour chaque exécution. Afin d'améliorer l'efficacité d'exécution du programme, la technologie JIT a été introduite dans Java. JIT compilera les blocs de code fréquemment exécutés en code machine local lorsque le programme est en cours d'exécution, puis exécutera le code machine, ce qui peut considérablement améliorer l'efficacité d'exécution du programme.
2) Analyse
La technologie JIT peut optimiser dynamiquement le code en fonction des conditions d'exécution réelles du programme, améliorant ainsi les performances du programme. Cependant, le processus de compilation JIT prend un certain temps, de sorte que certains goulots d'étranglement en termes de performances peuvent survenir lors du premier démarrage du programme.
Comme le montre la figure ci-dessous, le temps JIT est très long après l'extraction de l'application. Il peut être confirmé que JIT provoque le délai d'attente.
Figure 21 Temps d'exécution JIT
3) Solutions
Une meilleure solution pour optimiser JIT consiste à activer le préchauffage du service (la fonction de préchauffage est prise en charge par le framework Ctrip RPC).
Le principe est qu'une fois pullinée, l'application n'accède pas immédiatement à 100% du trafic, mais augmente progressivement le trafic au fil du temps, pour finalement recevoir 100% du trafic, ce qui permettra de compiler une petite partie du trafic. le code chaud à l'avance.
4) Effet
Après avoir activé le préchauffage du service, comme le montre la figure ci-dessous, le trafic des applications augmente progressivement et vous pouvez voir que le temps de réponse diminue de plus en plus avec le temps, ce qui permet d'obtenir l'effet de préchauffage.
Figure 22 Volume de requêtes et temps de réponse une fois le service activé
3.9 Changer la machine hôte
Lorsque la charge de l'hôte est trop élevée, vous pouvez envisager de remplacer l'hôte pour éviter qu'une charge excessive de l'hôte n'affecte la charge du conteneur.
1) Analyse
A. Indicateur de limitation du processeur
Jetez un œil à l’indicateur de limitation du processeur de l’application. La limitation du processeur entraînera la mise en veille du processeur et provoquera des pauses dans le service. Si l'utilisation du processeur est normale mais qu'une limitation du processeur se produit, cela est principalement dû à des problèmes d'hôte.
Figure 23 Situation de limitation du processeur
B. Indicateurs d'hôte
Vérifiez si l'utilisation du processeur de l'hôte, la charge du processeur, le disque, les E/S et d'autres indicateurs sont normaux. Comme le montre la figure ci-dessous, si la charge du processeur est supérieure à 1 après un certain temps, cela signifie que la charge de l'hôte est lourde.
Figure 24 Surveillance de la charge du processeur hôte
2) Solutions
Redémarrez la machine et changez d'hôte.
3.10 Optimiser le réseau
Dépannez les lignes réseau instables pour assurer la stabilité du réseau.
1) Analyse
L'objectif du réseau est d'examiner l'indicateur TCPLostRetransmit (paquets de retransmission perdus). Par exemple, dans l'image ci-dessous, si l'indicateur à un certain point est anormal, mais que d'autres indicateurs à ce stade sont normaux, on peut initialement soupçonner qu'il s'agit d'un problème de réseau. La confirmation finale doit être confirmée par le équipe liée au réseau.
Figure 25 Indicateur TCPLostRetransmit
2) Solutions
Trouvez l’équipe liée au réseau pour dépanner et optimiser.
4. Résumé
En examinant le texte intégral, il explique principalement comment analyser, localiser et optimiser les problèmes de délai d'attente, et résume 10 méthodes d'optimisation courantes, du plus simple au plus complexe. Ces méthodes ne résolvent pas nécessairement le problème de délai d'attente dans d'autres scénarios commerciaux différents. Elles doivent être vérifiées en fonction de votre scénario commercial réel.
Les méthodes résumées dans cet article sont toutes des situations réelles que nous avons rencontrées en production et résumées par une pratique continue. J'espère que ces contenus pourront apporter certains gains aux étudiants qui liront cet article.
4.1 Considérations d'optimisation
Les paramètres de délai d'attente et le réglage du GC doivent être optimisés en fonction de vos propres scénarios commerciaux.
Le coût et la complexité de la transformation de la programmation asynchrone de NIO sont élevés. Nous explorons également des moyens plus simples, tels que les threads virtuels introduits dans JDK19 (similaires aux coroutines Go), qui peuvent utiliser la programmation synchrone pour obtenir des effets asynchrones.
[Lecture recommandée]
Compte public « Ctrip Technology »
Partager, communiquer, grandir