Ajouter un cache peut diviser une latence en quelques heures, puis transformer le premier incident de cohérence en enquête de plusieurs jours. Une fiche produit reste fausse après correction, un client voit le prix d’un autre segment ou tous les processus recalculent la même valeur à l’expiration. Le cache n’a pas créé le défaut, mais il l’a rendu intermittent et plus difficile à attribuer.
Le vrai enjeu n’est pas de mettre des données « en mémoire ». Il consiste à décider combien de temps une réponse peut être ancienne, qui sait qu’elle a changé et ce que l’application fait lorsque le cache est vide ou indisponible. Sans ce contrat, le TTL devient une loterie et l’équipe traite des symptômes de conception avec davantage d’infrastructure.
Contre-intuitivement, un hit ratio élevé peut signaler un problème. Si une clé trop large sert la même réponse à des profils différents, le tableau paraît excellent pendant que le produit est faux. La mesure utile relie donc performance, fraîcheur, autorisation et coût de recalcul, au lieu de célébrer un pourcentage isolé.
Dans une application web métier sur mesure, le cache reste un dérivé reconstruisible, jamais une source de vérité. Cette méthode aide à choisir le niveau, fermer l’invalidation, tester les pannes et poser des seuils locaux avant de généraliser.
Prouver où le temps est réellement perdu
La première mesure décompose le parcours : temps applicatif, requêtes SQL, appels externes, sérialisation, réseau et rendu. Mettre en cache le résultat d’une requête de 20 millisecondes ne corrige pas l’API partenaire qui prend deux secondes. Le profil doit couvrir un percentile, des volumes et un scénario utilisateur, pas une exécution chaude sur le poste d’un développeur.
L’équipe cherche ensuite la répétition. Une donnée coûteuse calculée une fois par semaine et lue une fois n’a pas de bénéfice à être cachée. Une projection identique appelée par 500 requêtes par minute présente un potentiel. Le coût évité est la fréquence multipliée par le coût de calcul, diminué des écritures, invalidations et incidents de cohérence.
Signal faible. Si les optimisations changent chaque semaine de cible selon le dernier ticket, aucune baseline n’existe. Il faut d’abord instrumenter trois parcours critiques et figer une période témoin avant d’ajouter un nouveau pool.
Relier le cache à un budget de fraîcheur
Chaque donnée possède une tolérance. Le logo d’une marque peut rester ancien plusieurs heures ; un droit d’accès révoqué ne devrait pas dépendre d’un TTL long ; un stock disponible accepte parfois quelques secondes, mais pas au moment de confirmer une commande. La fraîcheur se définit par usage, pas par table.
Le budget inclut propagation, invalidation et reconstruction. Dire « TTL cinq minutes » ne garantit pas une donnée fraîche à cinq minutes si un cache HTTP, un CDN et un cache applicatif se cumulent. L’équipe dessine le chemin complet et fixe la borne de chaque couche.
Seuil local à qualifier. Un catalogue peut accepter quinze minutes sur le contenu éditorial et trente secondes sur la disponibilité, tandis qu’un changement de rôle doit être visible à la prochaine requête. Ces valeurs sont validées avec le métier et la sécurité ; elles ne constituent pas une recommandation universelle.
Choisir le bon niveau de cache
Le cache HTTP pour une représentation
Le navigateur, le proxy ou le CDN cachent une réponse selon les en-têtes et sa capacité à être partagée. Ce niveau évite même d’exécuter PHP, mais il exige de maîtriser Vary, les cookies, les réponses privées et les variations de langue. Une page personnalisée ne devient pas publique parce qu’elle ressemble au catalogue.
Le cache applicatif pour un résultat coûteux
Symfony Cache mémorise une valeur produite par le code : projection, configuration distante, permission calculée ou résultat d’agrégation. Le service qui connaît le calcul possède la clé et la stratégie de fraîcheur. Il ne cache pas automatiquement une entité Doctrine attachée à un contexte de persistance.
Le cache de données pour soulager une lecture répétée
Les caches de requêtes ou de second niveau peuvent être utiles sur des lectures stables, mais ils rapprochent invalidation et modèle de données. Une projection dédiée, indexée et simple peut coûter moins cher qu’une cache complexe. Le bon choix réduit le coût complet, pas seulement les requêtes visibles dans le profiler.
Construire des clés stables et bornées
Une clé décrit la valeur, son périmètre et sa version de schéma : produit, locale, canal et version tarifaire, par exemple. Elle n’utilise pas un objet sérialisé ou une URL entière non normalisée qui multiplie les variantes invisibles. Les dimensions sont documentées et leur cardinalité estimée.
La version dans la clé permet une invalidation globale lors d’un changement incompatible. Elle ne doit pas augmenter à chaque déploiement, sinon le cache froid devient systématique. Une version change lorsque la forme ou la signification de la valeur change, puis l’ancien espace expire naturellement.
Les identifiants sensibles ne sont pas exposés dans une clé consultable. Un hachage peut masquer la valeur, mais l’équipe doit toujours pouvoir relier une anomalie à son objet par une trace contrôlée. La clé de cache n’est pas un journal métier.
Traiter l’invalidation comme un événement métier
La question n’est pas « comment supprimer une clé ? », mais « quel fait rend la valeur ancienne ? ». Une modification de produit invalide sa fiche ; une publication de catalogue peut invalider une famille ; un changement de contrat affecte les prix d’un compte. Cette causalité appartient au cas d’usage qui confirme l’écriture.
Les tags permettent d’invalider un ensemble, à condition de limiter leur cardinalité et de comprendre le support du backend. Une purge massive après chaque petite modification recrée un cache froid permanent. Pour des groupes très larges, une version de génération peut être plus simple qu’une liste de millions de clés.
L’invalidation se produit après la transaction durable. Si elle arrive avant et que l’écriture échoue, les lecteurs reconstruisent l’ancienne valeur ; si elle n’arrive jamais après commit, la valeur reste périmée. Un événement enregistré dans la même transaction puis relayé rend cette séquence reprenable.
Expliciter cohérence et données périmées
Servir une valeur périmée pendant une panne peut préserver la consultation. Ce mode dégradé n’est acceptable que si la date de calcul, la durée maximale et les opérations interdites sont connues. Un prix indicatif peut être affiché avec confirmation ultérieure ; un droit révoqué ne doit pas être restauré par un stale illimité.
La lecture peut appliquer stale-while-revalidate : un processus sert l’ancienne valeur pendant qu’un autre renouvelle. Ce compromis réduit la latence à l’expiration, mais prolonge volontairement la fraîcheur. Il doit entrer dans le budget annoncé au métier.
Pour une décision financière, l’application peut cacher les données nécessaires au calcul tout en recalculant le verdict à l’écriture. La consultation reste rapide, la commande finale utilise une source autoritative. Cette séparation évite de faire d’une page rapide la preuve d’un montant contractuel.
Éviter l’avalanche au moment de l’expiration
Si une clé très demandée expire à midi, cent requêtes peuvent lancer le même calcul et saturer la base. Une protection de calcul, un verrou distribué ou un rafraîchissement probabiliste permet à une seule requête de régénérer pendant que les autres attendent ou servent une valeur encore acceptable.
Les mécanismes officiels du composant Cache Symfony et Cache Contracts fournissent le calcul via callback et une expiration anticipée probabiliste. Leur efficacité dépend cependant du pool partagé et du coût de callback ; un appel non borné peut toujours monopoliser les workers.
Les expirations sont étalées avec une gigue lorsque de nombreuses clés sont créées ensemble. Le déploiement ne doit pas vider tous les pools par réflexe. Un warmup cible uniquement les valeurs prévisibles dont le coût de démarrage menace l’ouverture du service.
Protéger droits et personnalisation
Une clé partagée ne contient jamais une réponse dépendant d’un utilisateur sans inclure la dimension d’autorisation pertinente. Inclure l’identifiant utilisateur partout réduit le partage et crée une cardinalité énorme. Il vaut mieux séparer la donnée commune du verdict personnel calculé ensuite.
Les rôles seuls peuvent être insuffisants si le droit dépend du compte, du site ou de la propriété de l’objet. Le contexte minimal est modélisé et testé. Lors d’une révocation, l’invalidation suit le fait métier ou la vérification finale contourne le cache.
Incident à simuler. Un gestionnaire change de filiale pendant que deux réponses sont présentes. Le test vérifie qu’aucune donnée de l’ancien périmètre n’est servie, même si le cache partagé reste chaud. La sécurité prime sur le hit ratio.
Prévoir la panne du cache
Le cache est une dépendance reconstruisible, mais son indisponibilité peut reporter toute la charge sur la base. Le mode dégradé limite la concurrence, réduit certaines fonctionnalités et protège la source. Un simple fallback « recalculer pour chaque requête » transforme une panne Redis en panne générale.
Les timeouts du backend de cache restent courts par rapport au budget de réponse. Une erreur de lecture peut autoriser un calcul borné ; une erreur d’écriture ne doit pas faire échouer une lecture métier déjà correcte. Les exceptions sont classées selon l’effet, pas masquées sans trace.
Le plan de reprise précise si le pool peut être vidé, reconstruit ou restauré. Restaurer un cache persistant ancien peut réintroduire des valeurs incompatibles. La version de clé et les tests de reconstruction rendent souvent une remise à zéro plus sûre qu’une sauvegarde.
Mesurer hit ratio, coût évité et anomalies
Le hit ratio est ventilé par pool et famille de clés. Une moyenne globale cache un pool efficace et un autre qui ne touche jamais. L’équipe suit aussi latence de lecture, durée de callback, volume, évictions, erreurs et nombre de reconstructions concurrentes.
La mesure business relie le cache au parcours : temps de page, durée d’une recherche, charge base et taux d’erreur. Une baisse de latence sans amélioration du percentile ou de la capacité n’est peut-être pas prioritaire. Le coût mémoire et le temps de diagnostic font partie du bilan.
Une hausse brutale du hit ratio après une release peut être suspecte si elle accompagne moins de variantes. Une baisse progressive peut révéler une dimension de clé devenue trop fine. Les tableaux de bord signalent ces ruptures, puis une trace échantillonnée permet d’expliquer la causalité.
Cas concret : tarification B2B
Un portail calcule un prix selon produit, compte, contrat, quantité, devise et date. Le calcul appelle trois référentiels et prend parfois 900 millisecondes. La première tentative met en cache la réponse finale par produit, puis sert par erreur un prix négocié à un autre compte dont le segment semblait identique.
La correction sépare les couches. Les données communes de produit et de devise sont cachées avec leurs événements d’invalidation. Les conditions du contrat utilisent une clé compte-contrat-version et une durée courte. Le prix final est recalculé lors de l’ajout au panier puis confirmé par la commande.
Le pilote porte sur 50 comptes, 2 000 produits et trois profils contractuels pendant deux semaines. Le go exige aucun écart de prix sur l’échantillon de référence, un percentile de consultation dans le budget local et une invalidation observée après modification de contrat. Ces seuils appartiennent au risque commercial de ce portail.
La panne du cache est provoquée en charge. Le portail limite les recalculs, sert les données éditoriales anciennes dans la borne autorisée et bloque la confirmation si le référentiel contractuel n’est pas joignable. Le mode dégradé protège la justesse avant la disponibilité complète.
Implémenter avec Cache Contracts
Le service de lecture dépend d’une interface de cache dédiée à son usage. La clé est construite dans un objet testé, le callback appelle le port de données et la valeur stockée est un DTO sérialisable. La durée et les tags viennent d’une politique nommée, pas d’un entier dispersé dans les contrôleurs.
Les pools sont séparés selon criticité, volume et politique d’éviction. Les petites configurations ne doivent pas disparaître sous l’avalanche d’images ou de résultats de recherche. Les quotas et backends sont choisis après mesure ; le nombre de pools reste assez faible pour être exploité.
L’écriture publie le fait d’invalidation après commit. Le consommateur supprime les clés ou change une version de génération, puis enregistre retard et échec. Le processus est idempotent : recevoir deux fois le même événement ne produit pas une incohérence.
Décision actionnable. Commencer par une famille de lectures coûteuses et reconstruisibles ; différer les droits et décisions financières ; refuser tout cache dont la clé, l’invalidation ou le comportement de panne n’est pas explicable en revue.
Fermer les responsabilités et le repli
Les entrées du service sont le périmètre et la version ; ses sorties sont le DTO et sa date de calcul. Les responsabilités séparent source, politique et adapter, tandis que les dépendances restent injectées. La journalisation relie miss, reconstruction et invalidation à la même clé logique.
Les seuils de fraîcheur et de latence sont configurés par famille. Le repli désactive la lecture du pool sans changer la source ; le rollback de la nouvelle politique conserve l’ancienne version de clé jusqu’à expiration. Cette mise en œuvre rend chaque changement réversible sans purge globale.
Tester fraîcheur, invalidation et concurrence
Les tests unitaires vérifient construction de clé, politique de durée et valeur produite. Les tests d’intégration utilisent le vrai adapter configuré afin de couvrir tags, sérialisation et expiration. Une horloge contrôlée évite les attentes et rend les frontières temporelles reproductibles.
La matrice teste miss, hit, expiration, donnée périmée autorisée, invalidation, backend indisponible et deux calculs concurrents. Elle vérifie le résultat et le nombre d’appels à la source. Un test de sécurité varie compte, rôle et locale pour détecter les fuites de périmètre.
La charge commence avec un cache froid, puis chaud, puis indisponible. Cette troisième phase est souvent la plus importante : elle mesure si la base et les workers survivent à la perte du raccourci. Le rollback désactive la lecture du nouveau pool sans supprimer la source.
Qualifier les cas où le cache aide
Le cache aide les lectures répétées, coûteuses et reconstruisibles dont la tolérance à la fraîcheur est connue. Il convient aux catalogues, projections, agrégats et résultats de services externes stables. Il est moins adapté aux décisions uniques, peu lues ou dont chaque milliseconde d’ancienneté change le droit.
Les équipes doivent pouvoir posséder invalidation et observabilité. Un cache ajouté par une équipe plateforme sans propriétaire métier de la fraîcheur devient une boîte noire. Le product owner arbitre la tolérance ; les développeurs possèdent clés et événements ; l’exploitation possède capacité et panne.
Si la requête lente vient d’un index absent, d’un N+1 ou d’un modèle qui charge trop de données, corriger la source reste prioritaire. Le cache vient après ou en complément, avec une justification chiffrée.
Erreurs fréquentes qui masquent la conception
- Choisir un TTL au hasard. La valeur semble rapide jusqu’au premier conflit avec le métier. Le budget de fraîcheur est défini avant la durée technique.
- Cacher des entités Doctrine. Le proxy, le contexte et les relations rendent la valeur fragile. Un DTO explicite et versionné ferme le contrat.
- Vider tout le cache au déploiement. La pointe de reconstruction frappe les sources au pire moment. Les clés sont versionnées seulement lors d’une incompatibilité.
- Inclure trop de dimensions. Chaque requête devient un miss et la mémoire dérive. La donnée commune est séparée du verdict personnalisé.
- Ignorer la panne du cache. Le fallback surcharge la base et double l’incident. La concurrence est bornée et les fonctions non essentielles réduites.
Plan d’action : déployer un pilote en six semaines
Semaines 1 et 2 : mesurer et contractualiser
L’équipe choisit trois parcours, profile leurs dépendances et estime fréquence, coût et répétition. Elle sélectionne une lecture reconstruisible dont la fraîcheur peut être décidée. Baseline, cardinalité de clés et mode dégradé sont écrits avant l’implémentation.
Elle associe chaque donnée à son propriétaire et aux faits qui la rendent ancienne. Les seuils de latence et de fraîcheur sont locaux, justifiés et observables. Les droits et opérations financières restent hors pilote si leur modèle n’est pas fermé.
Semaines 3 et 4 : implémenter et casser
Le service, le DTO, la clé et la politique sont construits. L’invalidation après commit et les métriques sont branchées. Les tests provoquent concurrence, backend indisponible, expiration groupée et changement d’autorisation.
Le pilote ouvre sur une petite cohorte. L’équipe compare valeur cachée et source, mesure coût évité et observe les reconstructions. Toute divergence inexpliquée bloque l’extension même si la latence baisse.
Semaines 5 et 6 : étendre ou retirer
La cohorte augmente seulement si fraîcheur, sécurité, capacité et comportement de panne restent dans les bornes. Le runbook est exécuté par le support ; le rollback désactive le cache sans migration de données.
La revue décide de conserver, modifier ou supprimer le pool. Elle priorise ensuite les lectures où le coût évité est prouvé, diffère les zones à forte personnalisation et refuse le cache utilisé pour éviter une correction de modèle.
- À faire d’abord : mesurer une lecture répétée et définir sa fraîcheur.
- À valider ensuite : clé, causalité d’invalidation et cardinalité.
- À tester avant le go : cache froid, concurrence et panne complète.
- À différer : toute décision dont la source autoritative reste ambiguë.
- À refuser : un gain de moyenne qui dégrade sécurité ou justesse.
Approfondir performance et architecture
Pour relier cache et budgets de service, la démarche performance, monitoring et observabilité aide à construire la baseline et les alertes.
Lorsque la lenteur vient surtout des accès persistants, l’analyse de Doctrine à grande échelle permet de corriger requêtes et unités de travail. Le choix du rendu web complète la réflexion pour les caches HTTP et la personnalisation.
Conclusion : mettre la fraîcheur sous contrat
Le cache accélère une lecture en échange d’une nouvelle responsabilité : expliquer quand la valeur devient ancienne et comment elle est reconstruite. Ce contrat vaut davantage qu’un TTL choisi par habitude.
Une conception saine sépare source de vérité, dérivé commun et décision personnelle. Les clés reflètent le périmètre, les événements portent l’invalidation et le mode dégradé protège la source au lieu de l’écraser.
Le succès se mesure sur la justesse, les percentiles, la charge évitée et la capacité de diagnostic. Un hit ratio flatteur ne compense jamais une fuite de droits ou un prix périmé.
Si votre cache Symfony accélère certains parcours mais rend les incidents opaques, Dawap peut vous accompagner pour profiler les flux, fermer la fraîcheur et intégrer une stratégie de cache dans une architecture web sur mesure fiable.