Quelques outils pour localiser un problème de performances
Cet article va passer en revue certains outils, parfois méconnus, utilisés par l'équipe de Pilot Systems pour localiser un problème de performances sur une application, que ce soit une application réalisée en interne, de la TMA ou un audit. Il expliquera aussi comment interpréter les résultats. Il n'est bien sûr pas exhaustif, et ne remplacera jamais une bonne connaissance du fonctionnement des technologies utilisées.
IO et mémoire virtuelle
Rappels théoriques
Dans un système d'exploitation moderne, la mémoire physique disponible peut être considérée comme un cache du disque. Toute opération sur le disque (lecture ou écriture) est conservée en mémoire, jusqu'à ce que la mémoire soit pleine. Alors une des trois options suivantes est choisie :
- Une page (l'unité de mémoire manipulée par le système, 4Ko en général) du cache disque est libérée (après avoir été écrite sur le disque, s'il s'agissait d'une écriture).
- Une page d'un fichier mmappé est libérée (par exemple, une page d'un exécutable ou d'une bibliothèque d'un programme s'exécutant actuellement).
- Une page de données d'un programme est mise sur le swap et libérée.
Le système choisit en fonction de différents paramètres (fréquence d'utilisation de la page, s'il doit faire une écrire ou non, ...).
free
La commande free donne une sortie du type :
total used free shared buffers cached
Mem: 3986840 3104616 882224 0 547160 1415768
-/+ buffers/cache: 1141688 2845152
Swap: 1951860 35960 1915900
Comment interpréter cette sortie ?
La première ligne indique la mémoire physique totale, utilisée et libre. La mémoire utilisée est utilisée par n'importe quelle source : les binaires des programmes ou bibliothèques, la mémoire interne utilisée par le noyau, les données des programmes, le cache disque.
La deuxième ligne indique la mémoire hors cache disque et tampons (utilisés pour le réseau par exemple).
La troisième ligne indique le swap total et utilisé.
Jusque là, rien de bien sorcier. Quelques précisions, qui peuvent ne pas être évidentes au premier abord, tout de même :
- Il est normal que le swap commence à être utilisé, lorsque la mémoire totale est pleine (première ligne), mais même lorsqu'une partie importante est utilisée pour les buffers et les caches. En effet, si une zone mémoire de données d'un programme n'est quasiment jamais utilisée, alors qu'une zone disque est souvent utilisée, il est plus performant de mettre la zone mémoire dans le swap, et la zone disque en cache.
- Il est normal que le swap puisse être utilisé alors qu'il y a de la mémoire y compris sur la première ligne (comme sur l'exemple donné). Cela signifie que pendant un moment la mémoire a été utilisée, et donc le système a utilisé le swap, puis qu'elle a été libérée. Tant que ce qui a été mis dans le swap n'est pas utilisé de nouveau, le système le laisse dans le swap : inutile de relire des données qui pour l'instant ne servent pas.
iostat
La commande iostat donne des informations, de manière périodique, sur l'activité disque. Elle s'utilise par exemple avec iostat -x 5 qui donnera toutes les 5 secondes une information du type :
Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sda1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 sda2 0.00 476.05 0.00 110.78 0.00 4779.24 43.14 9.99 96.76 2.80 31.06
Ainsi on peut voir sur cet exemple que sda1 n'est pas utilisé (il s'agit du swap), mais que sda2 subit des écritures de manière assez importantes, mais restant tout de même raisonnable (31% d'utilisation de la bande passante du disque).
Si jamais le taux d'utilisation dépasse les 90% alors la machine est saturée par les IO. C'est en général l'une des premières choses à regarder quand un serveur commence à ralentir : est-ce que les IOs sont saturées ?
vmstat
La commande vmstat elle donne un affichage du type :
procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 1 1 50820 393512 554120 2874504 0 1 556 1140 148 19 26 4 51 18
Elle permet de voir l'évolution des zones mémoires, ainsi que le nombre de pages lues depuis le swap (si), écrites sur le swap (so), autres lectures depuis le disque (bi) et autres écritures sur le disque (bo).
Solutions
Si on constate une activité importante sur le disque sur un serveur qui a des problèmes de performances, il y a un grand nombre de causes possibles, qu'il faut diagnostiquer.
Si les opérations sont surtout des lectures (ou des opérations sur le swap), il peut être très intéressant d'augmenter la quantité de RAM sur la machine. Dans le cas d'une base de données, il y a une différence très importante de performances lorsque l'ensemble de la base peut tenir en RAM. Dans ce cas, il peut même être intéressant de la précharger dans le cache disque au boot, avec une commande du type :
tar cf /dev/null /var/lib/postgresql
Si les opérations sont surtout des écritures, il faut chercher si ces écritures sont normales, ou si elles peuvent être optimisées. Par exemple, une requête SQL non optimisée peut forcer la base de données à utiliser des tables temporaires sur le disque, ou des fichiers temporaires pour faire du tri. Les commandes SQL explain ou explain analyze peuvent permettre d'avoir des détails sur ce genre d'opérations. Il est aussi possible de modifier les paramètres de la base de données, pour modifier le seuil au-delà duquel les tris seront effectués sur le disque et non en mémoire.
Utilisation CPU
uptime
La première commande à exécuter quand un serveur semble ramer est la commande uptime, qui va donner le load average. Il s'agit de la moyenne du nombre de processus en attente de ressources (accès au CPU ou au disque) pendant une période de temps (trois valeurs, pour 1 minute, 5 minutes, 15 minutes).
Deux informations sont importantes :
- Est-ce que le load est élevé, comparé au nombre de coeurs de la machine ? Tant que le load est inférieur au nombre de coeurs de la machine, sous réserve que les IO ne soient pas saturées, tout va bien. Par contre, s'il est supérieur, c'est que plus de ressources sont demandées qu'il n'y en a de disponible.
- Est-ce que les trois valeurs sont très différentes ? Si elles sont proches, c'est que la charge est continue, sur 15 minutes. Si elles varient beaucoup, c'est que la machine subit (ou a subit, suivant le sens de la variation) une charge élevée de manière ponctuelle.
top
La seconde commande pour analyser l'utilisation CPU est la commande 'top'. Elle indique la liste des processus, classés par l'utilisation CPU qu'ils font, ainsi que l'état total des différents coeurs.
L'état total des CPUs est réparti entre :
- us : userspace : le temps d'exécution des programmes eux-mêmes ;
- sy : system : le temps d'exécution du noyau (à la demande des programmes, la plupart du temps) ;
- ni : nice : le temps passé à exécuter des programmes de faible priorité ;
- id : idle : le temps passé à ne rien faire ;
- wa : io wait : le temps passé à attendre qu'une opération d'IO (en général lecture ou écriture sur le disque) se termine ;
- hi / si : interrupts : le temps passé à traiter les interruptions (devrait être négligeable) ;
- st : steal : sur une machine virtuelle uniquement, le temps "volé" pour être donné à une autre machine virtuelle.
Note : attention à la commande top, elle est modérément gourmande en ressources, et peut aggraver la situation sur un serveur déjà très chargé.
Multicoeurs et threads
Un point important est à considérer pour les deux commandes précédentes : le traitement du multicoeur (ou du multicpu, ce qui revient au même).
Lorsque top liste les temps globaux, il s'agit d'une moyenne sur tous les CPUs : ainsi, si on a 2 coeurs, et que top indique 50% de userspace et 50% de idle, il s'agit probablement d'un coeur fonctionnant à fond, tandis qu'un autre coeur n'est pas utilisé.
En effet, un même programme ne peut pas s'exécuter sur deux CPU à la fois, à l'exception des programmes multithreadés. Et encore, pour ceux-là, il faut qu'il n'y ait pas de lock, comme le Global Interpreter Lock de Python (donc, un programme Python, même multithreadé, ne s'exécutera que sur un seul coeur).
Ajouter des coeurs ne sert à rien si les services ne sont pas suffisamment parallélisés.
Appels systèmes et réseau
Si le système semble au repos, mais que l'application met du temps à répondre, il faut se demander si l'application n'est pas bloquée par quelque chose d'externe, en général un service réseau qui mettrait du temps à répondre.
Pour cela deux commandes sont indispensables :
- netstat et en particulier sa forme netstat -anp | grep ESTABLISHED qui indique toutes les connexions réseaux ouvertes.
- strace qui permet de suivre les appels systèmes d'un programme en cours d'exécution. Un strace -p <PID> permet de voir ce que fait le programme, et s'il est bloqué sur un read, recv ou select, il faut alors regarder si le fichier en question n'est pas une socket.
Deadlock debugguer
Un autre outil extrêmement pratique pour analyser les raisons de la lenteur d'un Zope (ici, il s'agit d'un produit Zope, mais le concept peut être reproduit sur d'autres frameworks Web) : le DeadlockDebugger (http://plone.org/products/deadlockdebugger/ ).
Ce produit permet d'avoir, en temps réel, l'état (la pile d'exécution) de tous les threads d'un Zope, et donc de voir dans quelle partie du code il est "coincé".
Attention cependant : en raison du mode de fonctionnement de l'ordonnancement des threads Python, le DeadlockDebugguer prendra quasiment toujours la main lors d'un appel système. Les dernières lignes de la pile seront très souvent un lseek ou un read, mais ce n'est pas pourtant que ce sont réellement ces appels qui prennent le plus de temps. Il vaut mieux remonter quelques niveau de plus de la pile, et voir à un niveau applicatif quel est le code concerné (recherche catalogue, rendu d'un template, ...).
Conclusion
Cet article liste quelques outils, la manière d'interpréter leurs résultats, et des actions à mener pour résoudre un problème du type "mon service rame". Cependant, il faut bien se souvenir que rien ne remplacera un travail d'investigation, une analyse du code et des requêtes SQL s'il y en a, ...

