La lenteur n’est pas forcément là où on croit

Récemment nous avons eu l’occasion de faire un audit de performance sur un site internet e-commerce.
Coeur développé en PHP sous le framework Zend, base de données SQL Serveur et serveur de fichier Synology. Un Webservice développé sous Webdev (Windev version site internet, du meme éditeur) répond à PHP sur les pages concernant les comptes clients : solde, commandes, avoirs, factures.
L’ensemble est hébergé chez un hébergeur professionnel sur des baies ESX mutualisées. Rien n’est dédié : la sortie Internet, les baies SAN, les hôtes ESX, seules les VM sont intégralement sous contrôle de l’éditeur du site Internet.
Site internet proposant un catalogue, que l’on consulte et dont on choisit les prestations : un envoi d’un PDF et quelques jours plus tard la commande arrive.
Problème : c’est globalement Lent. Les gens se plaignent de temps de réponse de l’ordre de 10 secondes.
Maintenant que vous devenez vous aussi expert de la performance au fur et à mesure de la lecture de ces articles de blog, avez vous une idée de ce qu’il se passe ?
Typiquement, la base de données est surement un peu chargée (elle récupère le Webservice / L’ERP ET le code PHP).
Le réseau n’est pas trop en cause, la charge du lien de l’hébergeur est faible. Le réseau virtuel est rarement en cause, sauf en cas de gros pépin (TCP Offload Engine) mais le symptôme serait plus douloureux.

Une base de données qui bagotte … un peu

Pour un affichage de la page d’accueil, voici la décomposition des appels :  on retrouve le serveur frontal qui accède à la base de données :
 schema_lenteur_bdd_php
Et on remarque : 2,3 secondes pour accéder  à la base de données ?
Tout ça en une seule requête :
 lenteurs_sql_top_requetes
Comment ça ? Une seule requête qui prend à elle seule 2,3 secondes ?
Une procédure stockée (pour être précis) qui en réalité calcule un solde à partir de dizaines de milliers de lignes (et donc de beaux Full Scan de tables).
Oui, c’est vrai, une optimisation de ces requêtes serait judicieuse.
Et côté réseau ? Rien, nada, que dalle : le serveur frontal accède à la base en moins d’1 ms.
Notre problématique SQL était donc bien préssentie. Mais les utilisateurs se plaignent de lenteurs de l’ordre de 10 secondes, il nous manque donc presque 7 secondes à expliquer ?

Une baie SAN qui pose problème… des fois

 charge_vs_lenteurs_page_accueil
Un étrange constat nous intrigue : mi juillet, le site se met à répondre très lentement régulièrement. Mais ce n’est pas lié à la charge !! Vers 12h, les temps de réponses unitaires dépassent les deux secondes (unitaires, une page en contient plusieurs).
Pourtant, le nombre d’appels concurrents chute de façon vertigineuse. Il y a moins d’appels, mais ils sont tous lents. Donc ce n’est pas un pic de charge, c’est un effet externe non maitrisé qui cause la lenteur.  
Côté navigateur, c’est proche de la vingtaine de secondes :

 lenteur_homepage_analyse

Qui sont les responsables ?
Etonnant, deux méthodes reviennent systématiquement :
– fopen
– Zend_Db_Statement_Pdc::_execute (appel en base de données)
 top_methodes_php_lentes
Il y a donc deux sources : les lectures de fichier sur le NAS, et les accès en base de données. Etonnant !
La réponse est simple : au même instant, un autre client de l’hébergeur déplaçait des fichiers sur le réseau, et saturait la baie SAN. Eh oui, c’est le risque du mutualisé.
Résultat, les disques sont saturés et les ouvertures de fichier sont ralenties (je ne vous parle même pas des écritures de fichiers) et la base de données est très sensible au Full Scan.
Oui, mais, ça ne s’est produit qu’une ou deux fois en trois mois.
Et le site est globalement toujours lent : 10 secondes pour la page d’accueil.
Il nous manque des secondes !

Un Webservice lié à l’ERP systématiquement lent

 ralentissement_webservice
Ah ça oui, quand il est appelé et que tout va mal, il est vraiment très lent. Parfois 20 secondes pour répondre aux questions.
Cette capture d’écran est liée à  un cas isolé (lié aux problèmes SAN expliqués ci dessus).
Les analyses nous montreront qu’en moyenne et uniquement pour les pages où il intervient (il n’est pas appelé partout), il est souvent responsable d’une à deux secondes de lenteur.
Effectivement, comme on peut l’imaginer, un ralentissement SAN impacte le Webservice autant qu’il impacte la base de données, et les ouvertures de Fichiers.
2,3 secondes en bases, 2 secondes sur ce Webservice. On a déjà 4,3 s !
Mais il en manque ?
Le PHP répond entre 300 et 800 ms finalement. Il nous manque donc presque 5 secondes de temps de réponse.

Attention au développement Front-end

Nous avons souvent tendance à l’oublier lorsqu’on développe : le serveur est local, nous développons sur une machine bien pourvue, un réseau puissant, et utilisons (presque) tous Google Chrome.
Mais on oublie souvent de se mettre dans la peau d’un utilisateur final.
1) il n’est pas forcément sur un ordinateur très optimisé, nettoyé,
2) il n’a pas forcément la fibre optique,
3) il n’a pas passé plusieurs mois à développer sur le site et n’a pas son cache navigateur rempli de toutes les versions possibles des CSS et Javascripts du site.
4) Ni des images d’ailleurs
Le chargement d’une page non améliorée pour le Front peut parfois s’élever à des valeurs impressionnantes :
Le graphique ci-dessous décompose la chaine de connexion d’un utilisateur qui affiche la page d’accueil depuis un Firefox 39 sur son PC.
La lecture du graphique se fait de gauche à droite :
1) 6,2 secondes passées dans le navigateur
2) 3,8 secondes passées sur le réseau et sur le serveur, dont :
     a. 669 ms passées en PHP
     b. 3,1 secondes passées en base de données
et nous n’avons pas discuté avec le webservice.
 chaine_complete_chargement_page
6,2 secondes passées dans le navigateur ? Comment est-ce possible ?
L’ordinateur est surchargé ?
Pour comprendre les mécanismes de ralentissement d’un navigateur, je vous propose de travailler le Critical Rendering Path !
 critical_rendering_path
Ce graphique indique la façon dont le navigateur construit sa page :
1) Le HTML est parsé par le navigateur, il est d’ailleurs la première réponse qu’il reçoit du serveur (au sens applicatif)
2) Au fur et à mesure de leur découverte, les fichiers CSS sont appelés par le navigateur. Typiquement, le pipeling du navigateur n’étant pas toujours activé, on peut estimer que le navigateur va ouvrir jusqu’à 6 connexions avec le serveur et va maintenir les connexions ouvertes (HTTP 1.1, Persistent and Keepalives connections).
3) Ce n’est qu’une fois l’intégralité du CSS parsé et analysé (et la construction du CSSOM, l’objet mémoire et ses dépendances représentant l’arbre CSS) que celui ci est appliqué au DOM (HTML analysé et parsé et sa représentation objet mémoire). Enfin, et finalement, le navigateur peut lire le Javascript et le travailler :
il  existe du Javascript non-bloquant, mais dès qu’il s’agit d’aller lire une  balise HTML (GetElementById ou $(‘#id’) pour les jQueristes) le Javascript est mis de côté jusqu’à ce que le HTML et le CSS soient fonctionnels. Logiques, on n’applique la peinture que lorsque les murs sont construits, pas avant.  Et on y fait référence, que lorsqu’ils sont présents.
Le navigateur va donc s’arrêter régulièrement pendant le chargement de la page, pour récupérer des fichiers :
critical_rendering_path_2
Dans cet exemple, on voit bien qu’entre T0 et T1, le navigateur est indiqué comme « IDLE »  : un GET d’un CSS est considéré comme RENDER BLOCKING, et d’un JS, comme PARSER Blocking.
 critical_rendering_path_4
Le résultat de tout cela est une page qui se charge en 6,2 secondes si :
– le css et le javascript sont mélangés dans le header,
– il y a beaucoup de fichiers (40 CSS et 39 Javascript dans notre cas)
– Le CSS est lourd et les logiques sont distinctes selon les plugins. Les développeurs de ces plugins utilisent leurs conventions de nommage,  certains travaillent même sur des préprocesseurs CSS (Less, Sass) qui font des sélecteurs à rallonge (  body > div#wrapper > header > div.menu > div#top > span#first-item.a#link ). Tandis que d’autres préfèrent le Javascript en tant que langage de style. Je ne parle même pas des effets graphiques codés en Javascripts Alors que CSS3 le fait très bien.
Et dernier constat, le site chargeait à la fois Underscore.js, jQuery et jQuery UI. Trois mastodontes pour un seul site.
Conclusion :
– Utiliser des plugins externes c’est se soumettre à leur bon développement,
– On mélange ici les libraires Javascript, lourdes et consommatrices en mémoire
– Beaucoup de fichiers Javascript et CSS, le HTML est sans cesse arrêté pendant son parsing.

Que devrait-on faire pour améliorer la vitesse du site ?

1° Il y a effectivement des améliorations à faire en SQL, et il faut éviter de se soumettre aux pics de lenteurs liés à la baie SAN !
2 ° Mais travailler le Critical Rendering Path. et Utiliser les Chrome Dev Tools.
3° Et ne pas regarder que la moitié du chemin parcouru par l’utilisateur lors du chargement des pages !