MySQL vs PostgreSQL : mémoire Docker — arrêter l’étouffement silencieux

Cet article vous a aidé ?

Ça commence par « la base de données a l’air un peu lente ». Puis vos tableaux de bord deviennent étranges : le CPU est bas, le disque semble correct, les requêtes ne semblent pas pire, mais l’application timeoute quand même. Vous redémarrez le conteneur et — miracle — tout redevient rapide. Pour une journée.

Ce n’est généralement pas un problème de requête. C’est la pression mémoire plus les limites du conteneur, le genre qui ne hurle pas dans les logs. Docker ne « bride » pas la mémoire comme il bride le CPU, mais vous pouvez tout à fait vous retrouver avec un effondrement de performance silencieux : tempêtes de reclamation, thrash de swap, blocages d’allocateur, amplification I/O, et des bases de données qui s’adaptent poliment en ralentissant.

Ce que signifie réellement « étranglement silencieux » dans Docker

Les limites mémoire de Docker ne sont pas comme les limites CPU. Pour le CPU, vous obtenez des compteurs de throttling clairs. Pour la mémoire, vous avez un mur dur (memory.max / --memory) puis l’une des deux choses suivantes se produit :

  • Le noyau réclame agressivement (cache de fichiers, mémoire anonyme, slabs), ce qui ressemble à « rien ne va mal » jusqu’à ce que la latence explose et que l’I/O grimpe.
  • Le tueur OOM termine votre processus, ce qui est au moins honnête.

Entre ces extrêmes se situe le milieu misérable : la base de données est vivante, mais elle se bat avec le noyau pour la mémoire et perd lentement. Voilà votre « étranglement silencieux ».

Le piège à trois couches : cache DB, cache noyau, limite du conteneur

Sur un hôte normal, les bases de données comptent sur deux niveaux de mise en cache :

  • Cache géré par la base (InnoDB buffer pool de MySQL, shared buffers de PostgreSQL).
  • Cache de pages du noyau (cache système de fichiers).

Placez-les dans un conteneur avec une limite mémoire stricte et vous avez ajouté une troisième contrainte : le cgroup. Maintenant chaque octet compte deux fois : une fois pour la mémoire de la base et une autre fois pour « tout le reste » (connexions, work memory, tri/hash, buffers de réplication, processus d’arrière-plan, bibliothèques partagées, overhead malloc). Le noyau se fiche que vous ayez « seulement changé une config » ; il comptabilise le RSS et le cache dans votre cgroup et réclame en fonction de la pression.

Pourquoi cela ressemble à un bug réseau ou stockage

La pression mémoire se manifeste par :

  • pics de latence p99 aléatoires
  • augmentation des temps de fsync/flush
  • plus de lectures I/O malgré un mix de requêtes stable
  • CPU qui a l’air « correct » parce que les threads sont bloqués dans le noyau
  • connexions qui s’accumulent, puis timeouts en cascade

Blague #1 : les bases de données sous pression mémoire sont comme des gens en manque de caféine — techniquement elles fonctionnent encore, mais chaque petite requête devient personnelle.

Faits et histoire pertinents

Quelques points de contexte qui changent la manière de penser « il suffit de définir une limite mémoire pour le conteneur ».

  1. PostgreSQL s’est toujours appuyé sur le cache de pages du système. Son architecture laisse intentionnellement beaucoup de mise en cache au noyau ; shared_buffers n’est pas censé englober « toute la mémoire ».
  2. Le buffer pool InnoDB de MySQL est devenu le cheval de bataille quand InnoDB a remplacé MyISAM (ère MySQL 5.5). Cela a ancré l’habitude de « rendre le buffer pool énorme ».
  3. Les cgroups Linux v1 et v2 comptabilisent la mémoire différemment au point de brouiller les tableaux de bord. Le même conteneur peut sembler « correct » sur v1 et « mystérieusement contraint » sur v2 si vous ne vérifiez pas les bons fichiers.
  4. Docker a amélioré les paramètres par défaut au fil du temps, mais les fichiers Compose codent encore des folklore dangereux. Vous voyez encore mem_limit défini sans aucun réglage DB correspondant, ce qui revient à jouer à la roulette performance.
  5. Le comportement du tueur OOM dans les conteneurs surprenait davantage auparavant. Les premières utilisations de conteneurs ont appris aux équipes que « l’hôte a beaucoup de RAM » n’importe peu si la limite du cgroup est basse.
  6. PostgreSQL a introduit le support des huge pages depuis longtemps, mais on l’utilise rarement en conteneur car c’est opérationnellement pénible et parfois incompatible avec des environnements contraints.
  7. MySQL/InnoDB a plusieurs consommateurs de mémoire en dehors du buffer pool (adaptive hash index, buffers par connexion, performance_schema, réplication). Ne dimensionner que le buffer pool est un classique piège à pieds.
  8. La mémoire par requête de PostgreSQL est souvent la vraie coupable. Un work_mem trop généreux multiplié par la concurrence est la façon dont vous « allouez accidentellement » jusqu’au mur du cgroup.

MySQL vs PostgreSQL : modèles mémoire en conflit avec les conteneurs

MySQL (InnoDB) : un gros pool, plus la mort par mille buffers

La culture d’optimisation MySQL est dominée par l’InnoDB buffer pool. Sur du bare metal, la règle empirique est souvent « 60–80 % de la RAM ». Dans les conteneurs, ce conseil devient dangereux à moins de redéfinir « RAM » en « limite du conteneur moins l’overhead ».

Seaux mémoire à prendre en compte :

  • InnoDB buffer pool : le chiffre clé. Si vous le réglez à 75 % de la limite du conteneur, vous avez déjà perdu.
  • InnoDB log buffer et mémoire liée au redo log : généralement petite, mais ne prétendez pas que c’est zéro.
  • Buffers par connexion : read buffer, sort buffer, join buffer, tmp tables. Multipliez par max connections. Puis multipliez par « le pic n’est jamais la moyenne ».
  • Performance Schema : peut être étonnamment significatif si tout est activé.
  • Réplication : relay logs, binlog caches, buffers réseau.

Sous pression mémoire, InnoDB peut continuer à fonctionner mais avec plus de lectures disque et plus de churn en arrière-plan. Cela peut ressembler à un ralentissement du stockage. Le stockage n’a pas empiré. Vous avez affamé le cache et forcé des lectures aléatoires.

PostgreSQL : shared_buffers n’est qu’une moitié de l’histoire

Postgres utilise la mémoire partagée (shared_buffers) pour mettre en cache les pages de données, mais il s’appuie aussi fortement sur le cache de pages du noyau. Ensuite, il ajoute une pile d’autres consommateurs :

  • work_mem par nœud de tri/hash, par requête, par connexion (et par worker parallèle). C’est là que les budgets de conteneur meurent.
  • maintenance_work_mem pour vacuum et construction d’index. Votre job nocturne lent peut devenir votre incident de jour.
  • Travailleurs autovacuum et background writer : ils n’utilisent pas seulement le CPU ; ils génèrent de l’I/O et peuvent amplifier la pression mémoire indirectement.
  • Overhead mémoire partagée, catalogues, contextes mémoire de connexion, extensions.

Pour Postgres, « la base est lente » sous des limites de conteneur signifie souvent que le noyau réclame le cache de pages et que Postgres effectue plus de lectures réelles. Pendant ce temps, quelques requêtes concurrentes peuvent gonfler la mémoire via work_mem. C’est une guerre sur deux fronts.

Schémas d’étranglement silencieux : MySQL vs Postgres

Schéma MySQL : buffer pool trop grand → peu de marge → cache de pages comprimé → comportement checkpoint/flush détérioré → pics d’I/O de lectures aléatoires → latence qui augmente sans erreur évidente.

Schéma Postgres : shared_buffers modéré mais work_mem généreux → pic de concurrence → croissance de la mémoire anonyme → pression cgroup → reclaim et/ou swap → temps d’exécution des requêtes explosent, parfois sans « requête fautive » unique.

Opinion opérationnelle : si vous conteneurisez des bases de données, arrêtez de penser en « pourcentage de la RAM hôte ». Pensez en « budget dur avec concurrence pire cas ». Vous serez moins populaire en revue de conception. Vous serez plus populaire à 3 h du matin.

Une citation, parce qu’elle reste vraie des décennies après : paraphrased ideaJim Gray : « La meilleure façon d’améliorer les performances est de les mesurer d’abord. »

Guide de diagnostic rapide

Voici l’ordre qui trouve rapidement les goulots d’étranglement lorsqu’une BD dans Docker « vient de ralentir ». Vous pouvez le faire en 10–15 minutes si vous gardez les mains stables et vos hypothèses faibles.

1) Confirmer le vrai budget mémoire du conteneur (pas ce que vous pensez avoir défini)

  • Vérifier le cgroup memory.max / docker inspect.
  • Vérifier si le swap est autorisé (memory.swap.max ou docker --memory-swap).
  • Vérifier si l’orchestrateur écrase les valeurs Compose.

2) Décider : reclamation, swap ou OOM ?

  • Chercher des kill OOM dans dmesg/journalctl.
  • Vérifier les événements mémoire du cgroup (memory.events sur v2).
  • Vérifier les fautes majeures de page, swap in/out.

3) Corréler avec le comportement mémoire de la BD

  • MySQL : taille du buffer pool, % de pages dirty, longueur de history list, usage de tables temporaires, max connections.
  • Postgres : shared_buffers, work_mem, tris/hashes actifs, création de fichiers temporaires, activité autovacuum, nombre de connexions.

4) Confirmer si le stockage est la victime ou le coupable

  • Mesurer les IOPS de lecture et la latence au niveau hôte.
  • Vérifier si les lectures ont augmenté après le début de la pression mémoire.
  • Vérifier le comportement fsync-intensif (checkpointing, WAL, flush redo).

5) Faire un changement sûr

Ne « tunez tout ». Vous ne saurez jamais ce qui l’a corrigé et vous casserez probablement autre chose. Choisissez un seul objectif : réduire l’appétit mémoire de la BD ou augmenter le budget du conteneur. Puis vérifiez avec les mêmes compteurs.

Tâches pratiques (commandes, sorties, décisions)

Voici des tâches réelles que vous pouvez exécuter aujourd’hui. Chacune inclut : commande, sortie exemple, ce que cela signifie, et la décision à prendre.

Task 1: Identify the container and its configured memory limit

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
NAMES           IMAGE                 STATUS
db-mysql        mysql:8.0             Up 3 days
db-postgres     postgres:16           Up 3 days
cr0x@server:~$ docker inspect -f '{{.Name}} mem={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}}' db-mysql
/db-mysql mem=2147483648 swap=2147483648

Signification : la mémoire est de 2 GiB. Le swap est égal à la mémoire, donc le conteneur peut swapper jusqu’à ~2 GiB (selon les paramètres). Ce n’est pas « gratuit ». C’est de la latence.

Décision : Si vous voyez le swap activé pour des BD sensibles à la latence, soit désactivez le swap pour le conteneur, soit dimensionnez la mémoire pour ne pas en avoir besoin.

Task 2: Verify cgroup v2 memory.max from inside the container

cr0x@server:~$ docker exec -it db-postgres bash -lc 'cat /sys/fs/cgroup/memory.max; cat /sys/fs/cgroup/memory.current'
2147483648
1967855616

Signification : limite 2 GiB, usage courant ~1.83 GiB. C’est proche du plafond.

Décision : Si memory.current reste proche de memory.max en charge normale, vous n’avez pas un problème de « pic ». Vous avez un problème de dimensionnement.

Task 3: Check cgroup memory pressure events (v2)

cr0x@server:~$ docker exec -it db-postgres bash -lc 'cat /sys/fs/cgroup/memory.events'
low 0
high 214
max 0
oom 0
oom_kill 0

Signification : les incréments de high indiquent une pression mémoire soutenue. Pas encore d’OOM, mais le noyau réclame.

Décision : Si high continue de grimper pendant les incidents, traitez-le comme un signal de première classe. Réduisez la consommation mémoire ou augmentez la limite.

Task 4: Find OOM kills from the host

cr0x@server:~$ sudo journalctl -k --since "2 hours ago" | grep -i oom | tail -n 5
Dec 31 09:12:44 server kernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker-8b1...,mems_allowed=0,oom_memcg=/docker/8b1...
Dec 31 09:12:44 server kernel: Killed process 27144 (mysqld) total-vm:3124280kB, anon-rss:1782400kB, file-rss:10240kB, shmem-rss:0kB

Signification : mysqld a été tué par le tueur OOM du cgroup. Ce n’est pas « MySQL a planté ». C’est « vous avez manqué de budget ».

Décision : Ne redémarrez pas en boucle. Corrigez d’abord le désalignement budget/config.

Task 5: Confirm swap activity on the host

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 st
 2  1  524288  31264  10240  88432  120  210  1820  2410  920 1640 12  8 61 19  0
 1  2  526336  29812  10320  87020  140  180  2100  1980  980 1710 10  7 58 25  0

Signification : swap-in (si) et swap-out (so) sont non nuls pendant la charge. C’est une taxe sur la latence.

Décision : Si la latence DB compte, évitez le swap. Augmentez la limite mémoire ou réduisez l’usage mémoire DB ; envisagez aussi de définir --memory-swap égal à --memory pour prévenir un usage excessif du swap (dépend de l’environnement).

Task 6: See container-level memory usage and page cache via docker stats

cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}'
NAME          MEM USAGE / LIMIT     MEM %     CPU %
db-mysql      1.95GiB / 2GiB        97.5%     35.2%
db-postgres   1.82GiB / 2GiB        91.0%     18.7%

Signification : les deux tournent près des limites. Attendez-vous à de la pression de reclamation et/ou des OOM.

Décision : Traitez toute valeur >90 % soutenue comme « mal configurée », pas comme « occupée ».

Task 7: Inspect per-process RSS inside the container (who is eating memory?)

cr0x@server:~$ docker exec -it db-mysql bash -lc 'ps -eo pid,comm,rss --sort=-rss | head'
  PID COMMAND           RSS
    1 mysqld        1789320
  112 bash            17520
   98 ps               4440

Signification : le RSS de mysqld est ~1.7 GiB. C’est avant le cache de pages et autres overheads.

Décision : Si le RSS de mysqld est proche de la limite, réduisez le InnoDB buffer pool et les buffers par connexion, ou augmentez la limite.

Task 8: MySQL—confirm InnoDB buffer pool size and other big knobs

cr0x@server:~$ docker exec -it db-mysql bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES WHERE Variable_name IN (\"innodb_buffer_pool_size\",\"max_connections\",\"tmp_table_size\",\"max_heap_table_size\",\"performance_schema\");"'
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 1610612736|
| max_connections         | 500       |
| tmp_table_size          | 67108864  |
| max_heap_table_size     | 67108864  |
| performance_schema      | ON        |
+-------------------------+-----------+

Signification : le buffer pool est à 1.5 GiB dans un conteneur de 2 GiB. Max connections est à 500, donc la mémoire par connexion peut dépasser la marge disponible.

Décision : Réduisez le buffer pool à un nombre défendable (souvent 40–60 % de la limite pour petits conteneurs) et réduisez max_connections ou ajoutez du pooling. Les conteneurs détestent les plafonds « au cas où ».

Task 9: MySQL—check if you’re doing lots of temp table work (often memory→disk amplification)

cr0x@server:~$ docker exec -it db-mysql bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE \"Created_tmp%tables\";"'
+-------------------------+--------+
| Variable_name           | Value  |
+-------------------------+--------+
| Created_tmp_disk_tables | 184220 |
| Created_tmp_tables      | 912340 |
| Created_tmp_files       | 2241   |
+-------------------------+--------+

Signification : une fraction significative des temp tables se déversent sur disque. Sous pression mémoire, cela empire et ressemble à une « régression stockage ».

Décision : Réduire les déversements temporaires (indexes, plans de requête), dimensionner soigneusement les paramètres temporaires, et assurer suffisamment de marge mémoire pour que les opérations temporaires ne soient pas forcées sur disque plus souvent.

Task 10: PostgreSQL—confirm shared_buffers, work_mem, and max_connections

cr0x@server:~$ docker exec -it db-postgres bash -lc 'psql -U postgres -d postgres -c "SHOW shared_buffers; SHOW work_mem; SHOW maintenance_work_mem; SHOW max_connections;"'
 shared_buffers
----------------
 512MB
(1 row)

 work_mem
----------
 64MB
(1 row)

 maintenance_work_mem
----------------------
 1GB
(1 row)

 max_connections
-----------------
 300
(1 row)

Signification : work_mem 64MB multiplié par la concurrence est un piège. maintenance_work_mem 1GB dans un conteneur de 2GB est une bombe à retardement quand les jobs de vacuum/index tournent.

Décision : Baisser work_mem et maintenance_work_mem, et compter sur du pooling de connexions. Si vous avez besoin d’un work_mem élevé, imposez des limites de concurrence ou utilisez des files au niveau applicatif.

Task 11: PostgreSQL—see temp files (a strong indicator of memory shortfalls or bad plans)

cr0x@server:~$ docker exec -it db-postgres bash -lc 'psql -U postgres -d postgres -c "SELECT datname, temp_files, temp_bytes FROM pg_stat_database ORDER BY temp_bytes DESC LIMIT 5;"'
  datname  | temp_files |  temp_bytes
-----------+------------+-------------
 appdb     |      12402 | 9876543210
 postgres  |          2 |      819200
(2 rows)

Signification : un grand temp_bytes suggère des tris/hashes qui déversent. Dans les conteneurs, déversements plus stormes de reclamation = utilisateurs tristes.

Décision : Trouver les requêtes les plus déversantes, optimiser les index, et définir work_mem en fonction de la concurrence, pas de l’optimisme.

Task 12: PostgreSQL—spot autovacuum pressure (it can look like random I/O “mystery”)

cr0x@server:~$ docker exec -it db-postgres bash -lc 'psql -U postgres -d postgres -c "SELECT relname, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"'
   relname   | n_dead_tup |        last_autovacuum
------------+------------+-------------------------------
 events     |    4821932 | 2025-12-31 08:44:12.12345+00
 sessions   |    1120044 | 2025-12-31 08:41:02.54321+00
(2 rows)

Signification : beaucoup de tuples morts signifie pression de vacuum. Le vacuum crée de l’I/O et peut aggraver le churn du cache, surtout avec des limites mémoire serrées.

Décision : Ajuster les seuils autovacuum par table et corriger l’amplification d’écriture. Ne « résolvez » pas ça en donnant une mémoire illimitée au vacuum dans un petit conteneur.

Task 13: Measure host I/O latency (are we forcing disk reads because cache is gone?)

cr0x@server:~$ iostat -x 1 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          11.22    0.00    6.11   18.33    0.00   64.34

Device            r/s     w/s   rkB/s   wkB/s  await  r_await  w_await  svctm  %util
nvme0n1         820.0   210.0 50240.0 18432.0  18.2    21.4      5.6   1.2   98.0

Signification : forte utilisation et read await élevé suggèrent que vous effectuez de vraies lectures et attendez. Si cela coïncide avec une pression mémoire, le disque est souvent juste le messager.

Décision : N’achetez pas immédiatement des disques plus rapides. Confirmez d’abord la famine du cache et le comportement de reclamation.

Task 14: Check major page faults (a reclaim/swap smell)

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' db-postgres); sudo cat /proc/$pid/stat | awk '{print "majflt="$12, "minflt="$10}'
majflt=48219 minflt=12984321

Signification : les fautes majeures indiquent que le processus a dû récupérer des pages depuis le disque (ou le swap). Sous pression, ce nombre augmente avec la latence.

Décision : Si les fautes majeures augmentent pendant les incidents, concentrez-vous sur la marge mémoire et le comportement du cache avant de blâmer uniquement les plans de requêtes.

Task 15: Verify Compose vs runtime settings (the “I set mem_limit” lie)

cr0x@server:~$ docker compose config | sed -n '/db-mysql:/,/^[^ ]/p'
db-mysql:
  image: mysql:8.0
  mem_limit: 2g
  environment:
    MYSQL_DATABASE: appdb
cr0x@server:~$ docker inspect -f 'mem={{.HostConfig.Memory}}' db-mysql
2147483648

Signification : le config Compose correspond au runtime ici. Dans beaucoup d’environnements ce ne sera pas le cas, car Swarm/Kubernetes/autres outils peuvent écraser.

Décision : Faites toujours confiance à l’inspection runtime plutôt qu’aux fichiers de config. Les fichiers de config sont des aspirations.

Trois mini-récits clients du terrain

1) Incident causé par une mauvaise hypothèse : « L’hôte a 64 GB, on est bons »

Une équipe SaaS de taille moyenne a déplacé un primaire MySQL dans Docker pour standardiser le déploiement. L’hôte avait beaucoup de RAM. Ils ont mis une limite conteneur à 4 GB parce que « le dataset est petit », et ont copié leur ancien innodb_buffer_pool_size depuis une VM : 3 GB. Ça a fonctionné. Pendant des semaines.

Puis une campagne marketing a frappé. Les connexions ont explosé, les buffers de tri ont été utilisés, et soudain la latence est passée de « correcte » à « que s’est-il passé à notre produit ». Le CPU n’était pas saturé. Le disque n’était pas saturé au début. Les serveurs applicatifs time-outaient.

Les premiers intervenants ont regardé les logs de requêtes, parce que c’est ce qu’on fait quand on est fatigué. Ils n’ont rien trouvé de dramatique. Ils ont redémarré le conteneur et ça a été mieux. Ce redémarrage a aussi effacé la fragmentation mémoire accumulée et les buffers au niveau connexion. C’était un placebo avec effets secondaires.

Finalement quelqu’un a vérifié les logs noyau et a vu la pression de reclamation du cgroup et des OOM kills occasionnels de processus auxiliaires. MySQL ne mourait pas toujours ; il vivait sur le fil et forçait le noyau à voler constamment le cache. Les lectures ont augmenté, le checkpointing est devenu plus moche, et chaque attente I/O s’est transformée en pic de latence visible par l’utilisateur.

La correction a été douloureusement ennuyeuse : réduire le buffer pool pour laisser une vraie marge, plafonner les connexions avec un pooler, et augmenter la limite du conteneur pour correspondre au profil réel de concurrence. Ce n’était pas le dataset le problème. C’était la charge.

2) Optimisation qui a mal tourné : « On augmente work_mem, c’est plus rapide »

Un service axé données faisait tourner PostgreSQL en conteneurs. Un ingénieur de bonne volonté a vu des tris déverser sur disque dans EXPLAIN (ANALYZE), et a augmenté work_mem de 4MB à 128MB. Les benchmarks se sont améliorés. Tout le monde a célébré et est retourné sur Slack.

Deux semaines plus tard, un incident. Pas pendant un déploiement — pire. Pendant un mardi normal. Un job batch s’est lancé, a exécuté plusieurs requêtes parallèles, et chaque requête utilisait plusieurs nœuds tri/hash. Multipliez ça par le nombre de connexions. Multipliez encore par les workers parallèles. Soudain le conteneur a atteint la pression mémoire et a commencé à réclamer agressivement.

La base n’a pas planté. Elle est juste devenue lente. Vraiment lente. Le job batch a ralenti, a maintenu des verrous plus longtemps, et a bloqué les transactions orientées utilisateur. Cela a généré davantage de retries côté application, ce qui a augmenté encore la concurrence. Boucle de rétroaction positive classique, sauf que personne n’était positif.

Ils ont reverté work_mem, mais les performances sont restées étranges parce que l’autovacuum avait pris du retard pendant le chaos. Une fois le vacuum rattrapé, tout est revenu à la normale. La vraie correction a été de dimensionner work_mem selon la concurrence pire cas, pas selon des benchmarks de requête unique, et d’isoler les workloads batch avec leurs propres budgets de ressources.

Blague #2 : augmenter work_mem dans un petit conteneur, c’est comme prendre une valise plus grande à la porte d’embarquement — vous vous sentez préparé jusqu’à ce que quelqu’un vous mesure.

3) La pratique ennuyeuse mais correcte qui a sauvé la mise : budgets, marge, et alarmes

Une autre équipe faisait tourner MySQL et PostgreSQL dans Docker à travers les environnements. Ils avaient une règle : chaque conteneur DB a une « fiche budget mémoire », un court document dans le repo listant la limite, les connexions pic attendues, et les pires allocations mémoire calculées.

Ils avaient aussi des alarmes sur les événements de pression mémoire du cgroup (v2) et sur l’activité swap, pas seulement « pourcentage mémoire du conteneur ». L’alerte n’était pas « vous êtes à 92 % ». C’était « memory.events high qui augmente rapidement ». C’est la différence entre un avertissement et une alarme incendie.

Un jour, une release de fonctionnalité a augmenté les requêtes de rapport concurrentes. Leurs dashboards ont signalé la montée de memory.events high alors que la latence utilisateur était encore acceptable. Ils ont limité la concurrence des rapports au niveau applicatif et planifié une augmentation de la limite du conteneur pour la prochaine fenêtre de maintenance.

Pas d’incident. Pas de drame. L’équipe s’est fait accuser d’« over-engineering » exactement une fois, ce qui est la preuve qu’on travaille correctement.

Erreurs courantes : symptôme → cause → correctif

1) Symptom: p99 latency spikes, CPU looks fine

Cause racine : pression de reclamation du noyau dans le cgroup ; les threads bloquent dans l’I/O ou les chemins d’allocateur.

Correctif : vérifier /sys/fs/cgroup/memory.events et memory.current. Réduire les paramètres mémoire de la BD ou augmenter la limite. Confirmer les fautes majeures et l’I/O await.

2) Symptom: random disk read spikes after “tightening” memory limits

Cause racine : cache de pages comprimé ; cache DB trop petit ou trop grand par rapport au conteneur ; le reclaim évince du cache utile.

Correctif : laisser de la marge pour le page cache et la mémoire non-buffer. Pour MySQL, ne pas régler le buffer pool près de la limite. Pour Postgres, ne pas supposer que shared_buffers remplace le page cache.

3) Symptom: database restarts with no clear DB error

Cause racine : OOM kill du cgroup. La BD n’a pas « crashé », elle a été terminée par le noyau.

Correctif : vérifier journalctl -k. Corriger le dimensionnement mémoire, réduire la concurrence, et éviter la dépendance au swap.

4) Symptom: “It’s slow until we restart the container”

Cause racine : pression mémoire accumulée, gonflement des connexions, churn du cache, retard d’autovacuum, ou fragmentation ; le redémarrage réinitialise les symptômes.

Correctif : mesurer la pression mémoire et les internes DB. Ajouter du pooling, redimensionner les buffers, et planifier correctement les opérations de maintenance.

5) Symptom: Postgres temp files explode and disks fill

Cause racine : work_mem insuffisant pour la forme des requêtes ou mauvais plans ; sous pression mémoire, les déversements deviennent plus fréquents.

Correctif : identifier les requêtes avec le plus de temp_bytes, optimiser les index, et définir work_mem selon la concurrence. Envisager des timeouts pour l’analytics incontrôlé sur de l’OLTP.

6) Symptom: MySQL uses “way more memory than innodb_buffer_pool_size”

Cause racine : buffers par connexion, performance_schema, overhead d’allocateur ; aussi le cache de pages OS et les métadonnées de système de fichiers comptés dans le cgroup.

Correctif : plafonner les connexions, tuner les buffers par thread, envisager de désactiver ou réduire performance_schema si approprié, et laisser de la marge.

7) Symptom: container memory usage seems capped but host swap grows

Cause racine : le swap est autorisé et le noyau pousse des pages froides ; le conteneur « reste vivant » mais devient plus lent.

Correctif : désactiver le swap pour le conteneur ou régler la limite de swap égale à la limite mémoire ; s’assurer que la charge tient en RAM.

8) Symptom: “We set mem_limit in Compose, but it doesn’t apply in prod”

Cause racine : l’orchestrateur écrase ; les champs Compose diffèrent selon le mode ; la config runtime diverge.

Correctif : inspecter les paramètres runtime et les codifier dans le mécanisme de déploiement réel (Swarm/Kubernetes configs). Faire confiance à docker inspect, pas au YAML folklorique.

Listes de contrôle / plan pas à pas

Étapes pas à pas : arrêter l’étranglement silencieux pour MySQL dans Docker

  1. Confirmer la vraie limite : docker inspect et /sys/fs/cgroup/memory.max.
  2. Réserver une marge : viser au moins 25–40 % de la limite pour l’utilisation non-buffer + page cache pour les petits conteneurs (sous 8 GB). Oui, c’est conservateur. C’est voulu.
  3. Fixer innodb_buffer_pool_size selon un budget, pas un ressenti : pour un conteneur 2GB, 768MB–1.2GB est souvent sensé selon la charge et le pooling de connexions.
  4. Plafonner les connexions : réduire max_connections et ajouter du pooling côté applicatif si possible.
  5. Auditer les buffers par thread : garder sort/join/read buffers modestes sauf si vous comprenez la concurrence pire cas.
  6. Surveiller les tables temporaires disque : la montée des temp tables disque signifie que vous payez de l’I/O pour des décisions mémoire.
  7. Alerter sur la pression cgroup : suivre memory.events high/max/oom_kill.
  8. Retester en charge de concurrence : des benchs à 10 connexions sont sympathiques ; en prod c’est 300 parce que quelqu’un a oublié de fermer des sockets.

Étapes pas à pas : arrêter l’étranglement silencieux pour PostgreSQL dans Docker

  1. Confirmer la limite : encore une fois, ne discutez pas avec les cgroups.
  2. Définir shared_buffers modérément : en conteneur, des valeurs énormes peuvent étrangler tout le reste. Beaucoup d’OLTP vont bien avec 256MB–2GB selon le budget.
  3. Faire de work_mem un nombre conscient de la concurrence : commencer petit (4–16MB) et augmenter chirurgicalement pour des rôles/requêtes spécifiques si besoin.
  4. Ne laissez pas la maintenance manger la machine : dimensionner maintenance_work_mem pour que l’autovacuum et les builds d’index ne puissent pas affamer le reste.
  5. Utiliser le pooling : les connexions Postgres sont coûteuses, et en conteneurs l’overhead devient plus pénible.
  6. Suivre temp_bytes et retard autovacuum : les déversements et la dette de vacuum sont des signaux précoces.
  7. Alerter sur les événements de pression mémoire : traiter memory.events high comme une menace pour le SLO de performance.
  8. Séparer OLTP et analytics : si vous ne pouvez pas, imposez des timeouts et limites de concurrence.

Hygiène au niveau conteneur (les deux bases)

  • Définir intentionnellement les limites mémoire : éviter « petites limites par sécurité » sans tuning. Ce n’est pas de la sécurité ; ce sont des incidents différés.
  • Décider d’une politique swap : pour les bases, « swap comme filet de secours » devient souvent « swap comme mode de vie permanent ».
  • Observer depuis l’hôte et depuis l’intérieur : swap hôte, I/O hôte, et événements cgroup comptent tous.
  • Garder les redémarrages honnêtes : si un redémarrage « corrige » le problème, vous avez une fuite, un backlog, un cycle de pression mémoire, ou un désalignement de cache. Traitez-le comme un indice, pas comme un remède.

FAQ

1) Is Docker “throttling” my database memory?

Pas comme le throttling CPU. Les limites mémoire créent de la pression et des échecs durs (reclaim, swap, OOM). Le « throttling » est la base qui tourne plus lentement parce qu’elle ne peut pas garder les pages chaudes en mémoire.

2) Why does performance improve after restarting the DB container?

Les redémarrages réinitialisent les caches, libèrent la mémoire par-connexion accumulée, effacent la fragmentation, et parfois laissent le noyau reconstruire le page cache. C’est comme éteindre la radio pour réparer un pneu crevé : le bruit change, le problème reste.

3) For MySQL, can I set innodb_buffer_pool_size to 80% of container memory?

Généralement non. Dans les petits conteneurs, vous avez besoin d’une marge pour les buffers par connexion, les threads d’arrière-plan, performance_schema, et un peu de page cache. Commencez plus bas et prouvez que vous pouvez en allouer davantage.

4) For Postgres, should shared_buffers be huge in containers?

Pas par défaut. Postgres profite du page cache OS, et les conteneurs utilisent toujours le cache noyau dans le même budget mémoire. Sur-allouer shared_buffers peut affamer work_mem, autovacuum, et le page cache.

5) What’s the fastest signal of memory pressure in cgroup v2?

/sys/fs/cgroup/memory.events. Si high augmente pendant des problèmes de latence, vous réclamez. Si oom_kill s’incrémente, vous perdez des processus.

6) Should I disable swap for database containers?

Si vous tenez à une latence cohérente, oui — la plupart du temps. Le swap peut prévenir les crashs mais se transforme souvent en panne lente. Si vous conservez le swap, surveillez l’I/O de swap et fixez des limites réalistes.

7) Why does disk look slow only when the DB is “busy”?

Parce que « occupé » peut signifier « affamé de cache ». Avec moins de cache, la BD effectue plus de lectures réelles et écrit plus de données temporaires. Le disque n’a pas empiré ; vous l’avez forcé à travailler davantage.

8) Can I fix this purely by increasing the container memory limit?

Parfois. Mais si la BD est configurée pour s’étendre indéfiniment (trop de connexions, work_mem trop grand, buffers trop larges), elle finira par atteindre le nouveau plafond aussi. Associez augmentation de limite et discipline de configuration.

9) Which is more prone to container memory surprises: MySQL or PostgreSQL?

Des surprises différentes. Les équipes MySQL surdimensionnent souvent les buffer pools et oublient l’overhead par connexion. Les équipes Postgres sous-estiment souvent comment work_mem se multiplie avec la concurrence. Les deux peuvent « sembler correctes » jusqu’à ce qu’elles ne le soient plus.

10) What if I’m using Kubernetes instead of plain Docker?

Les principes sont identiques : cgroups, reclaim, OOM. La mécanique change (requests/limits, comportement d’éviction). Votre travail reste le même : aligner les réglages mémoire BD avec la vraie limite appliquée et observer les signaux de pression.

Étapes suivantes

Faites-les dans l’ordre. Elles sont conçues pour transformer une plainte vague « Docker est bizarre » en système contrôlé.

  1. Mesurer la vraie limite du conteneur et si le swap est permis. Notez-la là où les humains peuvent la trouver.
  2. Vérifier les événements de pression mémoire du cgroup pendant une période lente. Si high augmente, vous avez votre preuve.
  3. Choisir une base et construire un budget mémoire : buffer/cache + par-connexion/par-requête + maintenance + overhead. Faites-le pessimiste.
  4. Réduire la concurrence avant de chasser les micro-optimisations : pooling de connexions, mise en file, limites de taux pour l’analytics.
  5. Retuner les paramètres DB selon le budget, pas l’inverse.
  6. Ajouter des alertes sur les signaux de pression (pas seulement le pourcentage d’utilisation). La pression est ce que ressentent les utilisateurs.
  7. Test de charge avec une concurrence réaliste. Si votre test ne peut pas déclencher la pression, c’est un test unitaire déguisé en test de performance.

Si vous faites tout cela, vous n’arrêterez pas seulement l’étranglement silencieux — vous empêcherez son retour déguisé en « problème aléatoire » réseau ou stockage. Et vous pourrez dormir la nuit, ce qui est le vrai SLA.

← Précédent
DNS : Mouvements puissants dig/drill — commandes qui résolvent 90 % des mystères DNS
Suivant →
Apache pour WordPress : modules et règles qui plantent les sites (et comment les réparer)

Laisser un commentaire