Chapitre 11.2 — Performance et montée en charge
Partie 11 : Niveau expert — Chapitre 2/8 Public : dev expérimenté ayant sécurisé son code (11.1) ; il doit maintenant le rendre rapide et tenir la charge. Version de référence : Moodle 5.2 (PHP 8.3+, OPcache, Redis, PHP-FPM).
⏱️ TL;DR
- Mesurer avant d’optimiser :
perfdebug, le profileurtool_excimer, la détection des requêtes lentes. On n’optimise pas au feeling.- Côté code :
get_recordset(streaming) vsget_records(tout en mémoire), chasse aux requêtes N+1, bon usage des caches MUC, pas d’écriture dans une boucle.- Architecture de montée en charge : load balancer → serveurs web PHP-FPM stateless → Redis (sessions + MUC) → base primaire + réplicas en lecture → moodledata partagé (NFS ou objectfs S3) → workers cron dédiés.
- OPcache obligatoire en prod ; CDN pour les assets ;
cachejs/cachetemplatesactivés.- Le cas critique : le pic d’examen (quiz à grande échelle) — le moment où tout casse si l’infra n’est pas dimensionnée.
- Monitoring : health checks, cron qui tourne, tâches en échec, requêtes lentes, métriques clés.
🎯 Objectifs
- Mesurer la performance (perfdebug, tool_excimer, requêtes lentes).
- Écrire du code efficace en base (recordsets, anti-N+1, caches).
- Comprendre l’architecture de scaling de référence.
- Anticiper le pic d’examen.
- Mettre en place un monitoring minimal.
1. Mesurer d’abord : la règle d’or
Optimiser sans mesurer, c’est deviner. Moodle fournit les outils pour savoir où le temps passe :
perfdebug(Administration → Développement → Débogage,$CFG->perfdebug = 15;) : affiche en pied de page le nombre de requêtes SQL, le temps, la mémoire, les lectures/écritures de cache. Le premier réflexe pour voir si une page fait 30 ou 3000 requêtes.tool_excimer(profileur d’échantillonnage, plugin officiel) : capture les stack traces des requêtes lentes en production avec un overhead minimal. Il révèle quelle fonction consomme, pas seulement « c’est lent ».- Log des requêtes lentes : côté base (slow query log MySQL/PostgreSQL) pour repérer les requêtes coûteuses réelles.
$CFG->debugsqltraceet les outils de la base pour tracer une requête précise.
⚠️ Piège : « optimiser » une boucle qui n’est pas le goulot pendant qu’une requête N+1 invisible mange 90 % du temps. Mesurez, identifiez le vrai goulot, optimisez-le, re-mesurez. L’intuition se trompe presque toujours sur la répartition du temps.
💡 Pour un dev React :
perfdebugest votre onglet Performance/Network ;tool_excimerest un profiler d’échantillonnage (comme un flamegraph). Même discipline que le web perf : profiler en conditions réelles, cibler le goulot, mesurer l’effet.
2. Code efficace en base de données
La plupart des lenteurs Moodle viennent de la couche base (DML, Partie 5). Trois leviers.
2.1 Recordsets vs get_records
get_records() charge tout le résultat en mémoire (un tableau). Pour de gros volumes, cela explose la mémoire. get_recordset() streame ligne par ligne :
// ❌ charge 100 000 lignes en mémoire d'un coup
$users = $DB->get_records('user', ['confirmed' => 1]);
foreach ($users as $u) { /* … */ }
// ✅ streaming : une ligne à la fois, mémoire constante
$rs = $DB->get_recordset('user', ['confirmed' => 1]);
foreach ($rs as $u) { /* … */ }
$rs->close(); // TOUJOURS fermer un recordsetRègle : get_records pour de petits ensembles bornés ; get_recordset dès que le volume est grand ou inconnu (tâches cron, exports, traitements de masse).
2.2 La chasse aux requêtes N+1
Le fléau n°1. Faire une requête par élément dans une boucle :
// ❌ N+1 : une requête utilisateur par inscription
$enrolments = $DB->get_records('user_enrolments', ['status' => 0]);
foreach ($enrolments as $e) {
$user = $DB->get_record('user', ['id' => $e->userid]); // 1 requête × N
// …
}
// ✅ 1 requête : jointure ou get_records_list
$sql = "SELECT ue.*, u.firstname, u.lastname
FROM {user_enrolments} ue
JOIN {user} u ON u.id = ue.userid
WHERE ue.status = 0";
$rows = $DB->get_records_sql($sql);Ou charger en une fois avec get_records_list('user', 'id', $userids). perfdebug révèle un N+1 par un nombre de requêtes proportionnel aux données affichées.
2.3 Les caches MUC (Muc = Moodle Universal Cache)
Pour des données coûteuses à calculer et souvent lues, utilisez le cache (Partie 5) plutôt que de recalculer :
$cache = \cache::make('mod_simplecheck', 'itemcounts');
$count = $cache->get($simplecheckid);
if ($count === false) {
$count = $DB->count_records('simplecheck_item', ['simplecheckid' => $simplecheckid]);
$cache->set($simplecheckid, $count);
}Le MUC route vers le store configuré (APCu, Redis, fichier). En prod scalée, on met les caches partagés sur Redis (§3). Attention à l’invalidation : un cache mal invalidé sert des données périmées.
⚠️ Piège : ne jamais écrire en base dans une boucle sans nécessité (
insert_record× N). Préférezinsert_records(insertion en masse) ou une transaction. Et ne mettez pas en cache ce qui change constamment (le coût d’invalidation dépasse le gain).
3. L’architecture de montée en charge
Une instance mono-serveur suffit pour des centaines d’utilisateurs. Au-delà (milliers de simultanés), on scale horizontalement. L’architecture de référence :
Les composants et le pourquoi :
- Serveurs web stateless (PHP-FPM) : plusieurs nœuds identiques derrière le load balancer. « Stateless » = aucune donnée locale à un nœud → on en ajoute/retire à volonté. Ce qui impose les points suivants.
- Redis pour sessions + MUC : les sessions ne peuvent pas vivre sur le disque local d’un nœud (l’utilisateur pourrait être routé sur un autre nœud). On les met dans Redis (partagé). Idem pour les caches partagés.
- Base primaire + réplicas en lecture : les écritures vont au primaire ; les lectures peuvent être réparties sur des réplicas (Moodle supporte la lecture sur réplicas). Soulage le primaire.
- moodledata partagé : les fichiers (uploads) doivent être accessibles de tous les nœuds → NFS partagé ou, mieux à grande échelle, objectfs (stockage objet S3-compatible).
- Workers cron dédiés : le cron (tâches planifiées, Partie 6) ne doit pas tourner sur chaque nœud web (doublons, charge). On dédie un ou des nœuds au cron.
⚠️ Piège n°1 du scaling : garder les sessions en fichier (
$CFG->session_handler_classpar défaut) avec plusieurs nœuds → un utilisateur routé sur un autre nœud perd sa session (déconnexions erratiques). Sessions sur Redis est le prérequis n°1 du multi-nœuds. Idem :moodledatalocal à un nœud casse les fichiers.
💡 Pour un dev React : c’est l’architecture 12-factor / stateless horizontal scaling que vous connaissez (Vercel/Kubernetes) : nœuds sans état, session et cache externalisés (Redis), stockage objet (S3), base avec réplicas de lecture, jobs (cron) séparés du web. Moodle scale exactement selon ces principes.
4. OPcache, CDN, réglages de prod
Quelques réglages non négociables en production :
- OPcache (extension PHP) : compile et met en cache le bytecode PHP. Sans OPcache, Moodle est plusieurs fois plus lent (il recompile 500k lignes de PHP à chaque requête). Dimensionner
opcache.memory_consumptionetopcache.max_accelerated_filesgénéreusement (Moodle a des milliers de fichiers). cachejs = true,cachetemplates = true,themedesignermode = false(Partie 8) : servir le JS/CSS/templates compilés et cachés, pas recompilés à chaque page.- CDN pour les assets statiques (CSS, JS, images du thème) : décharge les serveurs web et rapproche les fichiers des utilisateurs.
$CFG->cachedir/store MUC sur un backend rapide (APCu local + Redis partagé selon la couche).- HTTP keep-alive, gzip/brotli, HTTP/2 côté serveur web.
⚠️ Piège : laisser
themedesignermode = trueoucachejs = falseen production (oublié après du debug) fait recompiler SCSS/JS à chaque page → effondrement des performances. Vérifiez ces réglages avant toute mise en prod (Partie 8 §6).
5. Le pic d’examen : le stress-test réel
Le scénario qui casse les instances sous-dimensionnées : 500 étudiants démarrent le même quiz à la même minute. C’est le pire cas de charge de Moodle :
- Pic d’écritures (chaque réponse, chaque tentative → base) sur le primaire (les réplicas ne soulagent pas les écritures).
- Pic de sessions actives et de verrous (le quiz gère la concurrence des tentatives).
- Pic CPU (rendu des questions, notation).
Mesures d’atténuation :
- Dimensionner le primaire (le quiz écrit beaucoup) et surveiller les verrous.
- Étaler les démarrages si possible (créneaux décalés).
- Tester la charge en amont (JMeter, k6) sur le scénario quiz avant l’examen réel.
- Redis robuste (les sessions explosent), PHP-FPM avec assez de workers.
- Cron qui continue de tourner (la notation différée, les notifications en dépendent).
⚠️ Piège : compter sur les réplicas de lecture pour absorber un pic d’examen. Un quiz est write-heavy ; les réplicas n’aident pas les écritures. C’est le primaire et Redis qu’il faut dimensionner. Testez le scénario écriture réel, pas une navigation en lecture.
6. Monitoring d’une production
On ne pilote pas ce qu’on ne mesure pas. Le minimum :
- Cron : Moodle alerte si le cron n’a pas tourné récemment (Administration → Serveur → Tâches). Un cron en panne = notifications, notation différée, nettoyages arrêtés. À surveiller en priorité.
- Tâches en échec : la liste des
scheduled_task/adhoc_tasken échec (retry, erreurs). - Health checks : une URL de santé (
/lib/tests/...ou un endpoint custom) pour le load balancer. - Métriques : temps de réponse, requêtes/s, taille de la file cron, hit rate des caches, connexions base, mémoire Redis.
- Logs : erreurs PHP, requêtes lentes, événements de sécurité (11.1).
- Alerting : sur cron arrêté, base saturée, disque
moodledataplein, taux d’erreur.
💡 Pour un dev React : mêmes principes qu’un backend Node en prod (APM, health checks, alerting sur les jobs), avec une spécificité Moodle : le cron est vital (tant de fonctionnalités en dépendent). « Le cron ne tourne plus » est l’incident silencieux classique — mettez une alerte dessus en premier.
7. Synthèse
Performance = mesurer (perfdebug, tool_excimer) avant d’optimiser, coder efficace en base (recordsets, anti-N+1, caches MUC), puis scaler horizontalement (web stateless + Redis sessions/cache + réplicas de lecture + moodledata partagé + cron dédié) avec OPcache et CDN. Le juge de paix est le pic d’examen (write-heavy, à tester en amont). Et rien ne tient sans monitoring, à commencer par la surveillance du cron. Une plateforme rapide et robuste est le socle sur lequel les chapitres suivants (tests, CI) garantissent la durabilité.
✏️ Exercices
Exercice 1 — Diagnostiquer avant d’agir. Une page de rapport met 8 s à charger. Décrivez la démarche de diagnostic avant de toucher au code.
✅ Solution
(1) Activer perfdebug et recharger : combien de requêtes SQL, quel temps, quelle mémoire ? Un nombre de requêtes proportionnel aux lignes affichées signale un N+1. (2) Lancer tool_excimer pour voir quelle fonction consomme le temps (profiling). (3) Consulter le slow query log de la base pour la requête coûteuse réelle. (4) Cibler la cause identifiée (N+1, requête sans index, calcul répété), l’optimiser, re-mesurer. On n’optimise jamais au feeling ni avant d’avoir localisé le goulot.
Exercice 2 — Recordset ou get_records ?
Une tâche cron traite tous les utilisateurs (potentiellement 200 000). Le dev utilise get_records('user'). Quel problème et quel correctif ?
✅ Solution
get_records charge les 200 000 lignes en mémoire d’un coup → risque de dépassement mémoire (crash de la tâche). Correctif : get_recordset qui streame ligne par ligne (mémoire constante), avec foreach puis $rs->close(). Règle : recordset dès que le volume est grand ou inconnu (cron, exports, masse) ; get_records réservé aux petits ensembles bornés.
Exercice 3 — Sessions et scaling. Après passage à 3 serveurs web, les utilisateurs sont déconnectés au hasard. Cause et correctif ?
✅ Solution
Les sessions sont probablement stockées en fichier local à chaque nœud : quand le load balancer route un utilisateur sur un autre nœud, sa session n’y existe pas → déconnexion. Correctif : externaliser les sessions sur Redis (session handler Redis partagé) pour que tous les nœuds les partagent. C’est le prérequis n°1 du multi-nœuds (avec moodledata partagé). Les serveurs web doivent être stateless.
Exercice 4 — Pic d’examen. Un client s’inquiète pour un examen de 800 étudiants simultanés. Il propose « d’ajouter des réplicas de lecture ». Est-ce la bonne réponse ?
✅ Solution
Non, pas principalement : un quiz est write-heavy (chaque réponse écrit en base). Les réplicas de lecture n’absorbent pas les écritures ; c’est le primaire qu’il faut dimensionner (CPU, IO, gestion des verrous), ainsi que Redis (pic de sessions) et PHP-FPM (assez de workers). Il faut aussi tester la charge du scénario quiz en amont (k6/JMeter, en écriture), éventuellement étaler les démarrages, et s’assurer que le cron tourne (notation différée). Les réplicas aident les pages de lecture, pas le pic d’écriture d’un examen.
Exercice 5 — Réglage oublié.
Après un déploiement, tout le site est lent. Un dev avait activé themedesignermode pour déboguer un thème. Impact ?
✅ Solution
themedesignermode = true (et/ou cachejs = false) fait recompiler le SCSS/JS à chaque page au lieu de servir les versions cachées → chaque requête paie une compilation lourde → effondrement des performances sur tout le site. Correctif : désactiver themedesignermode, activer cachejs/cachetemplates, purger les caches, et vérifier ces réglages dans la checklist de mise en prod. (Vérifier aussi qu’OPcache est actif.)
🧠 Quiz de révision
Q1. Quels outils pour mesurer la performance, et pourquoi mesurer d’abord ?
Réponse
perfdebug (requêtes/temps/mémoire par page), tool_excimer (profiling par fonction), le slow query log de la base. Mesurer d’abord parce que l’intuition se trompe sur la répartition du temps : on cible le vrai goulot au lieu d’optimiser au hasard.Q2. Recordset vs get_records : quand utiliser chacun ?
Réponse
get_records pour de petits ensembles bornés (tout en mémoire). get_recordset dès que le volume est grand ou inconnu : il streame ligne par ligne (mémoire constante). Toujours close() un recordset.Q3. Quels composants doivent être externalisés pour scaler horizontalement ?
Réponse
moodledata) → NFS/objectfs partagé ; les lectures → réplicas (écritures au primaire) ; le cron → workers dédiés. Les serveurs web restent stateless.Q4. Pourquoi le pic d’examen est-il le cas critique et que faut-il dimensionner ?
Réponse
Q5. Quel est l’incident silencieux à surveiller en priorité en production ?
Réponse
moodledata, Redis, taux d’erreur.Chapitre suivant : 03 — Tests unitaires avec PHPUnit — le harnais PHPUnit de Moodle : initialisation, advanced_testcase et resetAfterTest(), le data generator, setUser(), tester events/tâches/messages via les sinks, dataProviders, couverture des external functions et privacy providers, dans moodle-docker.