jobsphp

Migration microservices PHP : les leçons de trois ans de pratique

Ingénierie Web. Migration microservices PHP : les leçons de trois ans de pratique

Le paradoxe n’a pas bougé d’un pixel: on migre un monolithe PHP pour aller plus vite, puis on découvre qu’on a surtout multiplié les endroits où ça peut casser. Une requête qui traversait trois classes devient un trajet réseau.

Migration microservices PHP: les leçons de trois ans de pratique

Une transaction SQL devient une discussion entre services. Un déploiement « facile » se transforme en ballet de conteneurs, de sondes, de files et de tableaux de bord.

Et pourtant, la migration microservices PHP reste une excellente idée… quand elle répond à une vraie friction. Équipe bloquée par un module trop central, cycles de livraison qui se marchent dessus, domaine métier devenu illisible, charge très localisée: là, oui, on a matière à découper. Mais les erreurs vécues dans les migrations ne viennent généralement pas de PHP. Elles viennent de ce qu’on déplace sous le tapis en croyant découper du code: la donnée, les dépendances, les responsabilités et les incidents.

Trois ans de pratique, de retours de terrain et de discussions entre devs donnent une leçon assez nette: un microservice n’est pas un dossier src/ avec un Dockerfile posé à côté. Sinon, on ne fait pas une architecture distribuée. On fait du legacy distribué — la version premium, avec davantage de logs à lire à 2 h 17 du matin.

Pourquoi le Strangler Fig évite-t-il le grand soir technique?

Le scénario classique commence par une phrase inquiétante: « On va refaire la plateforme en microservices. » Traduction habituelle: pendant dix-huit mois, une équipe reconstruit à côté du produit, tandis que l’ancien système continue d’évoluer parce que les utilisateurs, eux, n’ont pas reçu le mémo.

Le motif Strangler Fig prend le problème à l’envers. On conserve l’interface connue des consommateurs — navigateur, application mobile, partenaires, back-office — et l’on remplace progressivement les fonctions du monolithe par de nouveaux services. Pas de bascule cosmétique un vendredi soir. Pas de big bang héroïque. On dévie peu à peu les flux vers la nouvelle brique, puis on retire l’ancienne quand elle ne porte plus de trafic.

C’est moins spectaculaire sur une slide. C’est infiniment plus sain sous le capot.

Dans un monolithe PHP mature, le premier découpage ne devrait pas suivre l’arborescence technique. « Un service pour les contrôleurs », « un service pour les repositories », c’est non. On cherche les frontières métier: identité, catalogue, facturation, candidatures, notifications, recherche, etc. Un bon candidat à l’extraction possède déjà plusieurs de ces signaux:

  • son rythme de changement est distinct de celui du cœur applicatif;
  • son modèle de données a un sens autonome et ne sert pas de fourre-tout à tout le reste;
  • son équipe peut en assumer les déploiements et les alertes;
  • il expose une capacité métier claire, pas une fonction utilitaire vaguement nommée CommonService (le cimetière des bonnes intentions);
  • son échec peut être dégradé sans éteindre toute la plateforme.

Prenons un cas fréquent: les notifications. Dans le monolithe, une création de candidature déclenche un e-mail, une notification interne, parfois un webhook. Cela ralentit la requête, mélange les règles de livraison aux règles de candidature et rend chaque incident fournisseur très sympathique. Extraire ce domaine permet de publier un événement — « candidature créée » — puis de laisser le service de notification traiter son travail à son rythme.

En revanche, sortir « la gestion des utilisateurs » dès le premier sprint peut être une très mauvaise idée. L’identité traverse tout: session, droits, données personnelles, back-office, support. On touche à un nœud de dépendances massif. L’extraire trop tôt, c’est faire de l’architecture à la tronçonneuse.

Le premier microservice ne doit pas prouver qu’on sait utiliser Docker. Il doit retirer une douleur réelle du monolithe.

Le Strangler Fig a une exigence souvent oubliée: la couche de routage doit rester explicite. Tant qu’une fonction existe dans l’ancien et le nouveau monde, il faut savoir qui sert quoi, pour quel flux, et comment revenir en arrière. Un proxy, une passerelle d’API ou une façade applicative peut porter cette transition. Ce n’est pas glamour. C’est précisément pour cela que c’est indispensable.

Peut-on découper le code sans découper la donnée?

Non. Et c’est ici que beaucoup de projets prennent le mur, avec une vitesse étonnante.

Dans une architecture microservices PHP, un service propriétaire de ses données les garde privées. Les autres services ne lisent pas directement ses tables « juste pour cette requête ». Ils passent par son API ou par des messages. Cette règle paraît dogmatique au début. Elle existe pourtant pour une raison très concrète: si trois applications écrivent dans la même table, plus personne ne possède vraiment le modèle métier.

Le raccourci le plus tentant ressemble à ceci: le service de facturation a besoin de savoir si une candidature est validée, alors il ouvre une connexion sur la base du service de candidatures. Ça marche. Jusqu’au jour où l’équipe candidature renomme un statut, migre une colonne, partitionne une table ou modifie une règle métier. À ce moment-là, l’API interne non déclarée — la base SQL — explose en silence.

La séparation impose un coût: accepter la cohérence éventuelle. Une information peut mettre quelques secondes à circuler entre deux services. Cela ne veut pas dire « on ne sait pas faire des transactions ». Cela signifie qu’on choisit où la cohérence doit être immédiate, et où elle peut être asynchrone.

La différence est fondamentale:

SituationMonolithe avec transaction uniqueServices séparés
Création d’une candidature et écriture d’un auditUne transaction SQL peut englober les deux opérationsChaque service valide sa transaction locale
Échec après une première écritureRollback local possibleUne action compensatoire peut être nécessaire
Lecture d’un statut métierLecture directe dans la même baseAPI dédiée ou projection locale alimentée par événements
Évolution du schémaChangement coordonné dans un même dépôtContrat à versionner entre producteurs et consommateurs

La saga entre alors en scène. Pas comme une baguette magique ACID distribuée — ce fantasme a déjà fait assez de dégâts — mais comme une succession de transactions locales. Chaque étape produit un résultat, puis déclenche l’étape suivante. Si une étape échoue, on exécute selon le cas une action compensatoire: annuler une réservation, marquer une commande comme échouée, libérer une ressource.

Le mot clé, c’est « compensation », pas « rollback ». Une confirmation e-mail déjà partie ne se rollback pas. Un prélèvement bancaire non plus, au sens applicatif du terme. On doit donc modéliser des états métier honnêtes: en_attente, confirmée, annulée, remboursée, plutôt que faire semblant que tout disparaît atomiquement.

En PHP, cette discipline se traduit souvent par une outbox transactionnelle: on enregistre l’événement à publier dans la même transaction locale que la modification métier, puis un processus dédié le diffuse. Cela réduit le risque du fameux entre-deux: donnée enregistrée, événement jamais envoyé parce que le processus est tombé au mauvais moment. Ce n’est pas une petite subtilité de backend. C’est la différence entre une architecture explicable et une loterie distribuée.

Pourquoi les appels interservices deviennent-ils le vrai sujet?

Dans un monolithe, appeler une méthode échoue vite et près de chez soi. Dans des microservices, un appel peut échouer parce que le réseau a eu un hoquet, que le service distant est saturé, que la résolution DNS patine, que son conteneur redémarre, ou que la réponse est arrivée après votre délai d’attente. Et le plus drôle? L’opération distante a peut-être réussi, mais sa réponse s’est perdue.

C’est là que le réflexe « on met trois tentatives » devient dangereux. Pour une lecture idempotente, un nouvel essai peut être raisonnable si les délais sont bornés et accompagnés d’un backoff. Pour une écriture — créer une facture, débiter un compte, publier une candidature — le même réflexe peut créer des doublons.

Une API sérieuse doit rendre visibles ses garanties. Une clé d’idempotence associée à une commande d’écriture permet au serveur de reconnaître qu’il traite à nouveau la même intention métier. On ne se contente plus de demander « la requête a-t-elle marché? ». On peut demander: « cette demande a-t-elle déjà été acceptée, et avec quel résultat? »

Les briques de résilience ne sont pas interchangeables. Chacune répond à une panne différente:

1. Le délai d’attente limite le temps pendant lequel une requête attend une réponse. Sans lui, les workers PHP-FPM se mettent à patienter, puis s’accumulent, puis transforment une panne distante en panne générale. Ambiance.

2. Le nouvel essai borné couvre certains défauts transitoires, mais seulement si l’opération est idempotente ou si le protocole le rend sûre.

3. Le backoff évite que cent requêtes échouées réessaient exactement au même instant. Sinon, on attaque le service déjà à terre avec une énergie assez admirable.

4. Le circuit breaker cesse temporairement les appels vers une dépendance manifestement indisponible. Au lieu d’empiler les requêtes bloquées, on échoue vite ou l’on sert un mode dégradé.

5. La file de messages absorbe certaines pointes et découple les opérations non immédiates. Elle ne rend pas le système invulnérable; elle déplace le problème vers le suivi des retards, des rejets et des consommateurs. Donc: monitoring obligatoire.

Le piège microservices web le plus courant reste le « monolithe distribué »: cinq services, mais chaque action utilisateur exige une chaîne synchrone de cinq appels. Le service A attend B, qui attend C, qui attend D… À la première lenteur, tout le monde ralentit ensemble. On a remplacé un déploiement unique par une panne collective plus difficile à diagnostiquer. Beau progrès, vraiment.

Pour les parcours critiques, il faut garder les chemins synchrones courts. Et pour les données d’affichage, envisager des projections locales ou des lectures pré-calculées. Oui, cela implique de dupliquer certaines données dérivées. Non, ce n’est pas hérétique si la propriété de la donnée source reste claire.

Une architecture distribuée performante n’est pas celle qui appelle partout. C’est celle qui sait très précisément où elle a le droit d’attendre.

Et si l’API est protégée par un jeton, est-on tranquille?

Absolument pas. Un jeton prouve surtout qui appelle. Il ne prouve pas que cet appelant a le droit d’accéder à cet objet précis.

L’OWASP classe le contrôle d’autorisation au niveau objet — BOLA — parmi les risques majeurs des API. Le scénario est banal: un utilisateur authentifié appelle GET /candidatures/4821, remplace l’identifiant par 4822, et accède à une candidature qui ne lui appartient pas. Des UUID réduisent la devinette, mais ne remplacent jamais une vérification d’autorisation. Une URL non prévisible n’est pas une politique de sécurité. C’est juste un cadenas peint sur la porte.

Dans un service PHP, l’autorisation doit vivre près de l’action métier. Pour chaque endpoint qui manipule une ressource identifiée par une entrée client, on vérifie explicitement le droit sur cette ressource: appartenance à l’organisation, rôle sur l’offre concernée, relation avec la candidature, périmètre du recruteur, etc.

Cette logique mérite des tests d’intégration dédiés. Pas seulement « un utilisateur autorisé reçoit 200 ». Il faut aussi les scénarios que personne n’aime écrire, donc ceux qui sauvent les plateformes:

  • un recruteur de l’entreprise A tente d’accéder à une ressource de l’entreprise B;
  • un utilisateur authentifié appelle une fonction réservée à un administrateur;
  • un identifiant existant mais hors périmètre est passé dans une URL ou un corps de requête;
  • un service interne utilise un jeton de service avec des droits plus larges que nécessaire;
  • une requête abusive tente de consommer des ressources coûteuses: export massif, recherche lourde, génération de documents.

La sécurité des API ne se résume d’ailleurs pas au BOLA. Les limites de débit, la taille maximale des charges, les délais d’exécution et la pagination sont des décisions de sécurité autant que de performance. L’OWASP signale aussi la consommation non restreinte de ressources comme un risque API à part entière. Une route qui accepte une pagination de 100 000 lignes n’est pas généreuse: elle est probablement en train d’inviter un incident.

Docker, OPcache et Kubernetes: où se perd la performance microservices PHP?

On entend encore: « C’est dans un conteneur, donc c’est isolé. » Oui, mais isolé ne veut pas dire dimensionné.

Par défaut, un conteneur Docker n’a pas de contrainte de CPU ni de mémoire. Sur un hôte Linux sous pression mémoire, l’OOM killer peut intervenir. Dit autrement: sans limites, un service bavard peut manger les ressources communes, et un autre service parfaitement innocent peut mourir à sa place. Le grand jeu de la prod moderne.

Fixer des limites n’autorise cependant pas la divination. Copier --cpus=".5" parce qu’un exemple le montre, ou coller 512 Mo partout, n’est pas une stratégie. Un service qui traite des images, génère des PDF ou maintient de gros résultats en mémoire n’a pas le profil d’un service JSON léger. On mesure le trafic, la mémoire par worker, les pics, la latence, les redémarrages et la marge nécessaire lors d’un déploiement.

Même logique pour OPcache. Les valeurs par défaut de PHP — 128 pour opcache.memory_consumption, 10 000 pour opcache.max_accelerated_files — sont des points de départ, pas des réglages de production gravés dans le marbre. Un service Symfony volumineux, un microservice Slim très compact, un worker de consommation de messages: ces trois processus n’ont ni le même code chargé, ni le même rythme de redémarrage, ni les mêmes besoins.

Quelques symptômes parlent immédiatement à celles et ceux qui ont déjà regardé un tableau de bord un peu trop tard:

  • la mémoire OPcache est saturée ou les clés sont épuisées: le cache ne joue plus son rôle correctement;
  • les déploiements ne purgent pas comme prévu: on sert du code ancien, le bonheur;
  • les workers PHP-FPM sont tous occupés alors que le CPU semble calme: probablement de l’attente I/O, souvent un appel distant;
  • le conteneur redémarre sans erreur applicative claire: suspecter une limite mémoire et un OOM kill;
  • la latence augmente après avoir extrait un service: chercher d’abord le nombre d’aller-retours réseau, avant de lancer une refacto mystique.

Kubernetes ajoute son vocabulaire, mais pas une immunité magique. Une sonde de disponibilité réussie conditionne l’envoi de trafic vers un pod. Elle doit donc répondre à une question simple: « ce processus peut-il réellement recevoir cette requête? » Un endpoint de santé minimal est préférable à une page qui déclenche des connexions à toutes les dépendances distantes. Sinon, un fournisseur e-mail indisponible peut déclarer indisponible votre API de lecture. On connaît plus joyeux.

La fréquence par défaut des sondes est de dix secondes. C’est un détail jusqu’au moment où il ne l’est plus: démarrage lent, migrations, réchauffement du cache, dépendance momentanément absente. Les sondes de démarrage, de vivacité et de disponibilité n’ont pas le même rôle. Les confondre revient à demander à un détecteur de fumée de réparer l’incendie.

Peut-on piloter sans voir les traversées entre services?

Non, et c’est probablement la facture la plus sous-estimée d’une migration.

Dans un monolithe, une trace d’erreur suffit parfois à reconstituer l’histoire. Dans une architecture microservices PHP, il faut suivre une requête à travers plusieurs processus et, parfois, une file asynchrone. Sans identifiant de corrélation, on finit par ouvrir six interfaces et reconstituer un incident comme une enquête policière sans témoin.

OpenTelemetry est devenu une base solide pour les traces, les métriques et les journaux côté PHP; son auto-instrumentation requiert PHP 8.0 ou plus. Le point n’est pas de collectionner des outils parce que l’observabilité est hype. Le point est d’être capable de répondre vite à des questions banales et vitales:

  • quelle dépendance a ajouté 800 ms à cette route?
  • combien de messages attendent dans la file et depuis combien de temps?
  • quelle version du service a fait monter les erreurs?
  • est-ce un problème applicatif, réseau, base de données ou saturation de workers?
  • quel parcours métier est touché, pas seulement quel conteneur est rouge?

Les métriques techniques seules ne suffisent pas. Un taux d’erreur HTTP peut rester impeccable pendant qu’un consommateur de messages accumule du retard et que les notifications partent avec une heure de décalage. On a besoin de métriques métier: candidatures créées, événements en attente, paiements compensés, exports abandonnés, actions refusées par les règles d’autorisation.

C’est aussi là qu’une équipe PHP prend de la maturité. Pas en ajoutant une douzième couche d’abstraction. En sachant relier une alerte à un impact produit, une trace à un contrat d’API, et un contrat à une responsabilité claire.

La vraie sortie du monolithe, c’est une discipline collective

Une migration microservices PHP réussie ne produit pas forcément vingt services. Elle produit surtout des frontières lisibles, des données réellement possédées, des appels dont on accepte les échecs, et une exploitation qui ne découvre pas les problèmes après les utilisateurs.

Le monolithe n’est pas un ennemi. Un monolithe modulaire, testé, déployable et compris peut rester le meilleur choix pendant longtemps. Mais lorsqu’un domaine doit évoluer à son propre rythme, qu’il porte sa charge spécifique et qu’une équipe peut le posséder de bout en bout, l’extraction devient un vrai game changer.

Alors on avance par étranglement progressif, pas par réécriture romantique. On traite la cohérence comme un sujet métier, pas comme un détail SQL. On mesure avant de fixer des limites Docker ou de gonfler OPcache. On vérifie les droits sur chaque objet, même avec le plus beau des jetons. Et surtout, on installe traces, métriques et journaux avant que le premier incident distribué ne nous les impose.

Le débat ne devrait donc pas être « microservices ou monolithe? ». La bonne question est plus inconfortable — donc plus utile: quel morceau de notre système mérite vraiment de devenir autonome, et sommes-nous prêts à l’exploiter comme tel?

Questions fréquentes

Pourquoi éviter de découper le monolithe selon l'arborescence technique ?
Découper par couches techniques, comme les contrôleurs ou les repositories, ne crée pas de frontières métier autonomes et maintient une architecture trop dépendante du code existant.
Comment gérer la cohérence des données entre plusieurs services ?
Il faut accepter la cohérence éventuelle et utiliser des transactions locales successives, éventuellement complétées par des actions compensatoires en cas d'échec, plutôt que de chercher une transaction globale.
Quelles sont les bonnes pratiques pour sécuriser les API microservices ?
Un jeton d'authentification ne suffit pas ; il est crucial de vérifier explicitement les droits d'accès au niveau de chaque objet pour prévenir les risques de type BOLA.
Pourquoi les appels interservices synchrones sont-ils risqués ?
Une chaîne d'appels synchrones transforme une panne isolée en une défaillance collective, car chaque service devient dépendant de la disponibilité et de la latence des autres.
Comment configurer correctement les ressources Docker pour PHP ?
Il faut définir des limites de mémoire et de CPU basées sur des mesures réelles de trafic et de consommation, tout en adaptant les réglages d'OPcache aux besoins spécifiques de chaque service.