Il est 02:13. Votre API est « up » mais les utilisateurs regardent des spinners comme si c’était une application de méditation. Puis les logs donnent la chute : ERROR 1040 (08004): Too many connections. MySQL n’a pas planté. Il a juste arrêté de laisser entrer qui que ce soit.
La solution tentante est d’augmenter max_connections à un nombre héroïque et considérer le problème résolu. C’est ainsi que vous transformez un problème de connexion en problème de mémoire, puis en problème de pagination, puis en problème existentiel de carrière. Réglons ça correctement sur Ubuntu 24.04 : trouvez le vrai goulot, arrêtez l’hémorragie et augmentez la concurrence seulement là où c’est sûr — sans ralentir la base de données.
Ce que signifie réellement « trop de connexions » (et ce que ça ne signifie pas)
MySQL lève l’erreur « trop de connexions » lorsqu’il ne peut pas accepter une nouvelle connexion client parce qu’il a atteint un plafond interne. Le plus souvent, ce plafond est max_connections. Parfois c’est un autre mur : limites descripteurs de fichiers de l’OS, limites du service systemd, ou épuisement de ressources qui rend MySQL effectivement incapable de créer plus de sessions.
Voici ce que les gens manquent : « trop de connexions » est rarement causé par « trop de trafic » au sens simpliste. C’est généralement causé par des connexions qui ne se libèrent pas assez vite. Cela peut être des requêtes lentes, des transactions verrouillées, une CPU surchargée, un I/O saturé, un buffer pool mal dimensionné provoquant des lectures disque constantes, ou du code applicatif qui fuit des connexions. La base vous dit qu’elle se noie dans la concurrence. Votre travail est de déterminer si l’eau vient de trop de robinets ouverts ou d’un drain bouché.
Un principe qui vous fera gagner du temps : augmenter max_connections n’est justifié qu’après avoir des preuves que le serveur a de la marge (mémoire, CPU, I/O) et que l’application utilise les connexions de manière responsable (pooling, timeouts, transactions raisonnables). L’augmenter à l’aveugle transforme souvent une erreur brutale en misère lente.
Blague #1 (courte, pertinente) : Une fuite de connexion MySQL, c’est comme un robinet de cuisine qui goutte — personne ne s’en préoccupe jusqu’à ce que la facture d’eau commence à appeler votre portable.
Playbook de diagnostic rapide : contrôles premier/second/troisième
Voici la séquence « je suis de garde, mon café est froid, donnez-moi un signal ». Faites-la dans l’ordre. Chaque étape réduit rapidement la classe du goulot.
Premier : fuite de connexions ou goulot de débit ?
- Vérifiez les threads et leurs états actuels. Si la plupart sont
Sleep, vous avez probablement des problèmes de pooling côté app ou des timeouts d’inactivité trop longs. Si la plupart sontQueryouLocked, c’est un problème de débit/verrouillage. - Vérifiez si le nombre de connexions augmente. Une montée régulière suggère une fuite. Un pic soudain suggère un afflux de trafic ou une tempête de retries.
Second : qu’est-ce qui bloque le progrès — CPU, I/O, verrous ou mémoire ?
- CPU saturée + beaucoup de requêtes en cours : trop de parallélisme, mauvais plans de requête ou index manquants.
- I/O disque saturé + latence de lecture élevée : buffer pool trop petit, I/O aléatoire à cause d’index pauvres, ou stockage lent.
- Les verrous/attentes dominent : transactions longues, lignes chaudes, ou conception de schéma provoquant de la contention.
- Pression mémoire/swap : MySQL autorise trop de buffers par connexion, trop de connexions, ou l’OS effectue un reclaim agressif.
Troisième : atteignez-vous une limite qui n’est pas max_connections ?
- Descripteurs de fichiers trop bas : impossibilité d’ouvrir plus de sockets ou de fichiers.
- Limites systemd : la configuration du service
LimitNOFILEpeut écraser ce que vous pensez avoir défini. - Backlog réseau : débordement de la file SYN sous rafales (moins fréquent, mais se manifeste par des timeouts de connexion plutôt que 1040).
À la fin de ce playbook, vous devriez pouvoir formuler une phrase commençant par : « Les connexions s’accumulent parce que… ». Si vous ne le pouvez pas, vous faites une supposition. Et devinez quoi : les suppositions transforment les « correctifs rapides » en architecture.
Faits et contexte intéressants (pourquoi ça revient)
- Fait 1 : MySQL a historiquement utilisé un modèle « un thread par connexion » par défaut, ce qui rendait
max_connectionsun proxy direct du nombre de threads et de la pression mémoire. - Fait 2 : De nombreux buffers par connexion (
sort_buffer_size,join_buffer_size,read_buffer_size) sont alloués par session et peuvent faire exploser la mémoire lors d’un pic de concurrence. - Fait 3 : Le
wait_timeoutpar défaut est traditionnellement assez long pour que des sessions inactives traînent pendant des heures dans des apps mal poolées. - Fait 4 : L’effet « thundering herd » — beaucoup de clients qui retentent en même temps — transforme un petit incident en tempête de connexions. On le voit souvent avec des middlewares HTTP qui réessaient agressivement.
- Fait 5 : Le buffer pool d’InnoDB a été introduit pour réduire l’I/O disque en mettant en cache les pages ; un buffer pool sous-dimensionné ressemble souvent à « trop de connexions » parce que les requêtes bloquent et les sessions s’accumulent.
- Fait 6 : Le
thread_cache_sizede MySQL compte plus que ce que l’on croit : créer/détruire des threads sur des charges en rafales ajoute de la latence et du CPU. - Fait 7 : Les limites Linux (ulimits, descripteurs de fichiers) sont aussi anciennes que l’Unix multi-utilisateur ; elles mordent encore aujourd’hui les bases de données car les valeurs par défaut sont conservatrices.
- Fait 8 : Sur des distributions basées sur systemd (dont Ubuntu 24.04), les limites au niveau du service peuvent silencieusement écraser les réglages de ulimit du shell, ce qui embrouille même les opérateurs expérimentés.
- Fait 9 : « Augmenter max_connections » est un anti-pattern célèbre car cela peut augmenter la contention et réduire les taux de cache hit, rendant chaque requête plus lente.
Une idée paraphrasée qui vaut la peine d’être gardée sur votre mur, attribuée à John Ousterhout : paraphrased idea: Complexity is the tax you pay later; the best systems keep the fast path simple.
Le modèle de concurrence : connexions, threads, et pourquoi « plus » peut être plus lent
Pensez à un serveur MySQL comme à un restaurant qui place des clients (connexions) et leur assigne un serveur (thread). Si vous ajoutez plus de tables sans ajouter de capacité en cuisine, vous n’obtenez pas plus de plats : seulement plus de personnes qui attendent, plus de bruit et un service pire.
Quand vous augmentez max_connections, vous ne laissez pas seulement entrer plus de clients. Vous autorisez plus de travail concurrent. Cela change :
- Empreinte mémoire : chaque session consomme de la mémoire de base, plus des buffers par connexion, plus des tables temporaires, plus des structures de locks metadata.
- Ordonnancement CPU : plus de threads signifie plus de context switches. Sur une CPU saturée, cela réduit le travail utile.
- Contention sur les verrous : plus de transactions concurrentes augmentent le temps passé à attendre les verrous, surtout sur des tables/lignes chaudes.
- Profondeur de file d’I/O : plus de lectures/écritures en attente peuvent dépasser ce que votre stockage peut gérer, augmentant la latence pour tout le monde.
L’objectif n’est pas « accepter des connexions infinies ». L’objectif est « maintenir des temps de réponse stables pendant les pics ». Cela signifie souvent contrôler la concurrence : pooler les connexions côté app, limiter le max dans le pool, utiliser des transactions courtes et tuner MySQL pour qu’il soutienne la charge réelle.
Blague #2 (courte, pertinente) : La façon la plus simple de gérer 10 000 connexions MySQL est de ne pas avoir 10 000 connexions MySQL.
Tâches pratiques (commandes, sortie attendue, et décisions)
Ce sont des vérifications de niveau production. Exécutez-les pendant un incident ou dans une fenêtre de maintenance calme. Chaque tâche inclut : commande, ce que signifie une sortie typique, et la décision à prendre.
Task 1: Confirm the exact error rate and where it’s coming from
cr0x@server:~$ sudo journalctl -u mysql --since "30 min ago" | tail -n 30
Dec 29 02:08:11 db1 mysqld[1327]: [Warning] Aborted connection 18933 to db: 'app' user: 'appuser' host: '10.0.2.41' (Got an error reading communication packets)
Dec 29 02:08:13 db1 mysqld[1327]: [Note] Too many connections
Dec 29 02:08:14 db1 mysqld[1327]: [Note] Too many connections
Ce que cela signifie : MySQL journalise une pression sur les connexions, plus des connexions abortées qui peuvent indiquer des problèmes réseau, des timeouts côté client, ou une surcharge serveur.
Décision : Si vous voyez beaucoup de lignes « Too many connections », mesurez les sessions actuelles et leurs états. Si vous voyez surtout « Aborted connection », vérifiez le réseau, les timeouts clients, et la saturation CPU/I/O du serveur.
Task 2: Check configured and effective connection limits
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'max_connections'; SHOW STATUS LIKE 'Max_used_connections';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 300 |
+-----------------+-------+
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 298 |
+----------------------+-------+
Ce que cela signifie : Vous tapez contre le plafond. Ce n’est pas théorique ; vous l’atteignez.
Décision : N’augmentez pas le plafond tout de suite. Trouvez d’abord pourquoi les sessions ne se terminent pas. Si Max_used_connections est bien en dessous de max_connections, votre erreur peut venir d’une autre limite (FDs/systemd) ou d’une couche proxy.
Task 3: See what connections are doing right now
cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST;" | head -n 25
Id User Host db Command Time State Info
19401 appuser 10.0.2.41:53312 app Sleep 412 NULL
19408 appuser 10.0.2.45:58921 app Query 18 Sending data SELECT ...
19412 appuser 10.0.2.44:51220 app Query 18 Waiting for table metadata lock ALTER TABLE ...
19420 appuser 10.0.2.47:60011 app Sleep 399 NULL
Ce que cela signifie : Vous avez un mélange : beaucoup de sessions en Sleep de longue durée (pooling ou fuites applicatives) et quelques sessions actives bloquées sur des metadata locks.
Décision : Si la majorité est Sleep avec des durées longues, réduisez les timeouts d’inactivité et corrigez le pooling. Si beaucoup attendent des verrous de metadata, vous avez une modification de schéma ou une transaction longue qui bloque les autres — traitez le blocage.
Task 4: Count sessions by state (fast signal)
cr0x@server:~$ sudo mysql -NBe "SELECT COMMAND, STATE, COUNT(*) c FROM information_schema.PROCESSLIST GROUP BY COMMAND, STATE ORDER BY c DESC LIMIT 15;"
Sleep 221
Query Sending data 36
Query Waiting for table metadata lock 18
Query Sorting result 6
Ce que cela signifie : Votre « problème de connexions » est surtout des sessions inactives plus une vraie situation de contention sur les verrous.
Décision : Attaquez l’accaparement des sessions inactives (timeouts + dimensionnement des pools) et débloquez le metadata lock (terminer/kill DDL, éviter DDL en pic).
Task 5: Identify the metadata lock blocker
cr0x@server:~$ sudo mysql -e "SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, THREAD_ID FROM performance_schema.metadata_locks WHERE LOCK_STATUS='PENDING' LIMIT 10;"
+---------------+-------------+-----------+-------------+----------+
| OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE | LOCK_STATUS | THREAD_ID|
+---------------+-------------+-----------+-------------+----------+
| app | orders | EXCLUSIVE | PENDING | 8421 |
+---------------+-------------+-----------+-------------+----------+
cr0x@server:~$ sudo mysql -e "SELECT * FROM performance_schema.threads WHERE THREAD_ID=8421\G" | head -n 20
*************************** 1. row ***************************
THREAD_ID: 8421
NAME: thread/sql/one_connection
PROCESSLIST_ID: 19412
PROCESSLIST_USER: appuser
PROCESSLIST_HOST: 10.0.2.44:51220
PROCESSLIST_DB: app
PROCESSLIST_COMMAND: Query
PROCESSLIST_TIME: 18
Ce que cela signifie : Vous avez du DDL nécessitant un verrou de metadata exclusif, et d’autres sessions peuvent détenir des verrous conflictuels.
Décision : Trouvez qui détient le verrou (verrous accordés sur le même objet), et décidez d’attendre, tuer le blocant ou reporter le DDL. En production, « reporter le DDL » est souvent la décision mature.
Task 6: Check for long transactions that keep locks alive
cr0x@server:~$ sudo mysql -e "SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 5\G" | sed -n '1,40p'
*************************** 1. row ***************************
trx_id: 42119291
trx_started: 2025-12-29 01:58:03
trx_mysql_thread_id: 19321
trx_query: UPDATE orders SET status='paid' WHERE ...
Ce que cela signifie : Une transaction est ouverte depuis ~10 minutes. C’est une éternité en OLTP. Elle peut détenir des verrous de ligne et empêcher le purge, provoquant des blocages en cascade.
Décision : Si c’est accidentel (worker bloqué, mauvais chemin applicatif), tuez-la et corrigez la portée des transactions dans l’application. Si c’est intentionnel (job batch), planifiez-le hors pic ou refactorez en commits par lots.
Task 7: Check whether you’re CPU-bound
cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.8.0-xx-generic (db1) 12/29/2025 _x86_64_ (16 CPU)
02:12:10 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
02:12:11 PM all 78.2 0.0 9.1 1.3 0.0 0.8 0.0 0.0 0.0 10.6
Ce que cela signifie : La CPU est fortement utilisée. Si cela reste saturé, ajouter des connexions augmentera généralement la latence, pas le débit.
Décision : Si vous êtes lié par la CPU, priorisez l’optimisation des requêtes et la limitation de la concurrence (caps de pool) plutôt que d’augmenter max_connections.
Task 8: Check for disk pressure and latency
cr0x@server:~$ iostat -xz 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
62.11 0.00 7.40 7.55 0.00 22.94
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await aqu-sz %util
nvme0n1 920.0 48200.0 12.0 1.3 9.80 52.4 240.0 19320.0 14.20 6.12 96.3
Ce que cela signifie : Le stockage est proche de la saturation (%util ~96%), avec des temps d’attente notables. Les requêtes vont bloquer et les connexions vont s’accumuler.
Décision : Si vous êtes lié par l’I/O, augmentez le buffer pool (si la mémoire le permet), réduisez les lectures aléatoires via des index, et évitez d’augmenter la concurrence jusqu’à amélioration de la latence.
Task 9: Check for swapping (the silent performance killer)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 64Gi 58Gi 1.2Gi 612Mi 4.8Gi 2.1Gi
Swap: 8.0Gi 2.9Gi 5.1Gi
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa
12 3 3024896 128992 98112 3621120 64 120 980 410 9200 18000 72 8 10 10
Ce que cela signifie : Vous swappez (si/so), ce qui va faire exploser la latence des requêtes et causer des arriérés de connexions.
Décision : Réduisez l’utilisation mémoire (souvent en abaissant les buffers par connexion et/ou max_connections), et redimensionnez le buffer pool intelligemment. Le swap + « augmenter max_connections » est une auto-sabotage.
Task 10: Verify OS file descriptor limits for MySQL (systemd matters)
cr0x@server:~$ sudo systemctl show mysql -p LimitNOFILE -p LimitNPROC
LimitNOFILE=1048576
LimitNPROC=15238
cr0x@server:~$ sudo cat /proc/$(pidof mysqld)/limits | egrep "Max open files|Max processes"
Max open files 1048576 1048576 files
Max processes 15238 15238 processes
Ce que cela signifie : Votre service a des limites FD élevées. Si c’était bas (comme 1024/4096), cela pourrait se faire passer pour un problème de connexions ou causer des échecs étranges sous charge.
Décision : Si c’est bas, créez un override systemd pour mysql afin d’augmenter la valeur. Si c’est déjà élevé, ne blâmez pas ulimit ; passez à autre chose.
Task 11: Check table cache pressure (can drive latency and stalls)
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'table_open_cache'; SHOW STATUS LIKE 'Opened_tables'; SHOW STATUS LIKE 'Open_tables';"
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| table_open_cache | 4000 |
+------------------+--------+
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| Opened_tables | 812349 |
+---------------+--------+
+-------------+------+
| Variable_name | Value |
+-------------+------+
| Open_tables | 3998 |
+-------------+------+
Ce que cela signifie : Opened_tables est énorme et continue d’augmenter, tandis que Open_tables est proche du maximum du cache. MySQL ouvre constamment des tables — surcoût sous charge.
Décision : Augmentez table_open_cache si la mémoire le permet, et vérifiez que vous avez suffisamment de descripteurs de fichiers. C’est un problème « petites causes, grosse hémorragie ».
Task 12: Find the worst query patterns that hold connections open
cr0x@server:~$ sudo mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1000000000000 AS total_s, AVG_TIMER_WAIT/1000000000000 AS avg_s FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 5\G" | sed -n '1,80p'
*************************** 1. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 1203912
total_s: 84231.1134
avg_s: 0.0699
Ce que cela signifie : Vous avez un temps total élevé lié à quelques motifs de requêtes. Même si la latence moyenne semble « correcte », le temps total indique une charge lourde et une occupation des connexions.
Décision : Optimisez les statements digest chauds (index, index couvrants, réduire les colonnes sélectionnées, meilleures pratiques de pagination). Cela réduit le temps par connexion et règle la cause.
Task 13: Check thread cache effectiveness (cheap win for spiky workloads)
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'thread_cache_size'; SHOW STATUS LIKE 'Threads_created'; SHOW STATUS LIKE 'Connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 8 |
+-------------------+-------+
+-----------------+--------+
| Variable_name | Value |
+-----------------+--------+
| Threads_created | 238912 |
+-----------------+--------+
+---------------+---------+
| Variable_name | Value |
+---------------+---------+
| Connections | 9823812 |
+---------------+---------+
Ce que cela signifie : Cache trop petit et beaucoup de threads créés suggèrent un surcoût dû au churn de threads.
Décision : Augmentez thread_cache_size pour réduire le surcoût CPU pendant les pics. Cela ne ralentit pas les requêtes ; ça réduit le surcoût autour d’elles.
Task 14: Confirm buffer pool sizing and hit rate signals
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 8589934592 |
+-------------------------+------------+
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| Innodb_buffer_pool_reads| 832912311 |
+-------------------------+-----------+
+----------------------------------+------------+
| Variable_name | Value |
+----------------------------------+------------+
| Innodb_buffer_pool_read_requests | 3412981123 |
+----------------------------------+------------+
Ce que cela signifie : Beaucoup de lectures physiques par rapport aux requêtes suggère un cache qui ne suit pas (signal grossier ; ne sacralisez pas un ratio).
Décision : Si vous avez de la mémoire libre et que vous êtes lié par l’I/O, augmentez le buffer pool. Si vous swappez, faites l’inverse : réduisez d’abord d’autres consommateurs de mémoire.
Corrections qui ne ralentissent pas la base
Voici la hiérarchie pratique : corrigez le comportement de connexion de l’application, corrigez le temps des requêtes, corrigez le comportement des verrous, puis dimensionnez les limites MySQL pour correspondre à la réalité. Si vous faites l’inverse, vous masquez le problème jusqu’à ce qu’il revienne plus fort.
1) Corriger l’accaparement des connexions : pool, caps et timeouts
À faire : Utilisez un pool de connexions par instance d’app, fixez un cap strict, et appliquez des timeouts raisonnables.
- Taille du pool : commencez plus petit que vous ne le pensez. Si vous avez 50 instances d’app et que chaque pool est à 50, cela fait 2500 connexions potentielles avant que la base ait son mot à dire.
- Timeout d’acquisition de connexion : forcez un échec rapide côté app plutôt qu’une attente infinie. L’attente infinie garde des sockets ouverts.
- Timeout d’inactivité : fermez les connexions inactives pour ne pas dépenser votre budget de connexions sur des sessions en sommeil.
À éviter : « Une connexion par requête » sans pooling. C’est une machine à churn de threads et transforme les pics en tempêtes.
Côté MySQL, deux réglages aident à réduire les zombies en sommeil :
wait_timeout(sessions non-interactives)interactive_timeout(sessions interactives)
Conseil orienté opinion : Dans des environnements web typiques, un wait_timeout de 60–300 secondes est raisonnable si le pool applicatif est sain. Si votre app ne le tolère pas, l’app est en faute, pas la base.
2) Débloquer les accumulations de verrous : transactions courtes et DDL planifié
Les erreurs de connexion suivent souvent des embouteillages de verrous. Les sessions attendent ; de nouvelles sessions arrivent ; finalement vous atteignez le plafond. Résoudre les embouteillages de verrous est l’une des rares façons de régler « trop de connexions » sans ajouter de capacité.
- Gardez les transactions courtes : pas de temps de réflexion utilisateur à l’intérieur d’une transaction, pas de boucles longues, pas de « lire 10k lignes puis décider ».
- Discipline DDL : ne lancez pas de changements de schéma pendant les pics, surtout ceux qui prennent des metadata locks longtemps.
- Découpez les jobs batch : committez fréquemment, utilisez des lots bornés, et décrochez-vous quand le système est chaud.
3) Réduire le temps des requêtes : le temps est ce qui maintient les connexions ouvertes
Le nombre de connexions est essentiellement « taux d’arrivée × temps dans le système ». Vous ne pouvez pas toujours contrôler le taux d’arrivée. Vous pouvez réduire le temps dans le système.
- Indexez les prédicats et ordres réellement utilisés par les digests chauds.
- Privilégiez les index couvrants pour les endpoints de liste fréquents afin d’éviter des lectures de pages supplémentaires.
- Arrêtez de sélectionner des colonnes non utilisées. Oui, même si « c’est juste du JSON ». C’est mémoire, CPU et réseau.
- Éliminez la pagination OFFSET à grande échelle. Elle ralentit avec la croissance de l’offset. Utilisez la pagination par clé (keyset) quand c’est possible.
4) Faites de max_connections une décision calculée, pas un vœu
Si vous avez vérifié que vous n’êtes pas bloqué par des verrous, que vous ne swappez pas, et que vous n’êtes pas I/O-saturé, augmenter max_connections peut être approprié. Mais dimensionnez-le avec des calculs et des garde-fous.
Règle pratique (à utiliser avec jugement) :
- Mémoire de base : buffer pool InnoDB + buffers globaux MySQL + overhead OS.
- Mémoire par connexion : varie énormément selon vos réglages et workload. Le danger vient du fait que beaucoup de buffers par thread sont alloués à la demande ; le pire cas est moche.
Approche pratique :
- Mesurez l’usage mémoire au pic actuel de connexions (RSS de mysqld).
- Estimez la mémoire additionnelle par 50–100 connexions supplémentaires à partir des deltas observés, pas d’estimations.
- Augmentez par petites étapes (ex. +20%) ; surveillez swap, latence et waits de locks.
5) Réglez les buffers par connexion de façon conservatrice
C’est ainsi que vous évitez « nous avons augmenté max_connections et la machine a commencé à swappez ». Beaucoup de configs « performance » copiées-collées définissent de gros buffers sans considérer qu’ils se multiplient par sessions actives.
Paramètres à traiter avec scepticisme dans les systèmes très concurrents :
sort_buffer_sizejoin_buffer_sizeread_buffer_sizeread_rnd_buffer_sizetmp_table_size/max_heap_table_size(impactent les tables temporaires en mémoire)
Conseil orienté opinion : Gardez-les relativement petits tant que vous n’avez pas de preuve qu’un workload spécifique en bénéficie. La plupart des workloads OLTP gagnent plus d’index et d’un buffer pool adapté que de gros buffers par thread.
6) Thread cache : réduire le surcoût, surtout lors des churns
Les workloads en rafales créent du churn de connexions. Le churn crée des créations/destructions de threads. Un thread_cache_size décent aide à absorber les pics sans gaspiller le CPU.
Ce qu’il faut faire : Augmentez thread_cache_size jusqu’à ce que Threads_created cesse de grimper rapidement par rapport à Connections. Ne le mettez pas à 10 000 « au cas où ». Cachez juste assez pour couvrir les pics.
7) Utilisez le pooling au bon niveau (app d’abord, proxy ensuite)
Le pooling côté app est généralement préférable car il préserve le contexte de la requête et le backpressure. Mais il existe des cas où un proxy de pooling dédié est nécessaire :
- Beaucoup de clients court-terme (serverless, cron bursts, jobs CI).
- Apps legacy qui ouvrent trop de connexions et ne peuvent pas être corrigées rapidement.
- Environnements multi-tenant où il faut une gouvernance stricte.
Attention : le pooling au niveau transaction peut casser des hypothèses de session (tables temporaires, variables de session). Si votre app utilise cela, vous avez besoin d’un pooling de session ou de modifications de code.
8) Spécificités Ubuntu 24.04 : overrides systemd et les rendre persistants
Sur Ubuntu 24.04, l’histoire commune « on a défini ulimit mais ça n’a pas marché » est systemd. L’unité de service a ses propres limites.
Pour définir une limite FD persistante pour MySQL :
cr0x@server:~$ sudo systemctl edit mysql
# (opens an editor)
cr0x@server:~$ sudo cat /etc/systemd/system/mysql.service.d/override.conf
[Service]
LimitNOFILE=1048576
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart mysql
Ce que cela signifie : MySQL héritera désormais de la limite FD au démarrage du service.
Décision : Ne faites cela que si vous avez confirmé que les limites FD font partie du problème. Augmenter les limites ne corrige pas les requêtes lentes ; ça repousse juste l’échec.
9) Ajouter du backpressure : échouer vite en amont plutôt que fondre en aval
Si la BD est la ressource critique partagée, votre app doit la protéger. Stratégies de backpressure qui maintiennent la réactivité :
- Captez la taille du pool par instance d’app.
- Timeout d’acquisition court (ex. 100–500ms selon votre SLO).
- Queuez les requêtes dans l’app avec des files bornées ; rejetez au-delà.
- Désactivez les retries agressifs sur les échecs de connexion DB ; utilisez backoff exponentiel avec jitter.
10) Scalez la bonne chose : replicas de lecture, sharding, ou une machine plus grosse
Si vous avez fait l’hygiène et que vous atteignez toujours le plafond de connexions parce que la charge est réelle, montez en capacité délibérément :
- Read replicas pour les charges en lecture (déplacez reporting/endpoints de liste, pas les écritures critiques).
- Hôte plus puissant si vous êtes contraint par la mémoire/I/O et pouvez monter verticalement rapidement.
- Sharding si le scaling en écriture est le problème et que le modèle de données le permet — ce n’est pas une correction du mardi après-midi.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom: lots of “Sleep” sessions, maxed connections, but CPU is not high
Cause racine : pools applicatifs trop grands, connexions fuyantes, ou wait_timeout trop long gardant les sessions inactives.
Correction : plafonnez les pools, assurez-vous que les connexions sont retournées, réduisez wait_timeout à quelques minutes, et fixez des timeouts client-side.
2) Symptom: many sessions “Waiting for table metadata lock”
Cause racine : changement de schéma en ligne / DDL ou transaction longue bloquant les metadata locks.
Correction : planifiez le DDL hors pic, utilisez correctement des outils de migration online, et tuez/évitez les transactions longues qui gardent des verrous. Identifiez le bloqueur via performance_schema et agissez.
3) Symptom: “too many connections” appears together with swap activity
Cause racine : sur-engagement mémoire, souvent dû aux buffers par connexion et à une concurrence excessive.
Correction : réduisez les buffers par connexion, réduisez les caps de connexion, redimensionnez le buffer pool, et stoppez le swap avant d’augmenter max_connections.
4) Symptom: connections spike during an outage, then never recover cleanly
Cause racine : tempêtes de retries, clients se reconnectant agressivement, ou vérifications de santé du load balancer qui ouvrent des sessions.
Correction : backoff exponentiel avec jitter, circuit breakers, rate-limit des reconnexions, et assurez-vous que les health checks n’authentifient pas MySQL en permanence.
5) Symptom: “Aborted connection… error reading communication packets” grows during peak
Cause racine : clients qui timeoutent à cause de la lenteur serveur, perte de paquets, ou pile réseau surchargée.
Correction : corrigez la latence sous-jacente (CPU/I/O/verrous), validez la stabilité réseau, et alignez les timeouts client/serveur pour éviter le churn inutile.
6) Symptom: raising max_connections “fixes” the error but latency doubles
Cause racine : vous avez augmenté la contention et la pression mémoire ; vous n’avez pas augmenté le débit.
Correction : revenez en arrière ou réduisez, implémentez des caps de pooling, optimisez les requêtes principales, et augmentez la capacité (CPU/I/O) si nécessaire.
7) Symptom: errors persist even though max_connections looks high
Cause racine : vous touchez des limites OS/service (FDs), ou un proxy a son propre plafond de connexions.
Correction : vérifiez LimitNOFILE systemd, vérifiez les limites effectives de mysqld via /proc, et auditez les couches intermédiaires pour des caps.
Trois mini-récits d’entreprise tirés du terrain
Mini-story 1: The incident caused by a wrong assumption
Ils avaient un modèle mental propre : « Nous exécutons 20 pods d’app, donc 20 connexions. » Rassurant. Faux.
La vraie configuration incluait un déploiement de workers en arrière-plan, un collecteur de métriques effectuant des vérifications périodiques, une UI d’admin, et un job de migration qui « tourne parfois ». Chaque composant avait ses propres défauts de pool. Les web pods seuls allaient bien. Les workers, en revanche, montaient en charge pendant les événements de backlog — exactement quand la base était déjà occupée.
Le premier incident « too many connections » est arrivé pendant une campagne marketing. Ils ont augmenté max_connections, redémarré MySQL, et vu l’erreur disparaître. Pendant environ une heure. Puis la BD a ralenti, les timeouts ont augmenté, et l’app a réessayé plus fort. Les connexions ont remonté, maintenant avec plus d’attentes de verrous et plus de swap.
La leçon post-incident n’était pas « mettre max_connections plus haut ». C’était « compter les connexions de toute la flotte, y compris ce dont personne ne se souvient ». Ils ont corrigé ça en plafonnant chaque pool, en ajoutant un timeout d’acquisition, et en enseignant au système de workers de ralentir quand la latence DB augmente.
Mini-story 2: The optimization that backfired
Une équipe voulait réduire la latence des requêtes. Quelqu’un a augmenté agressivement join_buffer_size et sort_buffer_size parce qu’un blog disait que ça aide. Sur une machine de staging qui exécutait une requête à la fois, ça a aidé.
En production, la concurrence est tout. Au pic, des centaines de sessions étaient actives. Ces gros buffers par connexion n’étaient pas toujours alloués, mais quand certains endpoints déclenchaient des requêtes complexes, la mémoire montait vite. L’OS a commencé à swaper sous charge. Une fois le swap lancé, les requêtes ont ralenti. Quand les requêtes ralentissent, les connexions restent ouvertes plus longtemps. Quand les connexions restent ouvertes, le serveur a manqué de connexions. Le symptôme initial ? « Too many connections ».
Ça ressemblait à un problème de plafond de connexions, mais c’était une amplification mémoire par connexion. La correction a été ennuyeuse : revenir sur les buffers par thread surdimensionnés, ajouter les bons index, et augmenter le buffer pool en restant dans une marge sûre. Leur latence s’est améliorée, et la limite de connexions a cessé d’être un drame quotidien.
Mini-story 3: The boring but correct practice that saved the day
Une autre organisation avait une habitude peu glamour : chaque service avait un « budget BD » documenté. Une taille maximale de pool par instance. Un nombre maximal d’instances autorisées avant révision de scaling. Un timeout d’acquisition standard et une politique de retry.
Pendant un test de bascule régional, le trafic a basculé brusquement. La charge a doublé. La base a chauffé mais n’est pas tombée. Au lieu d’ouvrir des connexions infinies, les apps ont mis en file un peu, rejeté vite les requêtes excédentaires, et récupéré quand le pic est passé.
Ils ont quand même eu une CPU BD élevée. Ils ont dû tuner quelques index. Mais ils n’ont pas eu la panne catastrophique « too many connections » qui déclenche des changements chaotiques au milieu de la nuit.
Le meilleur : le rapport d’incident était court. Les systèmes ennuyeux sont fiables, et les systèmes fiables sont ceux près desquels vous pouvez dormir.
Checklists / plan étape par étape
Étapes pour arrêter l’hémorragie pendant un incident (15–30 minutes)
- Vérifiez ce qui est saturé : exécutez Task 7 (CPU), Task 8 (I/O), Task 9 (swap).
- Identifiez les états de connexion : Task 4 pour voir Sleep vs Query vs Locked.
- Si les waits de verrous dominent : Task 5 et Task 6 pour trouver les bloqueurs. Décidez : attendre, tuer, ou reporter DDL/batch.
- Si Sleep domine : réduisez la taille des pools applicatifs (levier le plus rapide), puis envisagez d’abaisser
wait_timeoutaprès vérification d’impact. - Si I/O bound : arrêtez d’augmenter la concurrence. Réduisez la charge (rate limit, désactivez endpoints coûteux), puis tunez buffer pool/indexes.
- Si swap : réduisez immédiatement l’utilisation mémoire (baisser concurrence/pools, revenir sur buffers par thread surdimensionnés si applicable). Le swap est un signal « arrêtez tout ».
- Ce n’est qu’après, si vous avez de la marge et en avez besoin, que vous augmentez
max_connectionsmodestement comme mitigation temporaire.
Étapes pour une correction permanente (un sprint, pas une panique)
- Inventairez tous les clients : web, workers, cron, outils admin, métriques, ETL. Documentez le maximum de connexions attendu par composant.
- Mettez en place le pooling correctement : caps de pool, timeouts d’acquisition, et assurez-vous que les connexions sont retournées.
- Corrigez les requêtes chaudes : utilisez les résumés digest de performance_schema (Task 12), puis ajoutez/ajustez des index et réduisez les données récupérées.
- Corrigez le comportement des verrous : raccourcissez les transactions, découpez les jobs batch, et planifiez le DDL.
- Calibrez les limites MySQL : définissez
max_connectionssur la base de la marge mesurée ; tunezthread_cache_size,table_open_cache. - Validez les limites OS/service : assurez-vous que les limites systemd correspondent à votre conception (Task 10).
- Testez la charge avec de la concurrence : pas seulement des QPS. Vérifiez latence et taux d’erreur sous patrons de pics.
- Configurez des alertes préventives : alertez sur Threads_connected en hausse, Max_used_connections, waits de verrous, swap, et augmentation des connexions abortées.
À éviter (parce que vous le regretterez)
- Fixer
max_connectionsà un nombre énorme « pour que ça n’arrive jamais ». Ça reviendra, mais plus lentement et plus cher. - Copier-coller des my.cnf « haute performance » avec d’énormes buffers par thread.
- Lancer des changements de schéma pendant un pic et s’étonner ensuite des metadata locks.
- Permettre aux clients de réessayer instantanément et à l’infini. Ce n’est pas de la résilience ; c’est une fonctionnalité de déni de service.
FAQ
1) Should I just increase max_connections?
Seulement si vous avez confirmé avoir de la marge mémoire (pas de swap), et que le serveur n’est pas déjà saturé CPU/I/O. Sinon vous échangerez des erreurs nettes contre un effondrement lent.
2) Why does the database get slower when I allow more connections?
Plus de sessions signifient plus de contention, plus de context switching, plus de churn de buffers, et potentiellement plus d’I/O disque. Le débit ne scale pas linéairement avec les connexions.
3) What’s a good value for wait_timeout?
Pour des apps web typiques avec pooling, 60–300 secondes est une gamme de départ raisonnable. Si vous avez besoin d’heures, votre app utilise MySQL comme store de session, et c’est une autre discussion.
4) How do I know if it’s an application connection leak?
Si Threads_connected grimpe régulièrement et que la plupart des sessions sont Sleep avec des temps élevés, c’est une fuite/accaparement classique. Confirmez en corrélant avec le nombre d’instances applicatives et les configs de pool.
5) What if I see many “Waiting for table metadata lock” sessions?
Arrêtez de lancer des DDL pendant les pics, trouvez le bloqueur (souvent une transaction longue), et décidez de le tuer. Ensuite mettez le DDL derrière une fenêtre de changement et un processus.
6) Do read replicas fix “too many connections”?
Oui, si la pression vient des lectures et que vous y routez effectivement ces lectures. Elles ne régleront pas la contention d’écritures, les transactions longues, ou un mauvais pooling.
7) Can OS file descriptor limits cause connection errors?
Oui. Si mysqld ne peut pas ouvrir plus de sockets/fichiers, vous verrez des échecs étranges sous charge. Sur Ubuntu 24.04, vérifiez LimitNOFILE systemd et les limites effectives de mysqld.
8) Is lowering per-connection buffers always safe?
C’est généralement plus sûr que de les augmenter aveuglément. Des buffers plus petits peuvent augmenter l’utilisation temporaire disque ou ralentir certaines requêtes, donc validez sur un workload réel. Mais des buffers surdimensionnés sous haute concurrence sont une cause fréquente d’incident.
9) How do I choose between fixing queries and adding hardware?
Si vous êtes CPU/I/O saturé avec des requêtes connues problématiques, corrigez les requêtes d’abord. Si vous êtes bien optimisé et toujours saturé, augmentez la machine ou scalez horizontalement. Mesurez avant de dépenser.
10) What metrics should I alert on to catch this early?
Threads_connected, Max_used_connections, indicateurs de waits de verrous, taux de connexions abortées, utilisation de swap, disk await/utilization, et latence p95/p99 des requêtes (pas seulement la moyenne).
Prochaines étapes qui ne feront pas de mal plus tard
Si vous voyez « trop de connexions » sur Ubuntu 24.04, traitez-le comme un symptôme, pas une valeur de configuration. La correction stable la plus rapide est presque toujours de réduire l’occupation des connexions : raccourcir les requêtes, raccourcir les transactions, et empêcher l’app d’accaparer des sessions.
Faites ceci ensuite, dans l’ordre :
- Exécutez le playbook de diagnostic rapide et capturez des preuves (états des processus, CPU/I/O/swap).
- Corrigez le principal facteur : hordes de sessions en sommeil ou embouteillages de verrous. Ne débattiez pas ; mesurez.
- Plafonnez et ajustez les pools applicatifs. Ajoutez des timeouts d’acquisition et un backoff de retry raisonnable.
- Optimisez les digests de statements principaux. Cela réduit le temps dans le système et facilite tout le reste.
- Puis — et seulement alors — ajustez
max_connectionset les caches MySQL en fonction de la marge observée.
Quand vous réglez ça correctement, la base devient plus rapide sous charge, pas plus lente. C’est tout l’enjeu. L’erreur disparaît parce que le système est plus sain, pas parce que vous lui avez appris à tolérer plus de souffrance.