Ça arrive toujours au pire moment : une petite VM, un pic de trafic soudain, quelques requêtes lentes, et puis la base de données disparaît comme si elle avait mieux à faire. Vous vous connectez et voyez la scène : le tueur OOM de Linux a éliminé mysqld. Encore.
Quand quelqu’un dit « augmentez simplement max_connections pour que les utilisateurs n’aient plus d’erreurs », il vous demande généralement d’échanger une panne visible (trop de connexions) contre une panne invisible (explosion mémoire), et la panne invisible gagne en écrasant le processus. Cet article explique comment rendre cet échange explicite, le dimensionner correctement et ne pas se faire surprendre.
Ce que signifie réellement max_connections (et pourquoi c’est une promesse mémoire)
max_connections n’est pas un réglage de performance. C’est un réglage de risque.
En apparence, c’est le nombre maximum de connexions clientes simultanées que le serveur autorisera. Par temps calme, vous pourriez penser que c’est juste de la concurrence : plus de connexions signifie plus d’utilisateurs. En production, il s’agit du nombre de threads gourmands en mémoire que votre base accepte d’héberger en même temps avant d’épuiser la RAM et de se faire tuer.
Chaque connexion peut entraîner des allocations par session et par thread. Certaines sont minuscules. D’autres sont étonnamment importantes. Certaines ne sont allouées que si nécessaire (tris, jointures, tables temporaires). Le problème est que « si nécessaire » arrive précisément quand le système est déjà sous tension : les requêtes lentes s’accumulent, les connexions s’entassent, et ces allocations coûteuses se produisent quand vous en avez le moins envie.
Le modèle mental correct est donc :
- Mémoire globale (partagée par tous) : pool de buffer InnoDB, buffers de log, caches, structures internes.
- Baseline par connexion : pile du thread, buffers réseau, structures de session.
- Pics par requête : buffers de tri, buffers de jointure, mémoire de tables temporaires, buffers de lecture.
Si vous réglez max_connections trop haut sur un petit serveur, vous avez effectivement promis au noyau : « Je peux lancer jusqu’à N threads, et chacun peut effectuer un travail gourmand en mémoire en même temps. » Le noyau vous croit. Le tueur OOM a une opinion différente.
Une petite blague, parce qu’il en faut une : Augmenter max_connections pour corriger « too many connections » revient à acheter une plus grosse poubelle pour éteindre un feu de cuisine. Ça traite le symptôme visible et ignore la chaleur.
Faits et contexte intéressants (parce que ce débat a une histoire)
- Le modèle thread-per-connection de MySQL est ancien et robuste. Il évolue étonnamment bien avec de bons schémas de charge, mais il transforme les « trop de connexions inactives » en problème de mémoire et d’ordonnancement.
- MariaDB a forké MySQL en 2009 après l’ère Sun/Oracle ; la compatibilité est restée élevée, mais les valeurs par défaut et les fonctionnalités ont divergé.
- MySQL 5.6 a popularisé l’ère du « gros pool de buffer InnoDB » ; avant cela, beaucoup de déploiements dépendaient trop du cache OS et de MyISAM. Cela a changé la culture du dimensionnement mémoire.
- InnoDB est devenu moteur par défaut depuis MySQL 5.5. Cette décision a augmenté l’importance du dimensionnement mémoire global et réduit la tolérance aux « réglages aléatoires ».
- Performance Schema (MySQL) et l’instrumentation similaire ont changé la façon d’observer mémoire et attentes—si vous l’activez et l’utilisez réellement.
- Le comportement du tueur OOM Linux n’est pas « aléatoire », mais il est contre-intuitif : il choisit des victimes selon un score de nuisance, l’usage mémoire et des contraintes. Les bases de données sont volumineuses, donc elles sont des cibles attractives.
- « Le swap rend les bases lentes » est devenu une légende ; mais en réalité, « pas de swap sur une petite VM » transforme souvent un pic récupérable en kill de processus.
- Le pooling de connexions s’est généralisé dans les années 2010 pour les stacks web parce que les bases thread-per-connection n’aiment pas des milliers de sessions TCP majoritairement inactives.
MySQL vs MariaDB : différences importantes pour le risque OOM
Si vous cherchez une réponse simple du type « MySQL utilise moins de mémoire » ou « MariaDB est plus sûr », vous n’en trouverez pas. Les deux peuvent être stables ou catastrophiques. La différence tient aux pièges présents et aux valeurs par défaut que vous avez héritées.
Valeurs par défaut et packaging : votre vraie base, c’est « MySQL + choix de la distribution »
Sur petits serveurs, le facteur de risque principal n’est pas le fork ; ce sont la configuration packagée et ce que votre équipe a copié d’un blog de 2016.
Schémas courants :
- Les paquets MariaDB livrent souvent des valeurs par défaut conservatrices pour petites instances, mais votre image cloud peut les remplacer par des préréglages « utiles ».
- MySQL sur plateformes managées active parfois de l’instrumentation ou des caches supplémentaires qui augmentent la mémoire de base.
- Les deux peuvent être mal configurés si on surdimensionne les buffers par thread puis qu’on augmente
max_connections.
Gestion des threads et limites de connexion
Les deux utilisent typiquement un thread par connexion cliente. Les frais d’ordonnancement des threads et l’impact mémoire augmentent avec le nombre de connexions.
Où l’on se brûle :
- Un
max_connectionsélevé avec unwait_timeoutlong crée une grande population de sessions inactives. Inactif n’est pas gratuit. - Un fort churn de connexions (connexion/déconnexion par requête) gaspille du CPU et parfois fragmente la mémoire, donnant l’apparence de « fuites ».
- Le thread cache peut aider, mais il peut aussi conserver beaucoup de ressources thread si on le laisse trop gros.
Différences d’observabilité mémoire
Performance Schema de MySQL s’est amélioré pour détailler l’usage mémoire dans les versions modernes, mais ce n’est pas automatiquement « peu coûteux ». MariaDB a sa propre instrumentation et ses variables d’état ; la différence pratique, c’est : quel outil votre équipe utilise réellement.
Pour prévenir les OOM, le gagnant est le système qui :
- vous permet d’estimer les pics par connexion et par requête,
- vous montre la distribution actuelle des états de connexion (Sleep vs actif),
- vous aide à trouver les requêtes lentes qui causent l’accumulation,
- rend facile l’application de limites raisonnables.
La vraie différence de fork qui compte : on ne peut pas supposer que les knobs correspondent 1:1
La plupart des clés de configuration sont similaires, mais pas identiques selon versions et forks. Vous ne pouvez pas appliquer sans contrôle un guide d’optimisation écrit pour « MySQL » sans vérifier quel serveur vous exécutez. Certaines variables existent dans les deux mais ont des comportements ou valeurs par défaut différentes. La mémoire ne pardonne pas ; une petite erreur suffit.
Guide de diagnostic rapide
Voici l’ordre que j’utilise quand un petit serveur vacille et que tout le monde crie.
1) Confirmer qu’il s’agit d’un OOM et identifier le schéma de kill
- Vérifiez les logs du noyau pour des entrées du tueur OOM mentionnant
mysqld. - Décidez si vous avez un événement de pic unique (requête mauvaise) ou un surengagement chronique (trop de connexions + buffers surdimensionnés).
2) Mesurer l’état courant des connexions et la cause de l’accumulation
- Combien de connexions sont actives vs en sommeil ?
- Les threads sont-ils bloqués en « Sending data », « Copying to tmp table », « Sorting result », ou en attente de verrous ?
- Avez-vous un « thundering herd » causé par une mauvaise configuration de pool ?
3) Calculer rapidement la « pire mémoire crédible »
- Global : pool de buffer InnoDB + buffers de log + autres caches globaux.
- Baseline par connexion : pile du thread + buffers réseau + session.
- Pics : buffers de tri/jointure et tables temporaires pour les requêtes actives.
4) Appliquer un plafond stabilisant
- Baissez
max_connectionsà une valeur que vous pouvez vous permettre aujourd’hui. - Réduisez les buffers par thread à des valeurs sensées si quelqu’un les a gonflés.
- Raccourcissez les timeouts pour que les sessions inactives ne campent pas indéfiniment.
- Implémentez ou corrigez le pooling de connexions côté application.
5) Corriger la cause racine : requêtes lentes et contention sur les verrous
- Trouvez les plus gros contrevenants et faites en sorte qu’ils arrêtent d’occuper des ressources.
- Ajoutez des index, réécrivez des requêtes, réduisez la taille des résultats, ou découpez le travail.
Si vous faites les étapes 1–4, vous arrêtez l’hémorragie. L’étape 5 empêche la récidive.
Le modèle mémoire : buffers globaux, buffers par thread et la trappe
Les bases OOM sur petits serveurs parce que les gens raisonnent sur la mémoire comme si c’était un seul bouton. Ce n’est pas le cas. C’est un tas de boutons, et certains se multiplient par le nombre de connexions.
Mémoire globale : ce qui est toujours présent
Sur les systèmes dominés par InnoDB (qui sont la majorité), le gros poste est :
innodb_buffer_pool_size: typiquement le plus grand consommateur. Excellent pour la performance. Fatal quand surdimensionné.
D’autres consommateurs globaux ou majoritairement globaux incluent :
innodb_log_buffer_sizeet autres allocations internes InnoDB,- divers caches (cache de tables, etc.),
- la surcharge d’instrumentation si activée à grande échelle.
La mémoire globale est au moins prévisible. Vous la réglez, elle reste en grande partie stable.
Baseline par connexion : le multiplicateur silencieux
Chaque connexion a un coût fixe. L’empreinte exacte dépend de la version, de la build et du workload, mais le comportement multiplicateur ne change pas : plus de sessions signifie plus de mémoire et plus d’ordonnancement.
Contributeurs clés :
thread_stack: alloué par thread.- Buffers réseau et structures par session.
- Effets du thread cache : il peut garder des ressources thread vivantes après la déconnexion.
Pics par requête : où surviennent les OOM pendant les « tempêtes de requêtes lentes »
Les allocations en pics sont les vraies méchantes. Les classiques :
sort_buffer_size: utilisé pour les tris non couverts par des index.join_buffer_size: utilisé pour des jointures quand les index ne sont pas efficaces.read_buffer_sizeetread_rnd_buffer_sizepour certains patterns d’accès.- Tables temporaires : peuvent utiliser de la mémoire jusqu’à une limite, puis déverser sur disque.
Voici la trappe : ces buffers sont souvent alloués par thread par opération. Si vous avez 200 connexions actives effectuant des tris simultanément, même des tailles de buffer modestes peuvent devenir catastrophiques.
Pourquoi « j’avais de la mémoire libre hier » ne veut pas dire que vous êtes en sécurité
Les événements OOM sont généralement corrélés à des pics de concurrence et à la latence en queue. Une requête lente augmente son temps d’exécution, ce qui augmente le nombre de requêtes concurrentes, ce qui augmente l’utilisation mémoire, ce qui ralentit encore. Vous obtenez une boucle de rétroaction qui s’achève soit quand la charge diminue… soit quand le noyau supprime votre base.
Seconde petite blague, parce qu’on en a droit exactement à deux : Le tueur OOM est votre collègue SRE le plus décisif : il ne sombre pas dans le bikeshedding, il supprime simplement le plus gros processus et va déjeuner.
Une citation opérationnelle
Idée paraphrasée de John Allspaw : « Dans les systèmes complexes, les incidents proviennent de l’interaction d’un travail normal, pas d’une seule pièce cassée. »
Cela compte parce que l’OOM sur petits serveurs est rarement « une fuite ». C’est mémoire normale + connexions normales + un truc lent, tous en même temps.
Bien dimensionner max_connections sur petits serveurs
Voici un conseil tranché : sur petits serveurs, vous devriez préférer moins de connexions maintenues, réchauffées et multiplexées par un pool, plutôt que « laisser tout le monde se connecter directement ».
Étape 1 : choisir un budget mémoire pour MySQL/MariaDB
Sur une petite VM, vous ne possédez pas toute la machine. Le système a besoin du cache de pages, le gestionnaire de services a besoin d’espace, et si vous avez des apps co-localisées, elles ont besoin de place aussi.
Une approche pratique :
- Décidez combien de RAM vous pouvez donner au processus base sous pression. Pas « disponible », mais « sûr ».
- Réservez une marge pour les pics, le cache du système de fichiers et l’imprévu.
Si vous fonctionnez sans swap, soyez encore plus conservateur. La différence entre « lent » et « mort » est parfois 512Mo.
Étape 2 : calculer votre baseline globale
Commencez par innodb_buffer_pool_size. Ajoutez les autres allocations globales. Ne vous prenez pas trop la tête avec les plus petites ; laissez simplement une marge.
Étape 3 : estimer le coût par connexion (baseline + pic crédible)
Sur petits serveurs, vous n’avez pas besoin du nombre parfait. Vous avez besoin d’une borne supérieure conservatrice.
Décomposez en deux catégories :
- Baseline par connexion : pile du thread + session + buffers réseau. Supposez quelques Mo par connexion à moins d’avoir des preuves contraires.
- Surcharge par requête active : pour la fraction de connexions pouvant exécuter en même temps des opérations gourmandes en mémoire, supposez que des buffers de tri/jointure puissent être utilisés.
Cette dernière partie est clé : toutes les sessions ne sont pas actives. Si vous avez 300 connexions mais seulement 20 vraiment actives en même temps, dimensionnez pour 300 baselines + 20 pics. Si vous avez 300 actives parce que l’app fait du travail synchrone et que tout est lent, vous êtes déjà en danger et max_connections ne devrait pas être 300.
Étape 4 : régler max_connections à la valeur que vous pouvez réellement vous permettre
Sur un petit serveur, la valeur sûre par défaut se situe souvent dans la plage 50–200, selon la charge et les buffers. Si vous êtes tenté de la mettre à 1000 « au cas où », vous traitez la base comme une file de messages. Ce n’est pas une file de messages.
Décidez aussi si vous avez besoin d’un slot admin réservé :
max_connectionsplafonne le total.super_read_onlyet les patterns d’accès admin varient, mais la pratique générale consiste à garder de la place pour un accès d’urgence et l’automatisation.
Étape 5 : corriger le pattern de connexion côté application
Si vous avez beaucoup de connexions de courte durée, vous serez frappé par le surcoût handshake/auth et le churn de threads. Utilisez un pool. Gardez-le modeste. Fixez un plafond par instance d’app. Puis scalez horizontalement si nécessaire.
Bonus : le pooling rend souvent la latence tail meilleure parce que vous éliminez les « tempêtes de connexion » qui surviennent pendant des pannes partielles.
Tâches pratiques : 14 commandes qui vous disent quoi faire ensuite
Voici des tâches réelles à lancer sur un serveur Linux. Chacune contient : une commande, une sortie typique, ce que cela signifie et la décision que vous prenez.
Task 1: confirm OOM killer hit mysqld
cr0x@server:~$ sudo journalctl -k -b | egrep -i 'oom-killer|out of memory|killed process|mysqld' | tail -n 20
Dec 29 09:12:01 server kernel: Out of memory: Killed process 1423 (mysqld) total-vm:3187420kB, anon-rss:1456820kB, file-rss:0kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0
Dec 29 09:12:01 server kernel: oom_reaper: reaped process 1423 (mysqld), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Ce que cela signifie : Il s’agit d’un kill au niveau noyau, pas d’un plantage applicatif. Votre BD a demandé de la mémoire ; le noyau a dit non.
Décision : Traitez d’abord comme un problème de capacité/marges mémoire, pas comme un « bug MySQL ». On ne peut pas régler un processus mort.
Task 2: check system memory and swap posture
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 3.8Gi 3.4Gi 120Mi 52Mi 310Mi 140Mi
Swap: 0B 0B 0B
Ce que cela signifie : Vous fonctionnez avec presque aucune marge et sans swap. Tout pic devient un kill.
Décision : Ajoutez du swap (même petit) ou réduisez immédiatement l’empreinte mémoire de la BD. De préférence les deux.
Task 3: see the mysqld process RSS and threads (when it’s up)
cr0x@server:~$ ps -o pid,cmd,rss,vsz,nlwp --sort=-rss -C mysqld
PID CMD RSS VSZ NLWP
2189 /usr/sbin/mysqld 1684200 3278800 198
Ce que cela signifie : RSS est la mémoire réelle utilisée. NLWP vous donne un indice sur l’échelle des threads/connexions.
Décision : Si NLWP gonfle avec le nombre de connexions, vous devez du pooling/timeouts et réduire max_connections.
Task 4: check current max_connections and connection usage
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW GLOBAL STATUS LIKE 'Threads_connected';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 600 |
+-----------------+-------+
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 412 |
+----------------------+-------+
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 389 |
+-------------------+-------+
Ce que cela signifie : Vous avez autorisé 600 et vous êtes activement proche de 400. Sur une petite VM, c’est généralement mauvais signe.
Décision : Calez max_connections plus près de ce que vous pouvez vous permettre (et réduisez la concurrence effective via du pooling).
Task 5: break down “Sleep” vs active connections
cr0x@server:~$ mysql -N -e "SELECT COMMAND, COUNT(*) FROM information_schema.PROCESSLIST GROUP BY COMMAND ORDER BY COUNT(*) DESC;"
Sleep 340
Query 28
Connect 3
Binlog Dump 1
Ce que cela signifie : La plupart des connexions sont inactives. C’est souvent un problème de dimensionnement de pool applicatif ou des timeouts trop longs.
Décision : Abaissez wait_timeout et corrigez la taille max du pool par instance de service. Ne « résolvez » pas cela en augmentant max_connections.
Task 6: identify what active queries are doing (state distribution)
cr0x@server:~$ mysql -N -e "SELECT STATE, COUNT(*) FROM information_schema.PROCESSLIST WHERE COMMAND='Query' GROUP BY STATE ORDER BY COUNT(*) DESC;"
Sending data 11
Copying to tmp table 6
Sorting result 4
Waiting for table metadata lock 3
executing 2
Ce que cela signifie : Des tables temporaires et des tris sont en cours. Ce sont les endroits où les pics mémoire se produisent souvent.
Décision : Recherchez les requêtes fautives et réduisez le besoin de tris/tables temporaires (index, réécriture de requêtes), et resserrez les buffers par thread.
Task 7: find the top longest-running queries currently executing
cr0x@server:~$ mysql -e "SELECT ID,USER,HOST,DB,TIME,STATE,LEFT(INFO,120) AS INFO FROM information_schema.PROCESSLIST WHERE COMMAND='Query' ORDER BY TIME DESC LIMIT 10;"
+------+-------+-----------------+------+------|-------------------------------+----------------------------------------------------------+
| ID | USER | HOST | DB | TIME | STATE | INFO |
+------+-------+-----------------+------+------|-------------------------------+----------------------------------------------------------+
| 7712 | app | 10.0.2.15:53342 | prod | 188 | Copying to tmp table | SELECT ... ORDER BY created_at DESC LIMIT 1000 |
| 7761 | app | 10.0.2.15:53388 | prod | 163 | Sorting result | SELECT ... JOIN ... WHERE ... |
| 7799 | app | 10.0.2.18:40210 | prod | 142 | Waiting for table metadata lock | ALTER TABLE ... |
+------+-------+-----------------+------+------|-------------------------------+----------------------------------------------------------+
Ce que cela signifie : La « tempête » est visible. Vous avez des exécutions longues et même du DDL en attente de verrous.
Décision : Envisagez de tuer les pires (avec précaution), et séparez les modifications de schéma en ligne des heures de pointe. Corrigez les patterns de requêtes après stabilisation.
Task 8: check memory-sensitive per-thread variables
cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('sort_buffer_size','join_buffer_size','read_buffer_size','read_rnd_buffer_size','thread_stack','tmp_table_size','max_heap_table_size');"
+--------------------+----------+
| Variable_name | Value |
+--------------------+----------+
| join_buffer_size | 4194304 |
| max_heap_table_size| 67108864 |
| read_buffer_size | 2097152 |
| read_rnd_buffer_size| 4194304 |
| sort_buffer_size | 8388608 |
| thread_stack | 262144 |
| tmp_table_size | 67108864 |
+--------------------+----------+
Ce que cela signifie : Quelqu’un a gonflé les buffers. Avec une forte concurrence, c’est une recette OOM.
Décision : Réduisez-les à des valeurs conservatrices sauf preuve du contraire. Les gros buffers aident une requête isolée ; ils nuisent à la stabilité globale.
Task 9: check InnoDB buffer pool size vs RAM
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 3221225472 |
+-------------------------+------------+
Ce que cela signifie : C’est 3GiB. Sur un hôte ~4GiB, cela ne laisse presque rien pour les connexions, les tables temporaires et l’OS.
Décision : Baissez le pool sur les petites machines (souvent 50–70% de la RAM selon la cohabitation et le swap). Réévaluez ensuite le hit rate et la performance.
Task 10: check whether tmp tables are spilling to disk (performance + memory clues)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+---------+
| Variable_name | Value |
+-------------------------+---------+
| Created_tmp_disk_tables | 183492 |
| Created_tmp_files | 2281 |
| Created_tmp_tables | 512044 |
+-------------------------+---------+
Ce que cela signifie : Un nombre significatif de tables temporaires ont été déversées sur disque. Ça peut ralentir les requêtes, provoquant des accumulations ; la mémoire peut quand même piquer avant le déversement.
Décision : Corrigez d’abord les requêtes/index. N’augmentez pas aveuglément tmp_table_size/max_heap_table_size sur un petit serveur ; cela augmente le risque de pic mémoire.
Task 11: check thread cache and thread creation churn
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'thread_cache_size'; SHOW GLOBAL STATUS LIKE 'Threads_created'; SHOW GLOBAL STATUS LIKE 'Connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 50 |
+-------------------+-------+
+-----------------+--------+
| Variable_name | Value |
+-----------------+--------+
| Threads_created | 924812 |
+-----------------+--------+
+--------------+---------+
| Variable_name| Value |
+--------------+---------+
| Connections | 985004 |
+--------------+---------+
Ce que cela signifie : Si Threads_created augmente rapidement par rapport à Connections, vous avez du churn de connexion et un cache insuffisant.
Décision : Préférez corriger l’app pour réutiliser les connexions. Ajustez thread_cache_size modestement, mais ne le laissez pas masquer un mauvais pattern de connexion.
Task 12: check timeouts that control idle-connection hoarding
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout';"
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| wait_timeout | 28800 |
+---------------+--------+
+-------------------+--------+
| Variable_name | Value |
+-------------------+--------+
| interactive_timeout| 28800 |
+-------------------+--------+
Ce que cela signifie : Huit heures. Sur de petits serveurs avec des apps actives, c’est une raison courante d’accumulation de connexions Sleep.
Décision : Réduisez les timeouts (pour les utilisateurs non interactifs) et faites respecter le dimensionnement du pool. Si vous avez vraiment besoin de connexions longue durée, il vous faut une machine plus grosse ou moins de connexions.
Task 13: check open files and table cache pressure (secondary symptom)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'table_open_cache'; SHOW GLOBAL STATUS LIKE 'Open_tables'; SHOW GLOBAL STATUS LIKE 'Opened_tables';"
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| table_open_cache | 4000 |
+------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Open_tables | 3980 |
+---------------+-------+
+----------------+---------+
| Variable_name | Value |
+----------------+---------+
| Opened_tables | 784920 |
+----------------+---------+
Ce que cela signifie : Si Open_tables est souvent proche de table_open_cache et que Opened_tables augmente vite, vous ouvrez des tables en churn. Cela ajoute de la latence et peut indirectement augmenter la concurrence des connexions.
Décision : Ajustez le cache de tables et les limites de fichiers OS, mais ne confondez pas ça avec le pilote principal d’un OOM. Corrigez d’abord les multiplicateurs mémoire.
Task 14: verify systemd limits and cgroup memory constraints
cr0x@server:~$ systemctl show mysql -p MemoryMax -p TasksMax -p LimitNOFILE
MemoryMax=infinity
TasksMax=4915
LimitNOFILE=1048576
Ce que cela signifie : Vous n’êtes pas limité par systemd ici. Si MemoryMax avait une valeur, la BD pourrait être tuée par des contraintes de cgroup plutôt que par l’OOM du système.
Décision : Si vous exécutez dans des conteneurs ou des cgroups stricts, dimensionnez pour cette limite, pas pour la RAM de l’hôte, et envisagez de régler oom_score_adj et des réservations mémoire avec soin.
Trois mini-récits d’entreprise tirés des tranchées OOM
Mini-récit n°1 : l’incident causé par une mauvaise hypothèse
Ils avaient un petit environnement de production pour un outil « non critique ». L’outil est devenu critique dès que le reporting de paie en a dépendu. La base vivait sur une VM 4Go, et elle avait été « OK pendant des mois ». Cette phrase est toujours la mise en place.
Un développeur a remarqué des erreurs occasionnelles Too many connections pendant une fenêtre chargée et a modifié la config : max_connections est passé de quelques centaines à mille. L’hypothèse était simple : plus de connexions = moins d’erreurs. Personne n’a vérifié les mathématiques mémoire. Personne n’a demandé ce que faisaient les connexions en sommeil.
Le lundi suivant, une requête de rapport anodine a ralenti à cause d’un index manquant. Les requêtes se sont entassées, l’app a ouvert plus de sessions, et la BD a obéi car on lui avait donné la permission. La mémoire a grimpé régulièrement. Puis le noyau a tué mysqld. L’outil est tombé, le reporting de paie a échoué, et beaucoup ont compris ce que veut dire « rayon d’impact ».
Le postmortem n’a pas été spectaculaire. Il était pire : ennuyeux. « Nous avons supposé que les erreurs de limite de connexion étaient la cause, pas le symptôme. » Ils ont corrigé la requête, mis en place un pool avec un plafond strict, réduit max_connections et diminué les timeouts. La même VM a survécu aux pics futurs avec la même RAM. La différence : la base n’était plus volontairement utilisée comme balle anti-stress pour l’app.
Mini-récit n°2 : l’optimisation qui a mal tourné
Une équipe cherchait à réduire la latence sur une API publique. Ils avaient lu qu’augmenter sort_buffer_size et join_buffer_size pouvait accélérer des requêtes complexes. Ils ont augmenté les deux sensiblement, testé avec un benchmark mono-utilisateur, et ça avait l’air super. Le changement est passé.
En production avec de la concurrence réelle, c’était une autre histoire. Plusieurs workers applicatifs exécutaient des requêtes de reporting similaires en même temps, chacun allouant de plus grands buffers. La consommation mémoire n’a pas seulement augmenté—elle a monté en escaliers quand la concurrence augmentait. Ils n’ont pas heurté un mur de performance ; ils ont heurté le tueur OOM.
Le plus pénible était la nature intermittente du crash. Parfois le pattern de charge alignait les buffers alloués simultanément ; parfois non. Ça faisait ressembler le cas à un « crash aléatoire » ou à une « fuite ». Ils ont passé des jours à regarder des graphes et à blâmer le stockage.
La correction a été humiliante : revenir à des tailles de buffers proches des valeurs par défaut, ajouter un index qui éliminait le tri, et limiter la concurrence côté app (pool) pour éviter les stampedes de requêtes lourdes. La performance globale s’est améliorée parce que la BD a cessé de se débattre et l’app a arrêté de relancer. La morale n’était pas « ne jamais tuner les buffers ». C’était « ne jamais tuner des buffers par thread avec un benchmark mono-thread et s’en contenter ».
Mini-récit n°3 : la pratique ennuyeuse mais correcte qui a sauvé la mise
Une autre organisation gérait de nombreuses petites instances MySQL/MariaDB, chacune attachée à un service. Rien de sophistiqué. L’astuce était le processus : chaque instance avait une « feuille de capacité » légère versionnée dans le repo à côté de la config. Elle listait la RAM, la taille du pool de buffer, les hypothèses sur les buffers par thread et le plafond de connexions sûr. Ce n’était pas parfait. C’était cohérent.
Une nuit, un pic de trafic a frappé un service de faible priorité partageant un nœud avec d’autres choses. Le pool du service a mal fonctionné et a essayé d’ouvrir beaucoup plus de connexions que d’habitude. La base a refusé une fois max_connections atteint, et l’app a commencé à échouer des requêtes. Ennuyeux, visible et récupérable.
Parce que la base n’a pas OOM, les autres services sur l’hôte sont restés vivants. L’on-call a eu le temps d’intervenir comme un humain : réduire la taille du pool, redémarrer quelques workers, et déployer un correctif de config. Pas de récupération de système de fichiers. Pas de boucles de crash. Pas d’état corrompu.
C’était le but : un refus propre à la frontière BD est souvent préférable à laisser le système d’exploitation tirer sur la base. La pratique ennuyeuse—limites documentées et plafonds conservateurs—a transformé un incident multi-service potentiel en simple accroc pour un service.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom: “Too many connections” errors, followed by OOM after you raise max_connections
Cause racine : Accumulation de connexions due à des requêtes lentes ou un pool défaillant ; augmenter max_connections augmente l’exposition mémoire.
Correction : Gardez max_connections dans le budget mémoire. Implémentez du pooling avec un plafond strict. Trouvez et corrigez la requête lente ou la contention de verrous qui provoque l’accumulation.
2) Symptom: Hundreds of sleeping connections; memory still high
Cause racine : wait_timeout long plus pools surdimensionnés, plus overhead thread/session.
Correction : Abaissez wait_timeout (pour les utilisateurs applicatifs), définissez un max de pool par instance et réduisez les connexions inactives. Si vous avez besoin de nombreuses sessions longues, redimensionnez le serveur.
3) Symptom: OOM happens only during reporting jobs or end-of-month batch
Cause racine : Concurrence de requêtes gourmandes en mémoire (tris, jointures, tables temporaires) déclenchant des pics par thread.
Correction : Optimisez requêtes et index pour réduire tris/tables temporaires, plafonnez la concurrence pour ces jobs, et gardez les buffers par thread conservateurs. Envisagez d’exécuter les lots sur une réplique séparée.
4) Symptom: OOM happens after “performance tuning” with big buffers
Cause racine : Les tailles de buffers par thread multipliées par les sessions actives dépassent la RAM.
Correction : Revenez à des tailles sensées ; mesurez avec une concurrence proche de la production. Préférez corriger les requêtes/index plutôt que gonfler les buffers.
5) Symptom: DB restarts in a loop; logs show nothing obvious
Cause racine : Tué par l’OOM avant de pouvoir écrire des logs utiles, ou tué par les limites cgroup/container.
Correction : Vérifiez les logs du noyau et les limites cgroup. Réduisez l’empreinte mémoire et/ou augmentez la limite mémoire du conteneur. Ajoutez du swap si approprié.
6) Symptom: Memory usage grows slowly over days; occasional crashes
Cause racine : Souvent pas une fuite, mais un changement de charge : plus de sessions concurrentes, ensembles de résultats plus grands, plus d’usage de tables temporaires, ou fragmentation.
Correction : Suivez la distribution du nombre de connexions, les logs de requêtes lentes et les métriques de tables temporaires. Appliquez des plafonds de concurrence stricts et corrigez les facteurs de charge.
Listes de contrôle / plan pas à pas
Stabiliser maintenant (même jour)
- Confirmer l’OOM killer via les logs du noyau. Si c’est un OOM, cessez les débats.
- Réduire max_connections à un nombre sûr basé sur la RAM et les tailles de buffers actuelles. Oui, les utilisateurs verront des erreurs. Mieux des erreurs contrôlées que la récupération de données.
- Réduire les buffers par thread s’ils avaient été gonflés (
sort_buffer_size,join_buffer_size, buffers de lecture). - Abaisser wait_timeout pour éviter l’accumulation de connexions inactives.
- Activer ou vérifier le pooling de connexions et définir des plafonds stricts par instance.
- Tuer ou replanifier les pires requêtes seulement si vous comprenez le rayon d’impact (les requêtes de reporting sont généralement sûres à arrêter ; les migrations/DDL sont plus délicates).
Rendre robuste (cette semaine)
- Capturer des métriques de référence : max used connections, répartition actif vs sleep, tables temporaires, nombre de requêtes lentes.
- Activer le log des requêtes lentes avec un seuil raisonnable pour votre charge et revoir les pires requêtes.
- Ajouter les indexes manquants pour les requêtes qui provoquent tris et tables temporaires.
- Définir un « contrat de capacité » dans la config : taille documentée du pool de buffer, buffers par thread et plafond de connexions sûr.
- Décider du swap : un petit swap sur de petites machines augmente souvent la survivabilité. Surveillez les swap-in ; ne le laissez pas devenir permanent.
Prévenir la récidive (ce trimestre)
- Tester la charge avec une concurrence proche de la production, pas un benchmark mono-connexion.
- Séparer OLTP et reporting : répliques, instances dédiées, ou au moins limites de concurrence sur le reporting.
- Automatiser des garde-fous : alertes sur Threads_connected, Max_used_connections proche du max, et pression mémoire.
- Revoir les paramètres de pool avec les équipes applicatives comme vous révisez des règles de pare-feu : explicitement et régulièrement.
FAQ
1) Dois-je régler max_connections au nombre de threads de l’app ?
Non. Réglez les maxima des pools applicatifs pour que la somme de tous les pools reste en dessous d’un plafond sûr, avec une marge pour l’admin et les tâches batch.
2) MariaDB est-elle plus sûre que MySQL sur la mémoire ?
Pas intrinsèquement. Les deux peuvent OOM de la même manière : trop de threads et trop de mémoire par thread. La sécurité vient du dimensionnement et des limites, pas du logo.
3) Pourquoi la BD OOM alors que la plupart des connexions sont en sommeil ?
Les connexions en sommeil consomment toujours de la mémoire par session et des threads. De plus, la population « en sommeil » peut masquer un petit nombre de connexions actives faisant un travail gourmand qui déclenche le pic.
4) Si je baisse max_connections, je n’aurai pas plus d’erreurs ?
Oui, mais ce sont des erreurs contrôlées. Une base plafonnée échoue vite et de façon prévisible. Une base tuée par l’OOM fait tout échouer, y compris les tâches de récupération, et peut déclencher des relances en cascade.
5) Ajouter du swap est-ce une bonne idée pour les bases ?
Sur petites machines, souvent oui—si c’est fait intentionnellement. Un petit swap peut absorber des pics courts et éviter les kills OOM. Si vous observez du swap-in soutenu en charge normale, vous êtes sous-provisionné ou mal configuré.
6) Quelles variables sont les plus grandes « multiplicatrices mémoire par connexion » à surveiller ?
Les coupables courants sont sort_buffer_size, join_buffer_size, et les limites de tables temporaires (tmp_table_size/max_heap_table_size). Elles ne sont pas toujours entièrement allouées, mais elles définissent le pire comportement en cas de concurrence.
7) Quelle est la meilleure solution pour « trop de connexions » : pooling ou augmenter max_connections ?
Le pooling, presque toujours. Augmenter max_connections est parfois approprié après avoir fait les calculs mémoire et prouvé qu’il y a de la marge, mais ce n’est pas le premier geste sur un petit matériel.
8) Comment savoir si les requêtes lentes causent l’accumulation de connexions ?
Regardez le TIME/STATE du processlist, les logs de requêtes lentes, et si les requêtes actives ciblent des tables temporaires, des tris ou des attentes de verrous. L’augmentation du nombre de connexions avec la latence est le signe révélateur.
9) Dois-je réduire innodb_buffer_pool_size pour arrêter l’OOM ?
Si le pool de buffer écrase tout le reste sur un petit serveur, oui. La performance peut baisser, mais une base stable est plus rapide qu’une base morte. Corrigez ensuite les requêtes et la charge pour récupérer la performance.
10) Puis-je « mettre la base en conteneur » pour résoudre ça ?
Les conteneurs ne résolvent pas les maths mémoire. Ils les font respecter. Si vous définissez une limite mémoire basse, vous obtiendrez des kills plus rapides à moins de dimensionner aussi les buffers et plafonds de connexion pour cette limite.
Conclusion : prochaines étapes réalisables cette semaine
Si votre petit serveur plante, arrêtez de considérer max_connections comme un bouton de satisfaction client. C’est un engagement mémoire. Faites-en un engagement conservateur.
Faites ces étapes dans l’ordre :
- Prouvez que c’est un OOM avec les logs du noyau et mesurez RSS/threads quand la BD est active.
- Redimensionnez la mémoire globale (en particulier
innodb_buffer_pool_size) pour que l’OS et les connexions aient de l’air. - Plafonnez max_connections à ce que le serveur peut réellement supporter, pas à ce que votre app peut spammer.
- Gardez les buffers par thread conservateurs sauf si vous avez des preuves solides et des tests en vraie concurrence.
- Corrigez le pattern de connexion avec du pooling et des timeouts sensés. La plupart des « problèmes de capacité BD » sont en réalité des problèmes de comportement applicatif déguisés en base de données.
- Éliminez les requêtes lentes qui provoquent des accumulations et des temp-table storms. Moins de requêtes concurrentes est la mise à niveau mémoire la moins chère.
Quand le prochain pic arrivera, vous voulez que la base dise « non » tôt et clairement, plutôt que de se faire abattre par le noyau. Ce n’est pas du pessimisme. C’est de l’exploitation.