Pièges Docker MySQL/MariaDB : les paramètres par défaut qui ruinent les performances

Cet article vous a aidé ?

Certaines bases de données échouent bruyamment. MySQL et MariaDB dans Docker échouent souvent poliment : ils continuent de répondre tout en transformant silencieusement votre SSD en instrument de percussion.

La chute de performance n’est généralement pas causée par votre schéma ou votre « requête bizarre ». Ce sont les paramètres par défaut : pilotes de stockage Docker, sémantiques du système de fichiers, limites de mémoire, comportement de fsync, et quelques réglages de base de données qui étaient raisonnables en 2008 et catastrophiques dans un conteneur en 2026.

Le vrai ennemi : décalage entre les attentes de la base de données et la réalité du conteneur

MySQL et MariaDB sont nés en supposant un contrat assez simple avec le système d’exploitation :

  • « Quand j’appelle fsync, mes données sont sur un stockage stable. »
  • « Quand j’alloue de la mémoire, elle m’appartient. »
  • « Quand j’écris sur le disque, la latence est à peu près prévisible. »
  • « Quand j’utilise des tables temporaires, j’ai suffisamment d’espace disque local. »

Docker ne viole pas ce contrat volontairement. Il ajoute juste des couches : systèmes de fichiers en union, copy-on-write, limites cgroup, systèmes de fichiers virtualisés sur macOS/Windows, et pilotes de volumes avec des caractéristiques de durabilité et de latence très différentes. Votre base de données tente toujours d’être correcte. Votre plateforme de conteneurs cherche à être flexible. Correction et flexibilité peuvent coexister — à condition d’arrêter de faire confiance aux paramètres par défaut.

Voici la thèse : considérez une base de données en conteneur comme un appliance de stockage. Le stockage est une dépendance de première importance. Le CPU est généralement correct. Le réseau est rarement la première cause. C’est presque toujours la latence I/O, le comportement de fsync, ou la pression mémoire déguisée en « requêtes lentes ».

Faits intéressants et un peu d’histoire (parce que les paramètres par défaut ont une genèse)

Ce sont des petits faits, mais chacun explique un moment « pourquoi c’est comme ça ? » que vous rencontrerez en optimisant MySQL/MariaDB dans Docker :

  1. InnoDB est devenu le moteur par défaut dans MySQL 5.5, principalement parce qu’il gère mieux les plantages et la concurrence que MyISAM. Cela signifie aussi que vous héritez des fortes opinions d’InnoDB sur fsync et les redo logs.
  2. MariaDB a forké MySQL en 2009 après l’acquisition de Sun par Oracle. Le fork a préservé la compatibilité, mais les paramètres opérationnels et les caractéristiques de performance ont divergé subtilement au fil du temps.
  3. Les systèmes de fichiers overlay de Docker ont été conçus pour les images, pas pour les bases. Le copy-on-write est fantastique pour superposer des fichiers applicatifs ; c’est une charge pour les charges qui écrivent de petits blocs aléatoires toute la journée.
  4. Le doublewrite buffer d’InnoDB existe parce que des écritures partielles de pages se produisent (perte de courant, comportements anormaux de contrôleur, bugs du noyau). Il échange amplification d’écriture contre sécurité — un compromis moche mais généralement correct.
  5. Le query cache de MySQL a été supprimé dans MySQL 8.0 car il provoquait des contentions et des performances imprévisibles. Si vous « ajustez le query cache » dans un conteneur, vous êtes probablement sur une version ancienne et avez des problèmes plus importants.
  6. Les sémantiques d’O_DIRECT et fsync sous Linux varient selon le système de fichiers et les options de montage. La durabilité d’une base n’est pas une vérité universelle ; c’est un accord négocié entre les couches logicielles.
  7. Les cgroups ont rendu la « mémoire disponible » mensongère dans les conteneurs pendant des années. MySQL moderne se débrouille mieux pour lire les limites cgroup, mais beaucoup d’images et de versions anciennes dimensionnent encore les buffers sur la RAM de l’hôte.
  8. Les cliff de latence des SSD sont réels : quand vous satuez la file d’attente ou déclenchez la garbage collection, la latence augmente en premier, le débit après. Votre base donnera des timeouts bien avant que votre monitoring n’affiche « disque à 100 % occupé ».

Et une citation sur la fiabilité — parce que c’est encore la seule leçon qui compte :

« L’espoir n’est pas une stratégie. » — Général Gordon R. Sullivan

Playbook de diagnostic rapide : trouver le goulot en 15 minutes

Si une instance MySQL/MariaDB en conteneur est lente, ne commencez pas par le SQL. Commencez par la physique. La base attend quelque chose.

Premier : confirmez ce que « lent » signifie

  • Est-ce une latence par requête plus élevée, un débit réduit, ou les deux ?
  • Est-ce seulement les écrits, seulement les lectures, ou tout ?
  • Est-ce périodique (pics) ou constant ?

Deuxième : vérifiez la latence disque et la pression sur fsync

  • Recherchez un temps de fsync élevé sur les redo logs et des arrêts dus aux pages sales.
  • Vérifiez que vous n’écrivez pas dans overlay2 ou sur un volume réseau fin avec une latence de sync terrible.

Troisième : vérifiez la mémoire et les limites cgroup

  • Êtes-vous victime d’un OOM qui tue mysqld, d’un swap, ou d’un vidage agressif du cache de pages ?
  • Le pool de buffers InnoDB est-il dimensionné sur la mémoire de l’hôte au lieu de la limite du conteneur ?

Quatrième : vérifiez CPU steal et throttling

  • La famine CPU se déguise en lenteur DB « aléatoire », surtout sous des charges en rafales.

Cinquième : seulement maintenant, lisez le slow query log

  • Si les requêtes lentes attendent « waiting for handler commit » ou de l’I/O, vous revenez au disque et à fsync.
  • S’il s’agit d’un problème CPU avec de mauvais plans, alors optimisez SQL et index.

Voilà la séquence. Quand les gens la sautent, ils optimisent la mauvaise chose pendant des semaines.

Les paramètres par défaut qui ruinent les performances (et que faire à la place)

1) Écrire votre datadir sur overlay2 (ou tout système de fichiers en union)

L’anti-pattern classique Docker : vous lancez MySQL dans un conteneur, oubliez de monter un vrai volume, et le datadir vit dans la couche inscriptible du conteneur. Ça fonctionne. C’est catastrophique en bench. Ça complique aussi les mises à jour et les sauvegardes car votre état est collé à une couche éphémère.

Pourquoi c’est nuisible : overlay2 est copy-on-write. Les bases écrivent beaucoup de petites mises à jour, et InnoDB écrit selon des schémas qui provoquent des mutations de métadonnées. Même si vous avez un SSD, la couche de système de fichiers peut ajouter de la latence et de la charge CPU.

Faites plutôt : placez /var/lib/mysql sur un volume nommé ou un bind mount sur un vrai système de fichiers. En production, privilégiez des volumes locaux sur XFS/ext4 avec des options de montage sensées, ou un volume CSI choisi avec attention et une latence de sync connue.

2) Partir du principe que « volume Docker » signifie « rapide » (ce n’est pas le cas)

Un « volume » Docker est une abstraction. Le stockage sous-jacent peut être un répertoire local ext4. Ou NFS. Ou un disque bloc cloud. Ou un système de fichiers distribué. Le profil de latence peut être « acceptable » ou « pourquoi le commit prend 200ms ? »

Faites plutôt : mesurez la latence de fsync sur le driver de volume exact que vous utilisez. Traitez le stockage comme une dépendance avec un SLO.

3) Paramètres de durabilité InnoDB par défaut + stockage de synchronisation lent = tristesse

Deux variables importent le plus pour les charges d’écriture :

  • innodb_flush_log_at_trx_commit (généralement 1 par défaut)
  • sync_binlog (souvent 1 dans des configurations prudentes, mais variable)

innodb_flush_log_at_trx_commit=1 signifie qu’InnoDB vide les redo sur le disque à chaque commit. Sur un stockage avec une latence fsync élevée, la latence de commit devient la latence du stockage. Si vous avez aussi la binlog et sync_binlog=1, vous payez deux fois.

Que faire : décidez explicitement de vos exigences de durabilité. Pour beaucoup de systèmes internes, innodb_flush_log_at_trx_commit=2 est un compromis de risque acceptable. Pour des systèmes financiers, peut-être pas. Mais prenez la décision, ne l’héritez pas.

Blague #1 : Si vous mettez innodb_flush_log_at_trx_commit=0 en production, votre base de données est maintenant alimentée par des vibes et de l’optimisme.

4) Les binlogs sur un stockage lent (et sans plan)

Les binlogs ne sont pas optionnels si vous voulez la réplication ou la récupération point-in-time. Ce sont aussi un flux constant d’écritures qui peut devenir l’I/O dominante. Dans Docker, on oublie souvent :

  • Où sont stockés les binlogs (même datadir sauf configuration)
  • À quelle vitesse ils grossissent sous charge d’écriture
  • Que purger nécessite une politique

Correction : dimensionnez le stockage pour la rétention des binlogs, activez l’expiration automatique, et surveillez-les.

5) Limites mémoire du conteneur + valeurs par défaut du buffer pool InnoDB = thrash du cache

Dans un conteneur, la mémoire est politique. MySQL peut voir la RAM de l’hôte (selon version et configuration) et allouer un grand buffer pool. Puis la limite cgroup se déclenche, et le noyau commence à tuer des processus ou à réclamer agressivement.

Symptômes : arrêts périodiques, OOM kills, pics de latence « aléatoires », et un OS qui semble calme pendant que le conteneur brûle.

Correction : réglez innodb_buffer_pool_size en fonction de la limite du conteneur, pas de la RAM de l’hôte. Laissez de la place pour :

  • les buffers de connexion (les allocations par-connexion s’additionnent)
  • les sort/join buffers sous charge
  • les frais internes d’InnoDB
  • le cache du système de fichiers (oui, toujours pertinent)

6) Trop de connexions (parce que les valeurs par défaut sont généreuses)

max_connections est souvent élevé « au cas où ». Dans les conteneurs, ce « au cas où » devient « tout droit vers le swap ». Chaque connexion peut allouer de la mémoire par thread. Sous rafales, la mémoire gonfle et le noyau fait ce que font les noyaux : il vous punit pour avoir menti sur la capacité.

Correction : limitez max_connections, utilisez du pooling, et dimensionnez raisonnablement les buffers par thread. Votre base n’est pas une salle de concert ; elle n’a pas besoin d’un nombre infini de personnes debout.

7) tmpdir et tables temporaires qui atterrissent sur le mauvais disque

Les gros tris, ALTER TABLE et requêtes complexes peuvent déborder sur le disque. Dans les conteneurs, tmpdir peut se retrouver sur une petite partition racine, pas sur votre grand volume de données.

Correction : définissez tmpdir sur un chemin du même volume rapide (ou un volume rapide dédié), et surveillez l’espace libre. Sur Kubernetes, ce sont les limites de stockage éphémère qui surprennent souvent.

8) Taille des redo logs manquante ou mal réglée

Des redo logs trop petits provoquent des checkpoints fréquents, ce qui augmente les flushs en arrière-plan et amplifie la pression d’écriture. Trop grands peuvent rallonger le temps de récupération après crash. En conteneur, le cas « trop petit » est plus courant car les valeurs par défaut sont conservatrices et le stockage est souvent plus lent que prévu.

Correction : ajustez la capacité des redo logs en fonction de votre taux d’écriture et de vos objectifs de récupération. Sur MySQL moderne, pensez en termes de innodb_redo_log_capacity ; sur MariaDB et les anciennes versions, ajustez la taille et le nombre de fichiers de log.

9) « On utilisera juste du stockage réseau » (et puis fsync = 20ms)

Le stockage distant peut fonctionner. Il peut aussi transformer silencieusement votre chemin de commit en aller-retour sur le réseau et trois couches de cache. Beaucoup de systèmes de stockage distribués sont optimisés pour le débit, pas pour la latence fsync.

Correction : validez la latence des écritures synchrones avant de parier votre base dessus. Si vous devez utiliser des volumes réseau, préférez ceux avec des sémantiques de durabilité prévisibles et une faible latence des queues. Mesurez le p99, pas les moyennes.

10) Ne pas fixer le CPU ou ne pas observer le throttling

La performance des bases aime la cohérence. Le throttling CPU et la contention peuvent ressembler à des attentes I/O parce que le thread de la base n’est pas planifié pour finir son travail. Dans les conteneurs, vous pouvez accidentellement exécuter MySQL en « best effort » CPU pendant que des jobs batch fauchent le nœud.

Correction : définissez des requests/limits CPU appropriés (ou des quotas CPU Docker) et surveillez les métriques de throttling. Si vous voyez du throttling sous charge normale, vous êtes sous-provisionné ou mal configuré.

11) Exécuter sur Docker Desktop macOS/Windows en attendant un I/O natif Linux

Les environnements de dev sont là où naissent les mythes de performance. Docker Desktop utilise une VM. Les bind mounts passent par des couches de traduction. Les E/S fichiers peuvent être beaucoup plus lentes, surtout les patterns sync-centrés comme InnoDB redo.

Correction : pour des tests de performance réalistes, exécutez sur Linux avec un vrai volume. Pour le dev local, acceptez des I/O plus lentes ou utilisez des volumes nommés plutôt que des bind mounts.

12) Considérer la config MySQL comme « à l’intérieur du conteneur » au lieu de « partie du service »

Si votre configuration est intégrée dans l’image sans externalisation, vous finirez par déployer un changement nécessitant de reconstruire des images sous pression. C’est ainsi que des incidents « simples » deviennent longs.

Correction : montez la config, versionnez-la, et rendez-la observable (SHOW VARIABLES doit refléter votre intention).

Tâches pratiques : commandes, sorties et décisions (12+)

Ce sont des vérifications de niveau production. Chaque élément inclut : une commande, ce que la sortie signifie, et la décision à prendre.

Task 1: Confirm where MySQL is actually writing data (overlay vs volume)

cr0x@server:~$ docker inspect -f '{{ .Mounts }}' mysql-prod
[{volume mysql-data /var/lib/docker/volumes/mysql-data/_data /var/lib/mysql local  true }]

Sens : /var/lib/mysql est sur un volume nommé, pas dans la couche du conteneur.

Décision : Si vous ne voyez pas de mount pour /var/lib/mysql, arrêtez-vous et corrigez cela avant d’optimiser autre chose.

Task 2: Identify the Docker storage driver (overlay2, devicemapper, etc.)

cr0x@server:~$ docker info --format '{{ .Driver }}'
overlay2

Sens : overlay2 est utilisé. Correct pour les containers. Pas idéal pour l’état de la base sauf si vous montez correctement des volumes.

Décision : Si le datadir n’est pas un volume, attendez-vous à des ennuis. S’il l’est, vous avez en grande partie évité les pénalités d’overlay2.

Task 3: Check filesystem type and mount options backing the volume

cr0x@server:~$ docker run --rm -v mysql-data:/var/lib/mysql alpine sh -c "df -T /var/lib/mysql && mount | grep ' /var/lib/docker'"
Filesystem     Type  1K-blocks      Used Available Use% Mounted on
/dev/nvme0n1p2  xfs  488245288 124332456 363912832  26% /var/lib/docker
/dev/nvme0n1p2 on /var/lib/docker type xfs (rw,relatime,attr2,inode64,logbufs=8,logbsize=32k)

Sens : Ce volume réside sur XFS avec des options typiques.

Décision : Si vous voyez NFS ou un montage distant ici, considérez la latence fsync comme le principal suspect pour les performances d’écriture.

Task 4: Check container memory limit vs what MySQL thinks it has

cr0x@server:~$ docker inspect -f '{{ .HostConfig.Memory }}' mysql-prod
2147483648
cr0x@server:~$ docker exec -it mysql-prod bash -lc "cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes"
2147483648

Sens : Le conteneur est limité à 2 GiB.

Décision : Fixez innodb_buffer_pool_size à quelque chose comme 1.0–1.4 GiB selon la charge et le nombre de connexions, pas « 80% de la RAM de l’hôte ».

Task 5: Verify actual InnoDB buffer pool size

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -N -e \"SHOW VARIABLES LIKE 'innodb_buffer_pool_size';\""
innodb_buffer_pool_size	1073741824

Sens : Le buffer pool est de 1 GiB.

Décision : S’il est minuscule (par exemple 128 MiB) sur une instance très chargée, attendez-vous à des lectures disque constantes et de mauvaises performances.

Task 6: Check for OOM kills and memory pressure signals

cr0x@server:~$ docker exec -it mysql-prod bash -lc "dmesg -T | tail -n 5"
[Fri Jan  3 09:28:41 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker-3b1a...,mems_allowed=0,oom_memcg=/docker/3b1a...
[Fri Jan  3 09:28:41 2026] Killed process 1973 (mysqld) total-vm:3121044kB, anon-rss:1632100kB, file-rss:0kB, shmem-rss:0kB

Sens : Le noyau a tué mysqld à cause des limites mémoire cgroup.

Décision : Réduisez l’usage mémoire de MySQL (buffers, connexions) ou augmentez la limite mémoire du conteneur. Corrigez aussi les boucles de redémarrage et analysez le risque de corruption.

Task 7: Inspect redo log fsync pressure and commit stalls

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW ENGINE INNODB STATUS\\G\" | sed -n '1,120p'"
...
Log sequence number          912345667890
Log flushed up to            912345660000
Last checkpoint at           912345100000
0 pending log flushes, 0 pending chkp writes
...

Sens : Si vous voyez de nombreux pending log flushes ou un grand écart entre « Log sequence number » et « Log flushed up to », votre stockage n’arrive pas à suivre les fsync de redo.

Décision : Améliorez la latence du stockage, ajustez la capacité des redo, envisagez des compromis de durabilité (innodb_flush_log_at_trx_commit), et réduisez l’amplification d’écriture.

Task 8: Confirm durability-related variables (the ones people “assume”)

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_doublewrite');\""
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_doublewrite           | ON    |
| innodb_flush_log_at_trx_commit | 1   |
| sync_binlog                  | 1     |
+------------------------------+-------+

Sens : Vous payez le coût complet de durabilité (flush redo par commit + sync binlog par commit).

Décision : Gardez-le si vous en avez besoin. Si ce n’est pas le cas, changez-le intentionnellement et documentez le profil de risque.

Task 9: Measure real disk latency from inside the container (quick and dirty)

cr0x@server:~$ docker exec -it mysql-prod bash -lc "dd if=/dev/zero of=/var/lib/mysql/dd-test.bin bs=4k count=25000 oflag=dsync 2>&1 | tail -n 2"
25000+0 records in
25000+0 records out

Sens : Avec oflag=dsync, chaque bloc est synchronisé. Si cette commande prend des secondes de plus que prévu, votre latence d’écriture sync est mauvaise. (La ligne de timing de dd est omise ici car elle varie, mais vous la verrez sur votre système.)

Décision : Si les écritures synchrones sont lentes, arrêtez de blâmer les index. Réparez le stockage ou les paramètres de durabilité.

Task 10: Check whether you’re accidentally using a bind mount with poor semantics (Desktop, CIFS, etc.)

cr0x@server:~$ docker inspect -f '{{ range .Mounts }}{{ .Type }} {{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}' mysql-prod
volume /var/lib/docker/volumes/mysql-data/_data -> /var/lib/mysql

Sens : C’est un volume géré par Docker ; bon départ.

Décision : Si vous voyez bind vers un chemin sur un FS lent/distant, vous avez probablement trouvé un goulot.

Task 11: Check temp table and tmpdir behavior (disk spills)

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES LIKE 'tmpdir'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';\""
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| tmpdir        | /tmp     |
+---------------+----------+
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 48291 |
+-------------------------+-------+

Sens : tmpdir est /tmp, et vous avez beaucoup de tables temporaires sur disque.

Décision : Placez tmpdir sur un volume rapide avec assez d’espace. Analysez aussi les requêtes qui provoquent des débordements temporaires.

Task 12: Spot CPU throttling inside containers

cr0x@server:~$ docker stats --no-stream mysql-prod
CONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
3b1a1c2d3e4f   mysql-prod  398.23%   1.62GiB / 2GiB        81.0%     1.2GB / 900MB  40GB / 12GB  58

Sens : Une forte utilisation CPU peut être normale. L’important est de savoir si vous atteignez des quotas CPU (non montrés ici) et si vous observez des pics de latence corrélés au throttling.

Décision : Si le CPU est élevé et la latence aussi, vérifiez les plans de requête. Si le CPU est bas mais la latence élevée, examinez d’abord l’I/O et les verrous.

Task 13: Confirm slow query log is enabled and where it writes

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'slow_query_log_file';\""
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| slow_query_log | ON    |
+----------------------+-------------------------------+
| Variable_name        | Value                         |
+----------------------+-------------------------------+
| slow_query_log_file  | /var/lib/mysql/mysql-slow.log |
+----------------------+-------------------------------+

Sens : Le logging est activé et écrit sur le volume de données.

Décision : S’il écrit dans le filesystem du conteneur et que vous faites une mauvaise rotation, vous pouvez remplir la racine et planter MySQL. Placez les logs sur un stockage persistant et gérez leur rotation.

Task 14: Check how many threads and connections you’re actually running

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';\""
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 412   |
+-------------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 800   |
+-----------------+-------+

Sens : Vous êtes fortement chargé en connexions. Même si les requêtes sont rapides, la mémoire par thread peut ruiner votre conteneur.

Décision : Ajoutez du pooling, réduisez max_connections, et mesurez la croissance mémoire pendant les pics.

Trois mini-récits d’entreprise (anonymisés, douloureusement familiers)

Mini-récit 1 : L’incident causé par une mauvaise hypothèse

Ils ont migré une application legacy des VM vers des conteneurs parce que « c’est juste un empaquetage ». La base a suivi, parce que ça semblait moderne et propre. L’équipe a utilisé une image MySQL populaire, l’a lancée en Docker Swarm, et a fait un bind mount vers un répertoire hôte qui semblait suffisamment persistant.

Au troisième jour, la latence p95 d’écriture a doublé, puis doublé encore. L’application n’a pas planté ; elle est devenue de plus en plus lente jusqu’à ce que l’interface ressemble au rendu d’un stagiaire très patient. Les ingénieurs ont plongé dans les plans de requête, ajouté des index, et débattu du comportement de l’ORM. Le slow query log affichait une foule d’INSERT innocents prenant des centaines de millisecondes.

Finalement quelqu’un a vérifié l’hôte : le chemin du bind mount vivait sur un partage réseau monté « pour plus de commodité », parce que l’équipe ops voulait un accès facile pour les sauvegardes. Personne n’avait écrit NFS dans le compose ; c’était juste un répertoire. La latence de fsync était brutale, et les latences de queue pire.

La correction fut banale : déplacer le datadir sur un stockage bloc local à latence de sync prévisible, puis implémenter des sauvegardes correctes via dumps logiques et binlogs (et plus tard des sauvegardes physiques). La latence a chuté immédiatement. Les index ajoutés n’étaient pas nuisibles, mais ils n’étaient pas la cause.

Mini-récit 2 : L’optimisation qui s’est retournée contre eux

Une autre entreprise avait un service très écrit. Quelqu’un a lu que innodb_flush_log_at_trx_commit=2 peut être plus rapide, et il avait raison. Ils l’ont changé, vu d’excellents benchmarks, et déployé largement.

Pendant un mois, tout allait bien. Le débit s’est amélioré, et les graphiques de stockage se sont calmés. Puis un nœud a crashé violemment — kernel panic, arrêt non propre, tout le spectacle. MySQL a redémarré, a récupéré, et le service est revenu. Sauf que quelques écritures récemment acquittées avaient disparu.

Personne n’était content, mais personne n’était surpris. Le service n’avait pas de contrat de durabilité clair : il acquittait les transactions avant qu’elles ne soient pleinement durables, et la couche application supposait « acquitté = permanent ». L’optimisation était techniquement correcte mais opérationnellement non maîtrisée.

Ils ont finalement restauré une durabilité stricte sur les tables critiques et implémenté l’idempotence sur les chemins d’écriture où ils pouvaient tolérer la relecture. La vraie correction n’était pas la config ; c’était d’aligner la sémantique de l’application avec la réalité du stockage.

Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une équipe exécutant MariaDB en conteneurs avait une habitude : chaque plainte de performance commençait par les mêmes trois vérifications — type de volume, latence fsync, et marge mémoire du conteneur. Ce n’était pas glamour, mais ça rendait les incidents courts.

Pendant un pic de trafic, ils ont vu le lag de réplication grimper. Les devs ont immédiatement incriminé « un mauvais rollout de requête ». Le SRE de garde a vérifié la latence disque sur le primaire et les réplicas. Les réplicas allaient bien ; le primaire avait des spikes périodiques dans la latence d’écriture sync.

La cause racine n’était pas MySQL. Un scan antivirus programmé (oui, sur Linux) parcourait le point de montage et frappait les métadonnées. Comme ils avaient l’habitude de vérifier le stockage en premier, ils l’ont trouvé en quelques minutes, pas en heures.

Ils ont exclu les répertoires de la base du scan, documenté, et repris leurs activités. Le meilleur : personne n’a dû apprendre un nouveau paramètre de base de données à 2 h du matin.

Erreurs courantes : symptômes → cause racine → correction

Cette section est conçue pour être utilisée en plein incident quand quelqu’un demande pourquoi « la BDD est lente » et que la pièce devient bruyante.

1) Symptom: INSERT/UPDATE latency spikes, reads mostly fine

  • Cause racine : latence de fsync (flush redo, sync binlog) sur stockage lent ou volume réseau.
  • Correction : déplacer datadir/binlogs sur un stockage à latence plus faible ; mesurer les écritures sync ; ajuster la durabilité seulement avec approbation de risque explicite.

2) Symptom: MySQL restarts “randomly,” container exits with 137

  • Cause racine : OOM kill lié à la limite mémoire cgroup ; buffer pool + mémoire par connexion dépassent la limite.
  • Correction : réduire le buffer pool, plafonner les connexions, utiliser du pooling, augmenter la limite mémoire, et vérifier l’usage mémoire sous charge.

3) Symptom: Slow queries show “Copying to tmp table” or disk temp tables increase

  • Cause racine : tmpdir sur un FS lent ou petit ; débordements temporaires dus à des tris/group by.
  • Correction : déplacer tmpdir sur un volume rapide ; augmenter prudemment les seuils de tmp table ; corriger les requêtes/index qui provoquent les débordements.

4) Symptom: “Disk full” but datadir volume has space

  • Cause racine : binlogs ou slow logs qui croissent sans rotation ; tmpdir sur la racine du conteneur ; remplissage du système Docker root.
  • Correction : configurer la rotation des logs ; définir l’expiration des binlogs ; déplacer tmpdir et logs sur le volume adéquat ; surveiller à la fois Docker root et les volumes DB.

5) Symptom: Throughput collapses during backups

  • Cause racine : méthode de sauvegarde saturant l’I/O ou verrouillant les tables ; snapshot sur un stockage qui pénalise le copy-on-write ; lecture depuis le même volume que les redo writes.
  • Correction : planifier les sauvegardes avec des limites I/O ; utiliser une réplique pour les sauvegardes ; utiliser des sauvegardes physiques si approprié ; mesurer l’impact et définir un SLO.

6) Symptom: Replication lag grows after moving to containers

  • Cause racine : réplicas sur une classe de stockage différente ; throttling CPU ; coût fsync du binlog ; espace relay log trop petit ou disque lent.
  • Correction : standardiser le stockage ; vérifier le throttling ; ajuster les paramètres de réplication ; s’assurer que relay logs et datadir sont sur des volumes persistants rapides.

7) Symptom: Performance is fine until a load spike, then everything times out

  • Cause racine : tempête de connexions ; max_connections trop élevé ; création de threads et explosion mémoire.
  • Correction : mettre en place du pooling ; plafonner la concurrence côté application ; fixer max_connections selon la capacité ; envisager les fonctionnalités de thread pool si disponibles.

8) Symptom: CPU looks low but queries are slow

  • Cause racine : attente I/O, contention de verrous, ou throttling non visible dans des métriques naïves.
  • Correction : vérifier InnoDB status pour les attentes ; vérifier la latence disque ; vérifier les métriques de throttling CPU du conteneur ; inspecter les waits de verrous.

Blague #2 : Les conteneurs ne rendent pas votre base plus rapide ; ils la rendent juste plus mobile avec ses mauvaises habitudes.

Checklists / plan pas à pas

Étape par étape : MySQL/MariaDB prêt pour la production dans Docker

  1. Choisissez la classe de stockage en premier. Le stockage bloc local SSD est préférable au stockage réseau pour des commits à faible latence, sauf preuve du contraire.
  2. Montez le datadir comme volume. Aucune exception. Si vous ne pouvez pas monter un volume, vous n’avez pas une base de données ; vous avez une démo.
  3. Décidez explicitement de la durabilité. Documentez innodb_flush_log_at_trx_commit et sync_binlog avec une déclaration de risques claire.
  4. Dimensionnez la mémoire selon les limites du conteneur. Buffer pool + overhead + connexions de pointe doivent tenir avec une marge.
  5. Limitez les connexions et imposez le pooling. Utilisez un max_connections sensé. Ne laissez pas votre appli « découvrir » la limite via des outages.
  6. Placez tmpdir au bon endroit. Rapide, grand, surveillé. Idem pour les logs.
  7. Activez l’observabilité. Slow query log, performance schema (quand approprié), métriques clés d’InnoDB.
  8. Planifiez sauvegardes et restaurations comme un système. Testez le temps de restauration et la correction. Si vous ne pouvez pas restaurer, vous n’avez pas de sauvegardes.
  9. Testez la charge sur la même plateforme. Les benchmarks Docker Desktop servent aux impressions, pas à la capacité.
  10. Répétez les pannes. Kill -9 le conteneur en staging et vérifiez le comportement de récupération et les hypothèses d’intégrité des données.

Décisions minimales « day 1 » (écrivez-les)

  • Où est stocké /var/lib/mysql et sur quoi repose-t-il ?
  • Quel est le p95 et p99 attendu de latence fsync ?
  • Quelle est la limite mémoire du conteneur, et comment le buffer pool est-il dimensionné ?
  • Quel est le nombre maximum de connexions autorisées ?
  • Où vivent les binlogs et slow logs, et comment sont-ils rotés ?
  • Quelle est votre procédure de restauration, et quand l’avez-vous testée pour la dernière fois ?

FAQ

1) Dois-je exécuter MySQL/MariaDB dans Docker en production ?

Oui, si vous le considérez comme un service stateful avec un véritable ingénierie de stockage. Non, si votre plan est « ça marchait sur mon laptop ». Les conteneurs n’éliminent pas le besoin de discipline ops ; ils le rendent plus urgent.

2) Volume nommé ou bind mount pour /var/lib/mysql ?

Sur des serveurs Linux, les deux peuvent fonctionner. Les volumes nommés sont souvent plus simples opérationnellement. Les bind mounts conviennent si vous contrôlez le système de fichiers, les options de montage et les sauvegardes. Sur Docker Desktop, les volumes nommés surpassent généralement les bind mounts pour les bases.

3) overlay2 est mon driver Docker — suis-je condamné ?

Non. Vous n’êtes condamné que si vos fichiers de base vivent dans la couche inscriptible d’overlay2. Utilisez un vrai volume et vous éviterez la plupart des pires effets.

4) Est-ce que innodb_flush_log_at_trx_commit=2 est « sûr » ?

C’est une décision métier déguisée en réglage. Il peut entraîner la perte d’environ ~1 seconde de transactions en cas de crash (selon le timing). Si l’application peut réessayer en toute sécurité (idempotence) ou tolérer une perte mineure, cela peut être acceptable. Sinon, gardez la valeur à 1.

5) Pourquoi les performances sont correctes le matin et terribles l’après-midi ?

Souvent : contention de stockage (autres workloads sur le nœud), jobs de fond, sauvegardes, rotation de logs, ou garbage collection SSD sous écritures soutenues. Mesurez la latence disque dans le temps et corrélez avec les événements de charge.

6) Mon slow query log montre des requêtes simples qui prennent une éternité. Pourquoi ?

Parce qu’un SQL « simple » peut encore nécessiter un fsync au commit, attendre des verrous, ou attendre des flushs du buffer pool. Cherchez les waits de commit, I/O et verrous avant de réécrire les requêtes.

7) Dois-je désactiver InnoDB doublewrite pour accélérer ?

Seulement si vous comprenez totalement le risque et que votre pile de stockage garantit des écritures atomiques de page (beaucoup ne le font pas). Le désactiver peut améliorer les performances d’écriture mais augmente le risque de corruption sur écritures partielles. La plupart des équipes doivent le laisser activé et corriger la latence de stockage à la place.

8) Comment savoir si les tables temporaires me pénalisent ?

Vérifiez Created_tmp_disk_tables et surveillez l’usage disque où pointe tmpdir. Si les tables temporaires disque grimpent pendant les périodes lentes, vous avez probablement des requêtes qui débordent sur disque ou un tmpdir lent.

9) La réplication est-elle plus lente dans les conteneurs ?

Pas intrinsèquement. Mais les conteneurs facilitent l’erreur de déployer des primaires et réplicas sur des types de stockage différents, des quotas CPU différents, ou des voisins bruyants différents. La cohérence de l’infrastructure compte plus que « conteneur vs VM ».

10) Quel est le meilleur gain de performance unique ?

Placez votre datadir et vos redo/binlogs sur un stockage à faible latence et vérifiez le comportement de fsync. La plupart des plaintes « MySQL lent dans Docker » sont des problèmes de sémantiques de stockage déguisés en SQL.

Conclusion : prochaines étapes à faire aujourd’hui

Si vous exécutez MySQL/MariaDB dans Docker et que les performances sont mystérieuses, arrêtez de deviner. Faites ceci dans l’ordre :

  1. Confirmez que /var/lib/mysql est sur un vrai volume, pas sur overlay.
  2. Mesurez la latence des écritures synchrones sur ce volume (pas le « throughput » disque).
  3. Vérifiez les limites mémoire du conteneur et que le dimensionnement du buffer pool correspond à la réalité.
  4. Vérifiez les options de durabilité et confirmez qu’elles correspondent au contrat métier.
  5. Déplacez tmpdir/logs/binlogs sur le bon stockage et faites leur rotation.

Puis optimisez les requêtes. Puis les index. Pas l’inverse.

← Précédent
Panique du noyau : quand Linux dit « non » en public
Suivant →
Bases SR-IOV sur Debian 13 : pourquoi ça échoue et comment déboguer la première fois

Laisser un commentaire