Techos Blog technique de Xotelia

Migration PHP 7

Voilà c’est fait ! Nous avons enfin migré nos applications en PHP 7.0. Dans un premier temps Robocop, notre service et synchronisation et de récupération de réservations. Puis dans un second temps, le backoffice contenant du code legacy.

champagne

L’état des lieux

La stack technique de Xotelia est assez simple, PHP/Symfony pour le code (avec du legacy pour le backoffice), MySQL comme datastore principal, MongoDB comme datastore secondaire (stockage des évènements), Redis pour le cache, Memcached pour les sessions et RabbitMQ pour les queues.

Pour MySQL, aucun souci, nous utilisons le driver pdo_mysql qui en interne passe par mysqlnd. Pour Redis, là aussi aucun souci car le client que nous utilisons (predis/predis) utilise les streams de PHP par défaut. De même pour RabbitMQ, par défaut les streams de PHP sont utilisés. Il est aussi possible de passer par l’extension sockets, supportée par PHP 7.

Le premier problème rencontré concerne MongoDB. Nous utilisons l’ODM de doctrine pour mapper nos documents avec mongo, et cette bibliothèque utilise l’extension PECL mongo qui est dépréciée et qui n’est plus disponible avec PHP 7.

Deuxième problème, Memcached. Il n’y a pour l’instant aucune version stable de l’extension PECL memcached disponible pour PHP 7, il faut donc la compiler soit-même.

Dernier problème concernant le code, l’utilisation de classe réservée comme String, Int ou Null. Avec PHP 5 c’était possible, depuis PHP 7 ces classes sont réservées.

Les solutions

Je tiens d’abord à préciser que nous compilons PHP sur tous nos serveurs, afin de contrôler la version déployée, mais surtout ne pas être limité par la version disponible dans les dépôts (coucou Debian !). Je me suis grandement inspiré du Dockerfile de l’image PHP officielle, que j’ai adapté en rôle Ansible.

MongoDB

Concernant MongoDB, étant donné que l’extension PECL mongo n’est plus disponible au profit de mongodb, j’ai du m’adapter. Le problème c’est que les interfaces sont totalement différentes entre ces deux extensions. Par exemple la connexion à MongoDB se fait en utilisant MongoClient avec l’extension mongo tandis qu’avec l’extension mongodb il faut utiliser MongoDB\Driver\Manager. Lorsque j’ai commencé la migration vers PHP 7, doctrine n’avait pas prévu de migration vers la nouvelle extension, aujourd’hui il y a une pull request en discussion.

Après quelques recherches pour pallier à ce problème en fouillant dans les issues github et sur google, je suis tombé sur cette bibliothèque qui fournit l’interface de mongo tout en utilisant l’extension mongodb. Et en plus ça marche à merveille !!

Toujours concernant mongo, l’ODM permet de faire le mapping de ses documents via les annotations, et propose des alias comme String, que nous avions utilisé. J’ai donc remplacé toutes ces occurences dans notre code :

<?php
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

// before

/**
 * @ODM\Document
 */
class Foo
{
    /**
     * @ODM\String
     */
    private $message;
}

// after

/**
 * @ODM\Document
 */
class Foo
{
    /**
     * @ODM\Field(type="string")
     */
    private $message;
}

Memcached

L’autre problème concernait Memcached, et le fait qu’aucune version stable de l’extension PECL n’est disponible pour PHP 7. Là moins de problèmes, il faut simplement compiler soit-même l’extension depuis le code source sur git. Lorsque j’ai commencé la migration, il fallait passer spécifiquement par la branche php7, car sur master il y avait seulement le code pour PHP 5. Aujourd’hui la branche php7 a été fusionnée sur master.

La migration

Pour la migration, j’ai fait un playbook ansible qui met à jour les serveurs un par un, en les déconnectant du load balancer pour ne plus recevoir de trafic. Pour Robocop, tout se passe bien. Je déconnecte chacun des serveurs, je déploie la mise à jour de PHP, ainsi que la nouvelle version de l’application avec l’adaptateur pour mongodb et c’est parti ! Mis à part pour la mémoire, je n’ai pas de graph très représentatif concernant cette migration car en plus de l’application PHP, il y a aussi des shards MongoDB qui tournent sur chacun des serveurs. Sur 6 serveurs on a quand même gagné 26 Go de RAM.

Pour le backoffice, ça ne se passe pas aussi bien. J’applique le même principe que pour Robocop, c’est à dire la mise à jour des serveurs un par un. Seulement une fois le premier serveur reconnecté, l’application se met à ralentir fortement. En fouillant les logs je vois qu’il y à un log lors de chaque requête :

[23-Jan-2017 13:14:59] WARNING: [pool xotelia] child 24984 said into stderr: "NOTICE: PHP message: PHP Warning: session_write_close(): Failed to write session data (user). Please verify that the current setting of session.save_path is correct (memcached:11211) in /var/www/xotelia/releases/20170123124745/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php on line 226"

Gestion de crise

cry

Et là c’est le drame. Une fois tous les serveurs mis à jour, chaque requête met plus de 3 secondes et une bonne partie d’entre elles termine en erreur 500. Il faut savoir que sur le backoffice nous utilisons Kyototycoon qui fournit l’interface de Memcached pour les sessions. Je commence à faire des recherches sur cette erreur PHP concernant session_write_close et je tombe sur une issue symfony et donc un problème qui semble soit lié au framework, soit à la version de PHP. Étant donné que dans l’issue le problème est remonté avec symfony 3 et que nous avons symfony 2.8 (pour des raisons de compatibilité avec certains bundles), je me dirige vers PHP.

En regardant le readme du code source de memcached, il est conseillé de compiler l’extension avec libmemcached version 1.0.16 minimum. Nous avons la version 1.0.8 en production. Je me décide donc à compiler moi-même la dernière version (1.0.18) et ensuite de recompiler l’extension PHP. En parallèle de ces compilations, je me rends compte que l’on a absolument aucun problème de session en local, car nous avons mis Memcached et non Kyototycoon dans la vagrant. Je désinstalle donc Kyototycoon et installe Memcached à la place. À partir de ce moment là, plus aucune erreur concernant les sessions dans les logs. Je termine quand même les compilations pour avoir l’extension memcached compilée avec la dernière version de libmemcached.

Plus de peur que de mal, mais un ralentissement du système sur plusieurs heures le temps de trouver l’origine du problème. Je ne sais toujours pas pourquoi l’enregistrement des sessions échoue avec Kyototycoon et PHP 7. Un bon sujet pour un prochain article.

mémoire backoffice

Là aussi, pas vraiment d’autres graphs intéressant, car il n’y a pas que PHP sur ces serveurs, mais aussi MySQL et MongoDB. Et voilà pour aujourd’hui. Prochaine migration, Symfony 3.2 pour le backoffice (déjà déployé pour Robocop).

Bonus

Petit comparatif du temps d’exécution des tests unitaires entre PHP 5.6 et 7.0 :

# Xotelia

# 5.6.14

Time: 26.6 seconds, Memory: 332.00MB

OK, but incomplete, skipped, or risky tests!
Tests: 697, Assertions: 10393, Risky: 3.

# 7.0.4

Time: 19.57 seconds, Memory: 156.00MB

OK, but incomplete, skipped, or risky tests!
Tests: 697, Assertions: 10396, Risky: 3.

# Robocop

# 5.6.14

Time: 3.2 minutes, Memory: 131.50MB

OK (263 tests, 2497 assertions)

# 7.0.4

Time: 59.89 seconds, Memory: 58.00MB

OK (263 tests, 2497 assertions)