Peut-on utiliser WordPress pour mettre en place un site avec des fonctionnalités avancées et un fort traffic ? Soyons honnête, WordPress est un CMS très répandu et a fait ses preuves dans le domaine des blogs, mais qui n'a jamais douté de ses capacités en termes de performance et de scalabilité ?

En tant que développeur, puis expert technique et maintenant directeur technique, j'ai eu l'occasion à plusieurs reprises d'entendre des discours du type "tu plaisantes ? on ne va pas utiliser WordPress, ça ne tiendra jamais la charge !" ou bien encore "Wordpress c'est bien pour faire un blog c'est tout", "Wordpress c'est sale !", blablabla…

Il y a les fervents défenseurs de WordPress, ceux qui ne jurent que par ce CMS et qui se spécialisent, il y a ceux qui ne veulent pas en entendre parler, et puis ceux qui, comme moi, lui donne une chance (croyez moi, WordPress n'en a pas besoin, il a déjà largement fait ses preuves comme nous le verrons plus tard). Bref, dans ce post je vais vous expliquer pourquoi WordPress fait parti des solutions à envisager pour mettre en place un gros projet au même titre que Joomla, Drupal ou encore Typo3 (et oui ça existe encore).

Part de marché

W3Techs

Worpress est utilisé par plus de 23% des sites web, il représente 61% de la part de marché des CMS. Ces chiffres sont très évocateurs et indiquent l'importance de ce CMS.

Une petite recherche dans Google Trends (de janvier 2013 à décembre 2014, basée sur les recherches de mots clés) permet de sortir les graphiques suivants :

Google Trends France Google Trends monde
France Monde

Ces graphiques illustrent très bien l'intérêt porté à WordPress (courbe bleue). Les chiffres parlent d'eux mêmes : WordPress est le CMS le plus répandu au monde.

Perfomance

Gestion du cache

Wordpress dispose de deux systèmes de cache.

Tout d'abord : le cache persistant grâce à l'API Transient qui permet de mettre des données en cache au niveau de la base de données ou au niveau de memcached par exemple. Le fonctionnement est le suivant: la fonction set_transient(…) permet de mémoriser les données sous forme de tableau ou d'objet, la fonction get_transient(…) permet de récupérer la valeur si elle est présente et non expirée. La méthode delete_transient permet, vous l'aurez deviné, de supprimer une valeur du cache. get_site_transient(), set_site_transient() et delete_site_transient() permettent de gérer le cache dans le cas du multisite. Vous trouverez une très bonne présentation de l'API ici : http://scotch.io/tutorials/a-guide-to-transients-in-wordpress.

Ensuite : le cache non persistant. Ce type de cahe est valable généralement au niveau du cycle de vie de la page et est très utilisé pour les requêtes par exemple. La méthode wp_cache_get(…) permet de vérifier la présence d'un élément dans le cache, la méthode wp_cache_set(…) ajoute un élément dans le cache, la méthode wp_cache_add(…), quant à elle ajoute un élément dans le cache uniquement s'il n'y est pas déjà (contrairement à wp_cache_set qui l'ajoute systématiquement).

Vous trouverez une présentation des caches très intéressante ici : http://www.smashingmagazine.com/2012/06/26/diy-caching-methods-wordpress/.

Plugins

Il exisiste plusieurs module de gestion de cache, voici les principaux:

Ces plugins sont administrables en backoffice et modifient généralement les fichiers Nginx, Apache, Htaccess afin d'optimiser les configurations serveurs.

Configuration serveur

La performance ne dépend pas uniquement du CMS ou du Framework, il est possible d'intervenir à différents niveaux :

  • load balancing pour la répartition de la charge
  • cache des ressources statiques (Varnish, Nginx, Apache mod_cache etc…)
  • mise en cache dans la RAM (Redis, Memcached)
  • optimisation des ressources : RAM, SSD, CPU, Bande passante

Bonnes pratiques

Installer WordPress ou un autre CMS ne garantit pas un temps de réponse idéal, un poids de page minimum, un nombre de requêtes optimum… C'est la qualité du code qui permet de tendre vers une performance optimale. Voici quelques pistes d'amélioration simple à mettre en place :

  • minification / concaténation des fichiers javascript
  • minification / concaténation des fichiers CSS
  • optimisation des images
  • optimisation des requêtes SQL
  • utilisation de sprites CSS

Vous trouverez quelques unes de ces bonne pratiques dans ce support http://supports.severin-bruhat.com/web-performance

Modularité / évolutibilité

Il existe une multitude de plugins pour wordpress (plus de 35000) disponibles ici : https://wordpress.org/plugins/. Quelle que soit la fonctionnalité recherchée, il y a de fortes chances pour qu'un plugin existe déjà et que vous n'ayez pas besoin de la développer. Attention cependant, comme pour les autres CMS, qui dit "plugin" dit "code non maitrisé" et potentiellement faille de sécurité. En effet, des failles sont découvertes très régulièrement dans des plugins. Ce compte twitter alerte rapidement dès qu'une faille est trouvée : https://twitter.com/SecuPress. Pensez donc bien à mettre à jour régulièrement vos plugins et votre installation WordPress.

Si vous le souhaitez, vous pouvez développer vous mêmes vos plugins et "maitriser" d'avantage le code source. Pour ce faire, je vous laisse jeter un oeil à la documentation officielle. Dès lors que maitrisez le code, vous pouvez tout faire, WordPress n'a pas plus de limite qu'un autre CMS. J'ai l'habitude de dire que de toute façon avec du temps et de l'argent, quel que soit le CMS, on peut toujours faire ce que le client veut. Alors, si vous êtes à l'aise, codez (RTFM) !

Quelques exemples

Jusque ici, le blabla que vous avez pu lire ne vous a pas convaincu ? OK, alors maintenant place aux exemples :

Vous trouverez d'autres exemples de sites tournant sous WordPress ici : http://www.wpbeginner.com/showcase/40-most-notable-big-name-brands-that-are-using-wordpress/.

Conclusion

Alors, pensez-vous toujours que WordPress convient uniquement aux petits blogs tels que le mien ? Sans trop m'avancer, je pense que les sites de Coca, des Rolling Stones et de Bata reçoivent beaucoup plus de visiteurs et servent beaucoup plus de contenus que mon modeste blog 😉

N'hésitez pas à laisser un commentaire pour compléter ou contredire mon article.

Je suis récemment tombé sur cet article http://www.sitepoint.com/mysql-mistakes-php-developers listant quelques bonnes pratiques concernant MySQL. J'ai trouvé ce post intéressant et souhaite donc le partager avec vous (l'approche est un peu différente de l'article original).

Utiliser InnoDB plutôt que MyISAM

Par défaut, lorsque vous créez une base de données MySQL, celle-ci est au format MyISAM, c'est dommage ! En effet, MyISAM ne gère ni les clés étrangères, ni les transactions. MyISAM lock aussi complètement les tables en cas d'insertion ou de mis à jour, ce qui est "sécurisant" mais coûteux en terme de performance.

Préférez donc InnoDB qui présente les avantages suivants :

  • modèle ACID : atomicité, cohérence, isolation et durabilité (plus performant que le système de lock de MyISAM)
  • supporte les transactions
  • support les clé étrangères
  • permet les sauvegardes à chaud

C'est bien connu : pas d'avantages sans inconvénients ! Ainsi, InnoDB est plus complexe à administrer que MyISAM. Vous trouverez la procédure permettant d'activer le mode InnoDB par défaut et de migrer vos table de MySIAM vers InnoDB ici.

Utiliser l'extension PHP Mysqli

Depuis la version 4.1+ de mysql, il est préférable d'utiliser mysqli (MySQL Improved) pour communiquer avec la base de données. Les principaux avantages de cette extension sont les suivants :

  • interface (optionnelle) orientée objet
  • utilisation de requêtes préparées  : amélioration de la sécurité et des performances
  • support des transactions

Vous trouverez la documentation officielle à cette adresse : http://www.php.net/manual/fr/book.mysqli.php.

Sécuriser les inputs

Il est nécessaire de valider toutes les variables reçues côté PHP avant de les insérer dans une requête afin de limiter les risques d'injection SQL du type :

$username = $_POST["name"];
$password = $_POST["password"];
$sql = "SELECT userid FROM usertable WHERE username='$username' AND password='$password';";
// run query...

Imaginons que $_POST[“name”] ait pour valeur : "admin'; –" la requête envoyée au serveur MySql sera la suivante :

SELECT userid FROM usertable WHERE username='admin';

ET voilà, l'utilisateur sera connecté en admin dans le cas d'une requête utilisée pour une authentification. Il faut savoir que généralement, les frameworks et CMS gèrent automatiquement ce type d'injection en échappant certains caractères par exemple. Si vous devez faire les tests manuellement, voici 2 petites actuces :

  • appliquer la méthode mysql_real_escape_string() au chaines de caractères
  • vérifier le entiers / numériques grâce à la méthode intval()

Utiliser UTF-8

Pensez à utiliser le charset utf-8, notamment dans le cadre de projets multilingues.

CREATE DATABASE `mabase` CHARACTER SET utf8 COLLATE utf8_general_ci;

Bon côté PHP, vous allez me dire "oui mais moi avec ces histoires d'encodage je galère à chaque fois", patience, PHP 6 devrait régler ce problème.

Préférer un traitement SQL plutôt que PHP

En tant que développeur, vous préfèrez coder une moyenne en PHP plutôt que d'utiliser la petite fonction AVG de MySQL ? Erreur, il est plus performant de traiter ce cas côté MySQL. Songez donc à étudier la documentations MySQL pour vérifier s'il est possible d'effectuer les traitements dans la requête. De la même manière, évitez les requêtes dans une boucle PHP.

Optimiser les requêtes

99% des problèmes de performance sont causés par la base de données. La commande EXPLAIN de MySQL(tutoriel ici) permet de visualiser le plan d'exécution des requêtes, vous pourrez ainsi repérer les points bloquants. Des outils comme Query Profiler vous permettront également d'optimiser vos requêtes et votre schéma.

Utiliser le type de donnée adapté

Nous sommes souvent tenté de stocker des données sérialisées dans un champ de type texte. Ceci est très pratique mais peut s'avérer catastrophique en cas de forte volumétrie. Cette solution est donc à manipuler avec des pincettes, il est préférable d'utiliser au maximum les types de champs proposés par MySQL.

Eviter le "SELECT *"

Souvent, seuls certains champs sont nécessaires, il est donc inutile de tous les récupérer. Vous devez donc, spécifier les champs à récupérer un par un même si vous les voulez tous. En effet, si par la suite de nouveaux champs sont ajoutés à la table, le "*" les remontera alors qu'ils n'étaient pas nécessaires dans cette requête.

Manipuler les index raisonnablement

La règle de base pour positionner des index sur des tables est la suivante : toutes les colonnes aparaissant dans la clause WHERE de vos requêtes doivent avoir un index. On peut donc être tenté d'en positionner sur toutes les colonnes… surement pas ! En enffet, les index sont recalculés lors des opérations INSERT et UPDATE, ceci provoquerait des problèmes de performance.

Sauvegarder

Assurer vous de mettre en place des backups de la base. Vous pouvez également mettre en place un système de réplication master / slave.
Rappel : mysqldump permet d'effectuer des sauvegardes en ligne de commande.

Si vous avez des remarques ou des compléments à apporter, n'hésitez pas à commenter cet article.

 

 

Vous avez dit Gatling ?

Gatling est un outil permettant de réaliser des tests de montée en charge (stress tool) open source. Vous pouvez le télécharger sur Github

Gatling permet de mettre en place des scénarii ayant pour but de décrire le comportement d’un utilisateur sur l’application à tester. Pour ceux qui connaissent JMeter, le concept est le même, tous les deux tournent d’ailleurs sur une Java Virtual Machine.

Pourquoi utiliser Gatling ?

Ceux qui ont déjà manipulé JMeter ne pourront pas me contredire, injecter une multitude d’utilisateurs simultanément dans une application demande beaucoup de ressources machine. Ainsi, vous devez prévoir une machine performante sur laquelle installer JMeter afin de lancer les tests. Gatling est beaucoup moins gourmand. D’après un benchamrk effectué par Flood.io  Gatling supporterait  jusqu’à 20000 utilisateurs concurrents sur une JVM de 4GB (https://flood.io/blog/13-stress-testing-jmeter-and-gatling). Les performances de Gatling sont dues à son implémentation basée sur le  framework Akka (permettant de gérer très efficacement des applications multithread et concurrentes) et les IO non bloquants.

Définition des scénarii

Gatling permet d'enregistrer un parcours utilisateur  grâce à son « scénario recorder ». Ce mode lance un proxy qui vous permet de naviguer sur le site que vous voulez tester afin d’enregistrer un scénario qui sera joué lors de votre test de charge.

gatling-recorder

Gatling permet également d’écrire les scénarii en SCALA (plus compréhensible que le format XML utilisé par JMeter). Personnellement j’utilise le recorder dans un premier temps puis je customise ensuite le scénario à la main. Voici un exemple de scénario :

class MySimulation extends Simulation {

	val httpProtocol = http
		.baseURL("http://mydomain.com")
		.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
		.acceptEncodingHeader("deflate")
		.acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3")
		.connection("keep-alive")
		.userAgentHeader("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0")

	val headers_3 = Map("""Accept""" -> """*/*""")
	val headers_4 = Map("""Accept""" -> """text/css,*/*;q=0.1""")
	val headers_5 = Map("""Content-Type""" -> """application/x-www-form-urlencoded""")
	val headers_16 = Map("""Accept""" -> """image/png,image/*;q=0.8,*/*;q=0.5""")

	val scenario1 = scenario("Affichage basique type edito")
	    .group("Affichage basique type edito"){
			exec(http("scenario1 request_1")
				.get("""/"""))
			.pause(10)
			.exec(http("scenario1 request_2")
				.get("""/img-theme/images/favicon.ico"""))
			.pause(3)
			.exec(http("scenario1 request_3")
				.get("""/combo/?browserId=firefox&minifierType=&languageId=en_US&b=6120&t=1382088615000&p=/html/js&m=/aui/widget-base/assets/skins/sam/widget-base.css""")
				.headers(headers_4))
			.pause(6)
			.exec(http("scenario1 request_4")
				.post("""/web/portail/authentication?p_auth=YBAeIud5&p_p_id=58&p_p_lifecycle=1&p_p_state=maximized&p_p_mode=view&saveLastPath=0&_58_struts_action=/login/login&_58_doActionAfterLogin=false""")
				.headers(headers_5)
				.param("""_58_formDate""", """1382357885933""")
				.param("""_58_redirect""", """/""")
				.param("""_58_login""", """myLogin""")
				.param("""_58_password""", """myPassword""")
				.param("""_58_rememberMe""", """false"""))
			.pause(2)
		}
		
	setUp(scenario1.inject(ramp(100 user) over 1)).protocols(httpProtocol)
}

Vous remarquerez qu’on a simulé les temps de pause de l’utilisateur via la méthode « pause() ». Il est également possible, via la fonction « Feed() » de charger dynamiquement des variables depuis un fichier csv par exemple.

Tirs

L’exécution des scenarii s’effectue en ligne de commande (uniquement). C’est la dernière ligne du snippet précédent qui permet de configurer le tir :

setUp(scenario1.inject(ramp(100 user) over 1)).protocols(httpProtocol)

Ici on précise le scenario à exécuter, le nombre d’utilisateurs à injecter ainsi que la méthode à utiliser. Il en existe plusieurs :

  • atOnce : injecte tous les utilisateurs en une seule fois
  • ramp : injecte progressivement les utilisateurs
  • constantRate : injecte les utilisateurs de manière constante
  • je vous laisse consulter ce très bon article pour plus d’informations : http://blog.roddet.com/2013/06/gatling2-new-inject-api/

Rapport

Gatling permet de consolider les résultats des tirs grâce à différents rapports statistiques formalisés soit au travers de tableaux de données agrégées, soit au travers de graphiques:

gatling-error

Pour conclure

Gatling est un outil certes récent mais très aboutit, qui permet d'effectuer simplement des tests de montée en charge sur une application web. Pour moi, ses deux principaux atouts sont :

  • ses performances (moins gourmand que JMeter)
  • ses rapports (complets et faciles à interpréter)

Pour plus d’informations, n’hésitez pas à consulter la documentation officielle : https://github.com/excilys/gatling/wiki.

Dans cet article nous allons voir en détail quelques exemples d'optimisation à effectuer côté serveur web. Je n'ai pas souhaité classer ce post dans la catégorie "Tutoriels" car il s'agit ici plus d'une présentation de certains concepts plutôt que d'une véritable recette à suivre pas à pas. 

Gestion des connexions (TCP) persistantes :

La notion de connexion persistante vise à conserver la connexion TCP entre le client et le serveur pendant un bref instant. Ceci permet d'effectuer plusieurs requêtes HTTP avec une seule connexion. Pour activer les connexions persistantes, il est nécessaire de configurer le serveur de manière à ce qu'il notifie au client qu'il accepte celles-ci. Ceci s'effectue via l'en-tête "Connection" et la valeur "keep-alive" :

keep-alive

Voici un exemple de configuration Apache à placer dans le fichier de configuration (vhost ou htaccess) :

KeepAlive On #active la gestion des connexions persistantes
MaxKeepAliveRequests 100 #nombre de requêtes possibles
KeepAliveTimeout 15 #temps d'inactivité sur une même connexion

Attention, dans le cas où la majorité de vos documents statiques (css, javascripts, images) sont servis par un CDN (Content Network Delivery), il est préférable de ne pas autoriser les connexions persistantes. En effet, on sait dans ce cas que le client ne demandera pas d'autres ressources, il serait donc contre-performant de maintenir les connexions ouvertes.

Optimisation du cache navigateur :

Nous allons voir ici comment manipuler les en-têtes suivantes : expires, cache-control, Etag et Last-modified.

L'en-tête "Expires"

Cet entête définit la date d'expiration d'une ressource au sein du navigateur. Toute ressource arrivant au client avec cette en-tête sera mise en cache et permettra ainsi de délester le serveur Apache. Pour manipuler cette en-tête, il est nécessaire d'activer un module grâce à la commande : "a2enod expires". Vous pouvez ensuite configurer le cache au niveau de votre vhost ou .htaccess :

ExpiresActive On #on active l'expiration 
ExpiresByType image/gif "access plus 1 month" #les gif expirent un mois après leur accèsExpiressByType text/html "modification plus 1 week" #les fichiers HTML sont disponibles une semaine après leur mise à jour.

L'en-tête "Cache-Control"

Cet en-tête permet une plus fine gestion des caches en différenciant les navigateurs et les serveurs proxy. Voici les différentes valeurs possibles :

  • private : mise en cache possible pour les navigateurs mais pas pour les proxy
  • public : mise en cache possible pour les navigateurs et les proxy
  • no-cache : oblige les navigateurs et proxy à revalider systématiquement la ressource à chaque demande auprès du serveur
  • no-store : interdit la mise en cache de la réponse – must-revalidate : la ressource doit être revalidée si elle arrive à expiration (navigateur + proxy)
  • proxy revalidate : la ressource doit être revalidée si elle arrive à expiration (proxy uniquement)
  • max-age=xxx : temps relatif par rapport à la date de la réponse, pendant lequel le contenu est toujours vaide
  • s-maxage=xxx : identique à max-age mais s'applique aux proxy
  • no-transform : empêche les proxy d'encoder ou compresser les ressources

Généralement ces en-tête sont gérés automatiquement au sein des frameworks, applications, CMS etc. il est cependant possible de les configurer via le module "mod_header" d'Apache.

L'en-tête Etag (Entity Tag)

L'Etag correspond à une sorte d'identifiant de ressource. Ainsi, si la ressource est modifiée, l'Etag renvoyé par le serveur dans les en-têtes HTTP changera. L'Etag est structuré de la manière suivante :

  • numéro d'inode de la ressource sur le serveur (INode)
  • taille en octet de la ressource (Size)
  • date et heure de dernière modification (MTime)

La directive Apache "FileEtag" permet de modifier la structure de ce tag.

Compression des documents

Compresser ?

La compression des documents permet d'optimiser le chargement des pages. En effet, les contenus étant de plus en plus riches et variés (images, fichiers audios, fichiers vidéos, SVG, HTML, PDF etc), le poids des pages augmente, la consommation de bande passante est impactée en conséquence. 

Le fonctionnement est le suivant :

  1. Le serveur compresse les données
  2. Les documents compressés sont envoyés par le réseau
  3. Le navigateur décompresse les données avant d'en effectuer le rendu

Avantages / inconvénient ?

Bien que la compression ait un coût en terme de temps processeur, les gains concernant l'utilisation de la bande passante ainsi que le temps de chargement sont conséquents. Alors pourquoi s'en priver ?!

Quels fichiers compresser ? La compression est très efficace sur les données de type HTML, texte, PDF, CSS, Javascript ; en revanche il est quasiement inutile de compresser les fichiers binaires (images, vidéos, flash).

Mise en place sous Apache

  1. Activation du "mod_deflate" grâce à la commande : a2enmod deflate
  2. Configuration du vhost ou .htaccess :
<ifmodule mod_deflate.c>
   AddOutputFilterByType DEFLATE text/plain
   AddOutputFilterByType DEFLATE text/xml
   AddOutputFilterByType DEFLATE text/html
   AddOutputFilterByType DEFLATE text/css
   AddOutputFilterByType DEFLATE application/x-javascript
</ifmodule>
  1. Redémarrer Apache : service Apache2 restart (ou reload)

Conclusion

Cette article vous a présenté différentes oprimisations possibles avec Apache. Pour plus de détails, je vous conseil de lire l'excellent bouquin "Performances PHP – Audit et optimisation LAMP" aux éditions EYROLLES.

Je vous suggère également de consulter ce site http://browserdiet.com/ qui donne de très bon conseils pour améliorer les performances d'un site web. J'ai également rédigé une présentation consacrée aux web performances, ça se passe ici.

Il y à quelques semaines, je vous présentais un excellent outil de présentation Reveal.js. A cette occasion, je vous avais annoncé que je travaillais sur une présentation en rapport avec les web performances. Et bien, voilà, la présentation est disponible à cette adresse : http://supports.severin-bruhat.com/web-performance.

Au programme :

  • conseils sur les étapes de maquettage, d'intégration et de développement
  • les différents types de cache
  • quelques outils d'analyse
  • une démo

Cette introduction est basée principalement sur mon expérience LAMP, c'est pourquoi vous trouverez des références à Apache et PHP. Cependant, la majorité des concepts abordés s'applique à tous les languages.

Bonne lecture !