Caches et indexes : optimisez (vraiment) vos performances Magento

Cette présentation est tirée de la conférence technique que j’ai donné début juin en tant que consultant de l’e-Commerce Academy, lors du Bargento 2013.

Notre expérience en terme de cabinet d’expertise sur Magento nous a montré que beaucoup d’e-Commerçants, d’intégrateurs, d’agences ont rencontré, rencontrent et rencontreront encore des problèmes et des pertes de performance sur Magento.

Si une grande majorité des dits problèmes de performance ont pour origine une mauvaise implémentation dans Magento, que ce soit à cause de mauvais développements spécifiques ou l’intégration de module(s) communautaire(s) au socle technique discutable, il demeure toutefois que Magento est une solution qui n’est pas parfaite et qui présente des faiblesses techniques. De fait, une utilisation aux limites de le plateforme peut dégrader de façon parfois exponentielle les performances.

Le but de ce billet aujourd’hui est de vous montrer à mon sens quelles sont les 2 faiblesses natives les plus graves et de vous proposer des pistes d’optimisations dans chacun des cas.

1. Scénarios typiquement problématiques en terme de performance

1.1 Quelques exemples avec les imports de produits

Prenons 3 cas d’import produits.

  1. Un import produit en utilisant le Dataflow de Magento (System > Import-Export > Dataflow depuis le backoffice)
  2. Un Import en utilisant par exemple le script ci-dessous
  3. Un import en utilisant les API de Magento (via SOAP-XmlRPC ou REST)
script-import-sauvegarde-produit

Exemple de script d’import pour la sauvegarde de produit

Ces 3 mécanismes ont en commun qu’ils sont tous très lents dans le cas d’un import de masse sur les produits (> 5000 entités), car ils font appel en boucle et pour chaque produit à l’opération save(), ce qui s’avère être une opération coûteuse. Quand je dis très lent, j’entends environ une ou deux sauvegardes de produit par seconde, soit quasiment une heure pour les 5k (ou plus).

Prenons 2 autres cas d’imports produits.

  1. Un import via le nouveau mécanisme d’import/export de produits Magento (System > Import-Export > Importer)
  2. Un import via Magmi, un outil communautaire qui se veut très performant en terme d’import produits.

Il se trouve que si sur la partie purement écriture en base des lignes produits, ils sont beaucoup plus performants que ce que nous avons cité préalablement, ces 2 mécanismes ne réindexent pas les produits dans Magento, et dès qu’on lance une ré-indexation, les ennuis peuvent alors commencer.

1.2 Conséquences sur les performances et les temps d’importation

Les conséquences d’imports extrêmement longs peuvent être rangées comme suit.

Mineures

  1. Ralentissements pour les clients
  2. Ralentissements pour les administrateurs

Majeures

  1. Durée de ré-indexation (donc des imports)
  2. Locks DB
  3. Indisponibilité serveur (web et/ou bdd)

Il est évident que dans les pires cas, notamment d’indisponibilité du site, cela peut avoir des répercussions graves sur le chiffre d’affaires ou bien l’e-réputation de l’e-Commerçant.

1.3 D’où viennent les lenteurs ?

Pour mieux comprendre et surtout visualiser, j’ai réalisé le diagramme ci-dessous qui révèle la répartition et le coût des tâches lors d’une sauvegarde de produit dans Magento.

repartition-et-cout-des-taches-sauvegarde-produit

BS et AS signifie, que le cache est nettoyé une fois dans l’appel de la méthode _beforeSave sur le produit et une fois dans l’appel de la méthode _afterSave. Cette mesure a été faite sur un Magento 1.7.0.2 avec le demo store installé sans aucun ajout de modules ou développements spécifiques.

On voit très clairement ici que les 2 éléments les plus consommateurs en terme de temps sont :

  1. Le nettoyage du cache 16%
  2. La ré-indexation 76%

2. Affiner la gestion du cache Magento

2.1 Comment fonctionne le cache sous Magento ?

Le mécanisme de cache utilisé par Magento repose sur le module Zend_Cache du Zend Framework 1 qui est utilisé comme librairie.

Il y a donc 4 méthodes d’accès courantes au cache :

  1. Save (sauvegarde de data dans le cache)
  2. Read (lecture de data dans le cache)
  3. Clean (nettoyage du cache en se basant sur des tags)
  4. Remove (nettoyage du cache en ne retirant qu’une seule entrée du cache)

D’un point de vue logique, seule la partie 3 (clean) basée sur les tags demande un peu d’explications.

En effet quand on sauvegarde de la data dans le cache, les paramètres passés sont schématisés comme suit :

Paramètres de saveCache
Data Key Lifetime Tags

Les tags permettent de grouper des clés de cache entre elles ; ainsi lorsque l’on demande un nettoyage du cache via un cleanCache, le Zend Framework va collecter toutes les entrées de cache associées au tag que l’on demande à supprimer pour ensuite nettoyer toutes les entrées de cache concernées.

association-entrée-cache-tag

Schéma d’association d’entrée de cache/tag

2.2 Problèmes liés à cette gestion native du cache

Nous l’avons vu avec le diagramme, le nettoyage du cache était appelé 2 fois lors d’une sauvegarde produits, un traitement coûteux. De plus, le nettoyage du cache est appelé dans la transaction MySQL de la sauvegarde produit, en conséquence un nettoyage lent du cache va locker l’enregistrement en base le temps, entre autres, de vider le cache concerné.

Implémentation-code-gestion-sauvegarde-produit

Code implémenté pour la gestion de sauvegarde de modèle (dont les produits), les appels au cleanCache sont pour les produits implémentés dans le _beforeSave et _afterSave

2.3 Les “backends” de cache

Les backend de caches sont les implémentations de “stockage physique” de cache où sont stockés les données. Nous allons donc les catégoriser comme suit :

Cache à 1 niveau Cache à 2 niveaux
File (par défaut) Memcached + bdd
Database Memcached + Redis
Cm_File Memcached + File
Cm_Redis Apc + (File/DB)
Eaccalerator + (File/DB)

Pourquoi 1 et 2 niveaux?

Le problème est que l’implémentation physique de certains caches (apc, memcache), ne supporte pas nativement la gestion des tags. Par conséquent, si rien n’avait été fait lorsque l’on demandait à nettoyer des clés de cache via un tag, rien ne se produirait.

Il est prouvé qu’un backend de cache memcache ou apc est bien plus rapide qu’un backend de cache file, mais pour autant sans nettoyage de cache possible, cela entrainerait à coup sûr de graves problèmes fonctionnels (fiches produits pas à jour, etc…).

La solution retenue (présente dans le Zend Framework) est d’utiliser un mécanisme de cache à 2 niveaux, c’est à dire qu’il va y avoir un second backend de cache dit ‘lent’ qui sera associé à memcache (un backend de cache “rapide”) ; un second backend de cache qui supporterait lui, la gestion des tags. Le workflow technique de nettoyage du cache serait donc le suivant :

  1. Le backend de cache “lent” va récupérer toutes les clés de cache associées au tag
  2. Pour chaque clé de cache récupérée, le backend de cache lent va supprimer une entrée dans le backend de cache “rapide”, et va lui même supprimer une entrée de cache sur son support physique

D’un point de vue fonctionnel, cela répond à la problématique posée de nettoyage du cache, d’un point de vue technique en revanche, on comprend aisément à la vue de ces opérations, qu’un nettoyage du cache puisse être lent.

2.4 Benchmark sur les différents backends de caches

Tout d’abord avant de commencer ce chapitre, je voudrais remercier Colin Mollenhour qui m’a autorisé à reprendre ses benchmarks, réalisés pour la conférence Imagine 2012.

Nous allons ici donc voir et détailler quelques courbes comparatives concernant les différents backends de cache pour les opérations reads et clean. Les opérations de write ne nous intéresseront pas tellement puisque par essence, un cache n’est pas fait pour être continuellement accédé en écriture, mais plutôt en lecture.

backend-cache

De ce graphe on retiendra :

  • Le meilleur backend de cache disposant d’1 niveau en lecture est Cm_File (backend de cache proposé par Colin Mollenhour)
  • Le backend de cache DB est à proscrire, compte tenu de ses performances nettement moins intéressantes ; ce à quoi s’ajoute le fait qu’il va davantage solliciter le serveur  MySQL de Magento.
  • Le backend de cache qui gère le mieux l’opération de nettoyage du cache, reste le backend de cache Redis
  • Les backends de cache présentés ici (excepté DB) sont capables en moyenne de gérer 15k d’opérations de lecture par seconde, ce qui est bien suffisant, compte tenu de la réalité technique d’appel au cache.
backend-cache-reads-concurrents-clients

De ce graphe on retiendra :

  • Les backends de Cache Cm_File, et File (natifs) dans une moindre mesure, à 1 niveau, disposent d’un avantage certain sur la concurrence d’accès au cache
  • En cas de forte gestion d’accès concurrent en lecture au cache, on préfèrera Memcache à Redis.
backend-cache-cleans-concurrents-clients

De ce graphe on retiendra :

  • Les meilleurs backend à 1 niveau pour nettoyer le cache sont Cm_File et Redis
  • A priori pas de différence de nettoyage de cache pour les backends à 2 niveaux Memc+Redis et  Memc+DB (tout en sachant ce qui a pu préalablement être mentionné concernant DB qui est à proscrire, cf. Graphe 1)
backend-cache-reads-keys-tags

De ce graphe on retiendra :

  • Pour un cache bien rempli et qu’on veut gérer convenablement en lecture, il conviendra d’utiliser Memcache ou Redis.
backend-cache-cleans-keys-tags

De ce graphe on retiendra :

  • Pour un cache bien rempli et qu’on veut gérer convenablement en nettoyage, il conviendra d’utiliser Memcache ou Redis.

2.5 Quel backend de cache choisir ?

A ce niveau je n’ai pas grand chose à ajouter au schéma proposé par Colin Mollenhour, qui était d’ailleurs la conclusion de sa conférence.

quel-backend-de-cache-choisir

3. Optimiser les indexes

3.1 Qu’est ce que les indexes dans Magento ?

Magento implémente nativement un modèle de données compliqué (notamment l’EAV pour les entités produit/catégorie/client/adresse de client) ; de plus certains mécanismes fonctionnels complexes (moteur de promotions) rendent la gestion de données lourde à manipuler.

C’est la raison pour laquelle Magento implémente des indexes. Notez que ceux-ci sont surtout présents pour la gestion des entités catalogue (produits et catégories). Ils permettent d’accéder aux données en lecture de façon beaucoup plus simple et rapide, ce qui a priori a du sens d’un point de vue e-Commerce puisque l’on accède (surtout pour le catalogue) majoritairement à ceux-ci en lecture et non en écriture.

Cependant, en écriture cela rend beaucoup plus lourdes les opérations puisqu’il faut non seulement écrire dans le modèle de base de Magento mais également mettre à jour les indexes ; le soucis étant que si cela n’est pas fait, et bien nativement les données catalogue affichées (si elles le sont) ne seront plus à jour.

Vous trouverez ici les indexes principaux utilisés dans Magento avec une description courte pour chacun d’eux :

  Code Index/Table Description courte
  • catalog_product_attribute
  • catalog_product_index_eav
Contient les valeurs des attributs par produit de type liste déroulante, utilisée pour la navigation à facettes.
  • catalog_product_price
  • catalog_product_index_price
Contient les valeurs de prix finaux/min/max de tous les produits à la date du jour.
  • catalog_product_flat
  • catalog_product_flat_[store_id]
Contient les valeurs de certains attributs produits représentés sous forme de modèle plat : ce qui n’est utilisé en lecture que sur le scope frontend.
  • catalog_category_flat
  • catalog_category_flat_store_[store_id]
Même remarque que pour l’entité produit.
  • catalog_category_product
  • catalog_category_product_index
Construit les associations catégories/produits en tenant compte des associations produits/website + les propriétés ‘anchor’ + rootcategory.
  • catalog_url
  • core_url_rewrite
Construit les réecritures d’urls des produits/catégories
  • cataloginventory_stock
  • cataloginventory_stock_status
Combinaison des données (qty+status) de la table cataloginventory_stock_item +  association produit/website
  • catalogsearch_fulltext
  • catalogsearch_fulltext
Association produits ids/concaténation des attributs searchable.

3.2 Pourquoi est-ce long de ré-indexer ?

Les mécanismes qui rendent les ré-indexations coûteuses dans Magento sont :

  • Parfois la reconstruction de table à la volée (ou ajout/suppression de colonnes) pour les tables flat.
  • Combinatoire pouvant aller jusqu’à nombre de produits nombre de store views nombre de customer groups (en général, la combinatoire reste nombre de produits x nombre de store views.
  • Les indexes peuvent locker ou devoir attendre qu’un lock se libère avant de pouvoir s’exécuter.

Par ailleurs, il faut savoir que Magento adopte nativement (hors version EE 1.13) une stratégie du tout ou rien en ce qui concerne les indexes, soit :

  • On indexe les données relatives au produit lors d’un save
  • On ré-indexe tous les produits pour un même index, ou pour tous les indexes

Il est donc impossible de ne ré-indexer qu’un sous ensemble de produits, ce qui d’un point de vue logique et performance serait pourtant plus raisonnable. Magento n’implémente pas de mécanisme de ré-indexation partielle.

3.3 Problèmes occasionnés par les réindexations

Voici une liste technique de problèmes pouvant survenir lors d’une ré-indexation

  1. Locks de tables et d’enregistrements
  2. Ralentissements liés à des slowqueries
  3. Deadlocks possibles

3.4 Les solutions de l’éditeur

Sous la version Magento 1.13 EE sont implémentés les mécanismes suivants :

  • Un système de trigger qui valorise les tables de changelog d’index (ne contient que les ids des entités à ré-indexer)
  • Un CRON qui parcourt les tables de changelog et ré-indexe les entités en fonction des ids qui s’y trouvent.
  • La structure des tables d’indexes a été revue et optimisée pour accélérer les traitements (plus de contraintes de clés étrangères, de table en enginememory….)

Cependant cela peut soulever quelques questions :

  • Cet automatisme permet-il de désactiver les indexes que l’on ne veut pas indexer ?
  • Peut-on définir un ordre de priorité d’exécution ?
  • Comment s’opère la gestion du rafraîchissement du cache ?

Les résultats présentés par l’éditeur lors de la conférence Imagine 2013 à ce sujet sont vraiment encourageants.

reindexation-magento

3.5 Les solutions de la communauté

Plusieurs solutions ont été apportées par la communauté Magento, mais celle-ci ne sont pas toutes complètes. Je pense pour ma part qu’il faut considérer le problème sous 3 angles.

3.5.1 Ré-indexation partielle par sous-ensemble de produits

L’objectif serait de ne ré-indexer que les produits mis à jour, dans le cas où l’on réalise un import d’un sous ensemble de produits, et non plus de l’intégralité du catalogue comme c’est le cas.

Un module réalisé par l’e-Commerce Academy fait cela, voici quelques screenshots :

parametrage-instance-reindexation

Paramétrage des instances de ré-indexation dans le BO permettant d’associer des indexers a une instance.

reindexation-project-product

Lancement de l’instance reindex_project_product configurée ci-dessus

Voici un benchmark réalisé à ce sujet :

Volumes : 30k produit/10 catégories
Reindex All ~ 30min
Reindex partiel (10%) < 5min

3.5.2 Ré-indexation partielle de produits par propriété utile

L’objectif ici serait de ne ré-indexer que les produits pertinents, quand Magento par défaut ré-indexe tous les produits, y compris ceux qui sont désactivés ou non visibles dans le catalogue. Un patch proposé par l’agence D’nd permet de faire cela. Attention toutefois, celui-ci ne fonctionne que sur l’index core_url_rewrite. De plus, le patch D’nd permet d’exclure toutes les écritures de catégories dans les urls.

Voici un benchmark réalisé par D’nd à ce sujet :

benchmark-reindexation-partielle

3.5.3 Ré-indexation partielle impactant seulement les propriétés modifiées

On ne ré-indexe ici que les entités dont il est pertinent de mettre à jour l’index concerné en fonction des propriétés modifiées, quelques idées :

  • On ne met à jour l’index des prix que si l’on modifie le prix du produits (à nuancer)
  • On ne met à jour l’index des urls que si l’on modifie l’url du produit (à nuancer)
  • On ne met à jour le flat catalog produit qu’en modifiant une valeur d’un attribut « utilisé dans le listing produit »

4. En conclusion, faites le tri et soyez rigoureux

4.1 Pour optimiser le cache

  • Penser aux backends Cm File ou Redis
  • Rester pertinent sur ce qui est mis en cache (il n’est pas nécessaire de forcément tout cacher)

 4.2 Pour optimiser les ré-indexations

  • Préférer les développements spécifiques pour une gestion plus fine des indexes (module e-Commerce Academy)
  • Suivre les bonnes pratiques (pas trop de stores, bon usage des groupes clients)
  • Magento E.E 1.13

Il reste une autre option pour espérer et obtenir de meilleures performances pour tout ceci, attendre la sortie prochaine de Magento 2, mais qui n’est pas prévue avant fin 2014.

Restez donc vigilants sur ces points et sur la qualité de vos développements, et vous n’aurez pas ou peu de problèmes de performance sur Magento !

6 commentaires pour “Caches et indexes : optimisez (vraiment) vos performances Magento

    • Bonjour Sylvain,

      Merci pour ce tips, je ne connaissais pas ce module et effectivement non il à l’air de faire mieux en terme d’import produit que Magento (ce n’est pas bien difficile), mais en plus concernant le sujet sensible, ici la réindexation, c’est parfaitement géré dans : AvS_FastSimpleImport_Model_Import_Entity_Product::_reindexUpdatedProducts

      A priori ce module remplacera parfaitement le mécanisme d’import de Magento.

      Cordialement,
      Matthieu

      • Bonjour Mathieu,

        Effectivement il gère partiellement la réindexation notamment grâce à sa méthode public Mage::getSingleton(‘fastsimpleimport/import’)->setPartialIndexing(true);

        Cependant je le déconseille de l’utiliser sur une CE ou une EE < 1.13 car le temps d'import augmenterai de manière assez significative, du moins c'est ce que j'ai vécu :-)

        Bien cordialement
        Sylvain

  1. merci pour ce sujet très utile
    je voulais savoir s’il était possible de se procurer le module réalisé pour la ré-indexation partielle par sous-ensemble de produits?

    • Bonjour Chebbi,

      Peu ou prou, le code que tu trouveras dans AvS_FastSimpleImport_Model_Import_Entity_Product::_reindexUpdatedProducts (voir commentaire du dessus), correspond à ce que j’avais implémenté dans le module.

      Cordialement,
      Matthieu

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *