PrestaShop 9 : corriger l’erreur « too many open files »

Guide pratique pour diagnostiquer et résoudre « too many open files » sur PrestaShop 9 : limites systemd/nginx, optimisation PHP‑FPM/MySQL et prévention via monitoring.

Capture d'écran de plusieurs écrans d'ordinateur avec l'erreur 'too many open files'.

Table des matières :

  1. Comprendre « too many open files » sur PrestaShop 9 (et pourquoi ce n’est pas un bug PrestaShop)
  2. Isoler le composant fautif : runbook de diagnostic (Debian/Ubuntu, PrestaShop 9.1, PHP‑FPM 8.3)
  3. Corriger les limites proprement : systemd, nginx, PHP‑FPM, MySQL (et éviter l’« ulimit magique »)
  4. Réduire la consommation de descripteurs dans un contexte PrestaShop 9 (le vrai fix, pas le pansement)
  5. Cas réels et prévention : monitoring FD, alertes, tests de charge, environnements dev reproductibles

Comprendre « too many open files » sur PrestaShop 9 (et pourquoi ce n’est pas un bug PrestaShop)

Sur PrestaShop 9 (Symfony 6.4), l’erreur « too many open files » n’est quasiment jamais « une erreur PrestaShop » au sens strict. C’est un symptôme d’épuisement des descripteurs de fichiers (file descriptors, FD) au niveau du processus (PHP‑FPM, nginx, mysqld, varnish, etc.) ou du système. Typiquement, ça remonte sous forme de 500 côté front/back, de logs nginx du type accept4() failed (24: Too many open files), ou de warnings PHP failed to open stream: Too many open files.

La définition est POSIX/Linux : un descripteur est un entier associé à une ressource ouverte (fichier, socket TCP, pipe, inotify, etc.). Le noyau renvoie l’erreur EMFILE quand la limite par processus est atteinte. La page de manuel est explicite : « EMFILE The per-process limit on the number of open file descriptors has been reached. » (source : man 2 open, Linux — version web : https://man7.org/linux/man-pages/man2/open.2.html). Tant que vous ne savez pas quel processus atteint sa limite, augmenter « au hasard » ulimit -n revient à masquer la cause.

À ne pas confondre :

  • EMFILE : limite par processus atteinte (cas le plus courant sur un serveur PrestaShop).
  • ENFILE : table globale des fichiers ouverts au niveau système pleine (plus rare, mais possible si plusieurs services s’emballent ou si la valeur fs.file-max est trop basse).

Sur une stack PrestaShop moderne, les FD sont consommés à plusieurs étages :

  • nginx ouvre des sockets (accept, upstream), des fichiers de logs, des fichiers statiques, et maintient des connexions keep‑alive (et en HTTP/2, un seul socket peut porter plusieurs streams mais ça reste un FD côté serveur).
  • PHP‑FPM ouvre des fichiers PHP, des sockets vers MySQL/Redis, des fichiers temporaires, des logs, parfois des streams HTTP sortants (APIs, ERP).
  • MySQL/MariaDB consomme énormément de FD via le cache de tables, les binlogs, les connexions, les fichiers de données/indices.

Ce point croise directement les sujets performance/architecture : un serveur “qui tient” au quotidien peut tomber en défaut FD le jour d’un pic (soldes, campagne TV, emailing massif) parce que la concurrence et/ou les temps de réponse augmentent, donc les ressources restent ouvertes plus longtemps. Si vous n’avez pas une vue claire des goulots, reprenez une démarche d’audit reproductible (voir l’article interne : Audit performance PrestaShop : méthode en 6 étapes reproductibles).

Isoler le composant fautif : runbook de diagnostic (Debian/Ubuntu, PrestaShop 9.1, PHP‑FPM 8.3)

Pré‑requis : accès SSH (au moins lecture /proc), idéalement root pour inspecter les services systemd. Les exemples ci‑dessous sont validés sur Debian 12, PrestaShop 9.1.x, PHP‑FPM 8.3, nginx 1.24+. Risque : lancer lsof en plein pic de charge peut être coûteux (I/O + CPU), faites‑le de préférence hors pic ou en fenêtre courte.

Commencez par lire les logs au bon endroit, sans extrapoler. Si c’est nginx qui sature, vous verrez souvent Too many open files dans /var/log/nginx/error.log. Si c’est PHP, les erreurs apparaissent dans le log FPM ou dans le log applicatif. Si vous avez un doute sur la qualité de votre configuration d’erreurs (dev/prod), alignez‑la d’abord (article interne : Gestion d’erreur PHP : bonnes pratiques et configuration développement/production).

Ensuite, mesurez qui consomme les FD, et à quel plafond il se heurte. Les commandes utiles :

# 1) Limite par processus (soft/hard) dans votre shell (utile, mais pas suffisante)
ulimit -n

# 2) Limites et consommation globales kernel
cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr  # alloué / libre / max

# 3) Limites effectives d'un process (la vérité terrain)
cat /proc/<PID>/limits | grep -E 'Max open files|Max processes'

# 4) Top des processus par nombre de FD
for pid in /proc/[0-9]*; do
  p=${pid##*/}
  c=$(ls -1 $pid/fd 2>/dev/null | wc -l)
  cmd=$(tr -d '\0' < $pid/cmdline 2>/dev/null | head -c 120)
  echo "$c $p $cmd"
done | sort -nr | head -20

# 5) Détail des FD d'un PID (précis, mais potentiellement lourd)
ls -l /proc/<PID>/fd | head

# 6) Types de fichiers/sockets ouverts (pratique pour MySQL/nginx/php-fpm)
lsof -p <PID> | head

Compléments très efficaces quand l’erreur vient de connexions réseau (cas fréquent : beaucoup de sockets open, upstream saturés, appels sortants) :

# Vue rapide des sockets TCP (tous process confondus)
ss -s

# Connexions ESTABLISHED vers MySQL (si MySQL est local:3306 ou distant)
ss -tan | awk '$1=="ESTAB"{print $4" -> "$5}' | grep ':3306' | wc -l

Mini‑scénario réaliste (utile pour “penser” l’incident) : pendant les soldes, le trafic augmente et les temps de réponse montent (cache froid, DB plus lente). Les connexions HTTP restent ouvertes plus longtemps → nginx garde davantage de sockets en cours → les workers atteignent leur limite FD → accept4() échoue → les clients voient des 502/504. Dans ce scénario, « augmenter la limite » peut aider, mais la cause racine est souvent latence + concurrence (et pas “nginx bug”).

Ce diagnostic doit être fait pendant l’incident (ou sur un environnement de staging où vous reproduisez la charge). L’article interne PrestaShop performance : monitoring, tests de charge et runbooks soldes donne une méthode propre pour capturer ce genre d’état transitoire (snapshots, fenêtres de mesure, seuils d’alerte) sans bricolage.

Corriger les limites proprement : systemd, nginx, PHP‑FPM, MySQL (et éviter l’« ulimit magique »)

Sur une distro systemd, la majorité des déploiements échouent parce que les limites sont augmentées dans un shell (ulimit -n) mais pas dans l’unité systemd. Résultat : votre terminal a bien 65535, mais php-fpm tourne toujours à une valeur plus basse (1024/4096 selon les systèmes et services). La documentation systemd le rappelle : les limites se configurent au niveau du service via LimitNOFILE= (source : documentation systemd.exec(5) sur https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html).

Avant de modifier quoi que ce soit, vérifiez l’existant (ça évite les “on a déjà mis 65535” alors que non) :

systemctl show php8.3-fpm -p LimitNOFILE
systemctl show nginx -p LimitNOFILE
systemctl show mariadb -p LimitNOFILE  # ou mysql selon votre service

Pour PHP‑FPM (exemple Debian, service php8.3-fpm) :

sudo systemctl edit php8.3-fpm

Puis :

[Service]
LimitNOFILE=65535

Rechargez :

sudo systemctl daemon-reload
sudo systemctl restart php8.3-fpm
sudo systemctl show php8.3-fpm -p LimitNOFILE

Côté nginx, il y a deux niveaux : la limite systemd et la directive nginx worker_rlimit_nofile (sinon les workers ne peuvent pas exploiter la limite). Exemple :

# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65535;

events {
  worker_connections 8192;
}

La doc officielle nginx décrit worker_rlimit_nofile et son interaction avec les limites OS (source : https://nginx.org/en/docs/ngx_core_module.html#worker_rlimit_nofile). Si vous n’alignez pas worker_connections avec worker_rlimit_nofile, vous aurez des valeurs “théoriques” sans impact.

Checklist de cohérence nginx (simple, mais elle évite 80% des faux‑fix) :

  • LimitNOFILE (systemd) ≥ worker_rlimit_nofile (nginx)
  • worker_rlimit_nofile ≥ (worker_connections + marge logs/sockets)
  • le nombre de workers (worker_processes) × worker_connections colle à votre volumétrie réelle (sinon vous sur‑dimensionnez et vous consommez inutilement)

Pour MySQL/MariaDB, l’erreur peut être au niveau serveur (Too many open files) et non de l’app. Vérifiez open_files_limit, table_open_cache, table_definition_cache :

SHOW VARIABLES LIKE 'open_files_limit';
SHOW VARIABLES LIKE 'table_open_cache';
SHOW VARIABLES LIKE 'table_definition_cache';
SHOW VARIABLES LIKE 'max_connections';

Points d’attention concrets côté DB :

  • Augmenter open_files_limit sans cohérence avec la limite systemd de mysqld ne sert à rien.
  • Augmenter table_open_cache peut augmenter fortement les FD consommés si votre schéma (ou certains modules) multiplie les tables et si la charge “balaye” beaucoup de tables.
  • Un max_connections très haut sans pooling peut aggraver (plus de connexions = plus de sockets = plus de FD), surtout si l’application ouvre trop de connexions par requête.

Si vous êtes en phase d’optimisation globale (FPM + OPcache + MySQL), l’article interne Performance PrestaShop : benchmarks et optimisation PHP‑FPM, OPCache, MySQL fournit des repères de sizing et d’interactions (latence DB ↔ concurrence FPM ↔ saturation FD).

Réduire la consommation de descripteurs dans un contexte PrestaShop 9 (le vrai fix, pas le pansement)

Augmenter les limites règle l’urgence, mais un pic de FD est souvent un signal : trop de concurrence, fuites de ressources, ou architecture de cache mal calibrée. Dans PrestaShop, les causes fréquentes sont :

  • génération d’images à la volée (front, back, import) ouvrant beaucoup de fichiers temporaires ;
  • modules qui font des appels HTTP sortants sans timeouts/keep‑alive maîtrisé (ERP, tracking, connecteurs) ;
  • logs trop verbeux (fichiers log manipulés intensivement) ;
  • sessions/fichiers temporaires en mode “files” sur un disque lent, créant une pression I/O + FD (et des requêtes plus longues = FD gardés plus longtemps).

Sur gros catalogues, la recherche et le cache peuvent aggraver le problème si chaque requête fait exploser les accès : cf. Cache PrestaShop : Varnish, Redis, Memcached et OPcache côté serveur et, si vous externalisez la recherche, PrestaShop ElasticSearch : accélérer la recherche produit sur grands catalogues.

Le point technique concret : un process PHP‑FPM garde des FD ouverts tant que la requête n’est pas terminée, et certains clients (curl multi, streams, wrappers) peuvent ouvrir plusieurs sockets/fichiers par requête. Si vous avez pm.max_children=80 et que chaque enfant peut monter à 50–80 FD (PHP code + sockets + fichiers + logs), vous dépassez rapidement 1024. Le sizing doit être fait en couple :

  • pm.max_children × (FD moyens / enfant) < < LimitNOFILE (avec une marge)

Ce n’est pas “théorique” : c’est exactement ce qui se passe lors d’un pic avec des requêtes plus longues. Utilisez un échantillonnage pendant charge :

# FD utilisés par chaque worker php-fpm (approche rapide)
pgrep -f 'php-fpm: pool' | while read pid; do
  echo -n "$pid "
  ls /proc/$pid/fd 2>/dev/null | wc -l
done | sort -k2 -nr | head

Exemple de lecture (mini‑table de décision) :

Signal observé Interprétation probable Action utile
Les workers FPM “montent” tous à un niveau FD élevé et stable concurrence/latence (pas forcément fuite) réduire concurrence (pm.max_children), accélérer DB/cache, isoler pools
Quelques PIDs ont un FD très élevé et continuent de monter fuite ressource dans un chemin de code (module, import, HTTP sortant) identifier le endpoint / module, corriger et tester
nginx a beaucoup plus de FD que FPM sockets clients en grand nombre (keep-alive long, DDoS soft, HTTP/2) ajuster nginx (timeouts, limites), vérifier CDN/WAF, worker_connections/LimitNOFILE

Si vous identifiez une fuite côté module (ex. fichiers ouverts sans fclose(), ressources curl non libérées), corrigez le code et ajoutez un garde‑fou. En pratique, le bon réflexe côté PHP est de :

  • poser des timeouts explicites sur les appels sortants (pour éviter des sockets “qui traînent”) ;
  • fermer systématiquement ce qui doit l’être (fichiers, curl handles) ;
  • encapsuler les opérations à risque dans un try/finally (même si vous ne faites pas du “RAII” strict, l’idée est la même : garantir la libération).

Pour industrialiser la qualité (détection d’odeurs, refactorings sûrs), vous pouvez vous appuyer sur des outils décrits dans PHPStan et Rector : industrialiser la qualité du code PHP.

Autre cause “PrestaShop réelle” : une sur‑consommation de FD via concurrence plus que via fuite. Exemple classique : back‑office + imports + génération de miniatures + trafic front, tous sur la même pool FPM. La remédiation propre est architecturale :

  • isoler des pools (front/back/cron) avec des pm.max_children adaptés ;
  • limiter la concurrence par type de workload (import ≠ navigation) ;
  • déporter certaines tâches en async (cron sérialisé, queue) pour lisser la charge.

Ça rejoint des sujets d’automatisation et de moindre privilège (article interne : Automatisation PrestaShop : sécurité RGPD, moindre privilège et journaux d’audit) : un cron mal conçu qui lance 20 traitements parallèles peut vous exploser la limite FD, saturer le disque temporaire, et masquer la responsabilité (« c’est PrestaShop »). C’est d’autant plus important dans un contexte RGPD : si votre incident se traduit par des erreurs massives et des transactions incomplètes, vous voulez des logs et des traces d’audit exploitables (et pas un serveur qui tombe silencieusement).

Cas réels et prévention : monitoring FD, alertes, tests de charge, environnements dev reproductibles

Un cas fréquent en prod : nginx sature avant PHP lors d’une montée de charge (soldes, campagne) parce que les keep‑alive + HTTP/2 multiplient les sockets, et que la limite FD de l’unité systemd nginx est restée basse. Symptôme : erreurs accept4() + montée rapide de 499/502, alors que PHP‑FPM semble “ok”. Fix : aligner LimitNOFILE, worker_rlimit_nofile, et dimensionner worker_connections selon votre trafic réel, pas selon un copier‑coller.

Un second cas fréquent : MySQL atteint sa limite de fichiers ouverts en même temps qu’un pic de trafic + cache froid. Vous voyez des latences, puis des erreurs applicatives en cascade. Ici, l’action n’est pas seulement “augmenter open_files_limit”. Il faut corriger la cause de la pression : schémas de requêtes (index, joins), taille du catalogue, ou modules qui font du N+1. Sur ce volet base de données, gardez une routine de maintenance (article interne : Base de données PrestaShop : routine de maintenance et nettoyage automatisé) et des optimisations ciblées (article interne : Index MySQL : optimiser WHERE, JOIN et ORDER BY avec EXPLAIN).

Enfin, la prévention sérieuse passe par des alertes sur la consommation de FD, pas uniquement CPU/RAM. Deux métriques simples à suivre :

  • la tendance globale via /proc/sys/fs/file-nr (et côté Prometheus, les métriques node_exporter correspondantes node_filefd_allocated / node_filefd_maximum) ;
  • la consommation par service (par exemple en collectant ls /proc/<PID>/fd | wc -l pour nginx/php-fpm/mysqld via un exporter dédié, ou via scripts d’alerting — l’important est d’avoir un seuil et une historique).

Couplez ça à des tests de charge et à un runbook (article interne : PrestaShop performance : monitoring, tests de charge et runbooks soldes). Et si vous devez reproduire localement un incident FD (modules, import, génération d’images), faites‑le dans un environnement isolé et reproductible plutôt que sur votre poste “à l’arrache” : DDEV : outils développeur intégrés, ddev exec/ssh et extensions d’image facilite justement les itérations sans polluer une machine.

Pour finir, gardez une synthèse “opérable” en tête (utile pour une astreinte ou une équipe qui tourne) : 1) identifier le processus qui tape la limite (nginx ? php‑fpm ? mysqld ?),
2) corriger les limites au bon niveau (systemd + config service),
3) réduire la consommation (concurrence, pools, cache, modules, appels sortants),
4) monitorer les FD comme un SLO (tendance + seuils + alerte).

Tant que vous restez au stade « on a mis 65535 et ça a l’air d’aller », l’incident reviendra au prochain pic — et en général au pire moment.


À lire aussi