Tests comparatif de Zend Framework 2 (Ecrit en 2013)

J'ai été récemment confronté à l'optimisation d'un site web écrit avec Zend Framework 2. J'ai été étonné par les performances lamentables de l'application. Les développeurs m'assuraient que, selon un site web dont je ne connais pas l'url, les performances d'une application ne sont pas très différentes avec et sans l'utilisation de ce framework. Comme je suis d'un naturel sceptique, je préfère faire des tests moi même.

Présentation de l'application benchée

J'ai écrit une application avec Zend Framework 2 en me basant sur un tutoriel disponible sur le site officiel de Zend Framework 2. Cette application est composée d'un module nommé Application fourni par défaut avec un template de base. Elle comporte aussi un module Coucou qui fait « Coucou » quand on l'appelle via l'url /coucou et qui renvoie un mot du dictionnaire Français quand on l'appelle via l'url /coucou/get/<id>.

J'ai également écrit la même application dans un fichier PHP qui n'utilise que les fonctions de base du PHP. Il réagit exactement de la même manière que le module fait à partir de Zend Framework 2 hormis le préfixe de l'application qui est /coucou2. L'application Zend Framework 2 et l'application PHP sont utilisables dans le même serveur d'application qui est en l'occurrence Apache. Elles fonctionnent en parallèle.

L'archive du code de bench est disponible ci-dessous. Cette archive peut être détarrée n'importe où. Elle est prévue pour fonctionner sur un OS CentOS6. Le répertoire vendor est vide, il faut le remplir avec la commande php composer.phar install. Il faut bien sûr un accès à Internet. Il faut que Apache et PHP soient installés. Le serveur d'application peut être démarré avec la commande ./start.

Télécharger le code utilisé pour le benchmark

Présentation du matériel utilisé

La machine support est mon PC portable personnel. Il comporte un CPU à deux cœurs cadencés à 2,4GHz. La machine dispose de 3Go de ram. Le support exécutable est sur une centos 6 mise à jour à la date du bench (le 18/12/2013). J'utilise avec cette distribution, les versions d'Apache et PHP disponibles sur le «repo de remi» ainsi que le repo «EPEL». Les modules Zend Framework 2 sont récupérés avec composer.js. Apache est configuré avec 100 process parallèles et PHP est configuré pour ne pas dépasser 16Mo de ram. 16Mo permet d'exécuter une page Zend Framework 2 sans encombres. La base de données est installée sur la même machine. Lors des tests utilisant la base de données, le CPU est partagé avec elle.

Pour les tests d'injection, j'utilise l'injecteur de Willy Tarreau dont le repo git est disponible à l'adresse http://git.formilux.org/?p=people/willy/inject.git . L'injecteur est utilisé sur la même machine que celle qui héberge l'application.

Ce test n'a pas pour but de trouver les performances maximales de l'application, mais de comparer son comportement avec celui de l'application PHP sans framework. Le fait d'installer la base de donnée et l'injecteur sur la même machine que celle qui héberge l'application pourrait limiter les performances de l'application, mais ce test est un comparatif. Les deux versions de l'application auront les mêmes contraintes.

Les benchmarks

Le premier test consiste à injecter du trafic sur la page qui fait le moins de travail, à savoir celle qui affiche «Coucou». L'injection est faite sur l'URL /coucou qui passe par Zend Framework 2 et sur l'URL /coucou2 qui n'utilise pas Zend Framework 2.

Les URI utilisés pour ce premier test sont:

  • Stress de Zend: /coucou
  • Stress du PHP: /coucou2

Voilà la description des colonnes:

  • Nb. Users: Le nombre d'utilisateurs simultanés.
  • Application: Le nom de l'application associée aux chiffres. On retrouve PHP pour l'application PHP native et Zend Pour l'application Zend Framework 2
  • CPU: Le pourcentage de CPU consommé. 100% indique une consommation de 100% de chacun des CPU de la machine.
  • Requêtes/s: Le nombre de requêtes HTTP effectuées par secondes.
  • Pages/h: Le nombre de pages qui peuvent être générées en une heure.
  • Temps de rép. (ms): Le temps de réponse moyen des requêtes HTTP. Cette valeur est exprimée en millisecondes.

Voila le comparatif de performances:

Nb. Users Application CPU Requêtes/s Pages/h Temps de rép. (ms)
2 PHP 80% 5 400 19 440 000 0.1
Zend 98% 41 (÷ 132) 147 600 50 (× 500)
5 PHP 80% 5 450 19 620 000 0.1
Zend 100% 41 (÷ 133) 147 600 120 (× 1200)
10 PHP 80% 5 650 20 340 000 0.6
Zend 100% 41 (÷ 138) 147 600 240 (× 400)
50 PHP 85% 5 000 18 000 000 5
Zend 100% 41 (÷ 122) 147 600 1200 (× 240)
100 PHP 85% 4900 17 640 000 10
Zend 100% 39 (÷ 126) 140 400 2500 (× 250)

Dans ce cas de figure, il y a un facteur 137 entre Zend Framework 2 et du PHP sans framework. Un facteur 137 veut dire que pendant que l'application Zend Framework 2 fabrique une seule page, l'application PHP en fabriquera 137. On voit aussi un facteur 250 à 500 sur les temps de réponses. Cela veut dire que lorsqu'un site un peu chargé met 1 seconde à répondre en PHP, il mettra 500 secondes à répondre une fois migré sur Zend Framework 2. On voit d'ailleurs sur ces mesures que dès que le serveur est un peu chargé, Zend Framework 2 n'arrive pas à assurer des temps de réponse acceptables.

Voila maintenant le comparatif sur les URL qui font des accès en base de données. Je demande toujours le même ID, de cette manière il reste dans le cache de la base de données et celle-ci ne pénalise aucune des deux applications. Les URL utilisés sont:

  • Stress de Zend: /coucou/get/56789
  • Stress du PHP: /coucou2/get/56789
Nb. Users Application CPU Requêtes/s Pages/h Temps de rép. (ms)
2 PHP 94% 2 600 9 360 000 0.1
Zend 98% 35 (÷ 74) 126 000 55 (× 550)
5 PHP 100% 2 550 9 180 000 1.3
Zend 100% 35 (÷ 73) 126 000 140 (× 108)
10 PHP 100% 2 400 8 640 000 3.6
Zend 100% 35 (÷ 69) 126 000 280 (× 77)
50 PHP 100% 2 100 7 560 000 18
Zend 100% 33 (÷ 64) 118 800 1400 (× 77)
100 PHP 100% 2 200 7 920 000 29
Zend 100% 31 (÷ 71) 111 600 3200 (× 110)

Il est à noter que les chiffres obtenus ci-dessus sont totalement en phase avec ceux qui m'ont été rapportés par d'autres développeurs effectuant des tests unitaires sur ce framework sur des serveurs réels déjà tunés.

Il faut croire que Zend Framework 2 gère un peu mieux les connexions à la base de données que mon code. L'écart entre les performances n'est plus que d'un facteur 74.

Il n'y a pas besoin d'être très malin pour se rendre compte qu'un tel écart ne peut pas être causé simplement par un défaut de code ou un manque de tuning, il y a forcément un problème structurel beaucoup plus profond.

Pour essayer de comprendre le gap entre les deux méthodes, j'ai installé et activé l'outil xdebug. Il est habituellement utilisé pour faire du profiling de code, mais si on l'utilise avec une seule requête il se limite à tracer tous les appels de fonctions de cette requête. J'ai appliqué cette méthode avec le code Zend Framework 2 et avec le code PHP sans framework. Le résultat est édifiant:

BDD Application Appels de fonctions
Sans PHP 6
Zend 7 273 (× 1212)
Avec PHP 11
Zend 8 290 (× 753)

Les quatre fichiers sont téléchargeables ici: Zend: /coucou Zend: /coucou/get/<id> PHP: /coucou2 PHP: /coucou2/get/<id>

Cette mesure montre qu'entre la version la plus simple d'un code PHP et la version équivalente faite avec Zend Framework 2 il y a un facteur approximatif de 1200 sur le code executé ! Cela signifie que Zend effectuera 1200 fois plus d'appels de fonctions qu'un code natif PHP pour effectuer le même travail.

On peut noter que pour faire une simple requête en base de données, Zend Framework 2 fait tout même 1012 appels de fonctions de plus qu'un simple code PHP.

Taille du code produit

Le dernier point de comparaison concerne le code produit. Pour cette démo, la partie PHP est composée d'un seul fichier nommé tfo.php. Ce fichier comprends 90 lignes commentaires inclus.

La partie Zend Framework 2 comprend 8 fichiers répartis dans le répertoire du module Coucou. Ces 8 fichiers contiennent 183 lignes de code tout compris. Elle comprend également 1 fichier dans le répertoire du module Application qui contient 1 seule ligne. A ces fichiers, il faut ajouter la configuration répartie dans 3 fichiers du répertoire config qui contiennent 101 lignes de codes. Soit une total de 12 fichiers et de 285 lignes de code.

Application Nb. fichiers Nb. répertoires Nb. lignes Nb. fichiers framework Nb. lignes framework
PHP 1 1 90 0 0
Zend 12 (× 12) 15 (× 15) 285 (× 3.1) 2 427 299 161

Voila ci-dessous l'organisation des fichiers utiles pour afficher une simple «Coucou» et faire une requête dans une base de données. L'organisation est donnée pour les deux type de code comparés à savoir PHP et Zend Framework 2.

PHP Zend Framework 2
  • public
    • tfo.php (90 lignes)
  • module
    • Application
      • view
        • layout
          • layout.phtml (1 ligne)
    • Coucou
      • Module.php (50 lignes)
      • view
        • coucou
          • coucou
            • get.phtml (10 lignes)
            • index.phtml (1 ligne)
      • config
        • module.config.php (35 lignes)
      • src
        • Coucou
          • Model
            • Coucou.php (15 lignes)
            • CoucouTable.php (32 lignes)
          • Controller
            • CoucouController.php (37 lignes)
      • autoload_classmap.php (3 lignes)
  • config
    • application.config.php (65 lignes)
    • autoload
      • local.php (8 lignes)
      • global.php (28 lignes)

La plupart des lignes de codes écrites dans les différents fichiers du module Zend Framework 2 ne sont que des descriptions. Quelques fichiers contiennent du code et un fichier est exécuté mais ne sert strictement à rien puisqu'il se contente de retourner un tableau vide (autoload_classmap.php).

Conclusion

Ces benchmarks montrent que le coût d'entrée à Zend Framework 2 le moins cher est de 7200 appels de fonctions et 50ms de temps de réponse dans des conditions optimales. Les conditions optimales sont atteintes lorsque il y a autant d'utilisateurs que de CPU. Les 50ms seront accélérées par du matériel plus puissant, mais ne seront pas vraiment réduites car il s'agit de pur gaspillage de cycles CPU. A ma connaissance, il n'y a aucun moyen d'optimiser le langage pour qu'il soit interprété plus rapidement.

A priori en ajoutant le cache de code PHP APC, les temps de réponse seront un peu moins longs car il n'y aura plus besoin de charger et compiler les fichiers PHP. Attention, car les 7200 appels de fonctions ne disparaîtront pas. Un ajout de cache externe pour conserver des données précalculées tel que memcached permettra d'éviter le calcul des données, mais ne fera pas baisser le cout d'entrée à Zend Framework 2. On retrouvera toujours ces 7200 appels de fonctions.

A priori aucune optimisation ne peux accélérer ce fonctionnement de Zend Framework 2 de manière significative. Le problème est que Zend Framework 2 exécute trop de code PHP pour rendre le service.

Le code source joint permet au lecteur de faire ses propres tests et d'avoir ses propres conclusions plutôt que d'utiliser des conclusions toutes faites et trouvées n'importe où sur Internet. Il est courant de trouver des tests biaisés fait par des personnes qui n'ont pas une opinion objective sur les produits.

Zend Framework 2 est utile pour structurer du code avec le modèle MVC. Il fourni également des tas de fonctions pour faciliter les accès à des technologies existantes (JSON, SOAP, MySQL, ...). Il va donc être un excellent choix pour s'exercer au développement web et pour permettre à des sociétés de développement de sites web de proposer de multiples maquettes à des clients pour les aider à choisir un rendu. La contrepartie de ce choix de facilité est la maigreur des performances. De la même manière que la plupart des frameworks, pour simplifier le travail du développeur, Zend Framework 2 va exécuter des milliers de lignes de code supplémentaires dont un développement sans framework sait se passer. Ces milliers de lignes de codes superflus gaspillés à chaque requêtes, représentent la réflexion et les efforts que le développeur aura économisé une seule fois lors de l'écriture de son code.