MySQL vs PostgreSQL : « CPU 100% » — comment prouver que ce sont les requêtes, pas le matériel

Cet article vous a aidé ?

CPU 100% est l’équivalent en production d’une alarme incendie hors de portée : bruyant, persistant, et tout le monde veut que ça s’arrête immédiatement. L’instinct premier est de blâmer « la machine » (ou l’instance cloud, ou l’hyperviseur, ou les voisins). Cet instinct est souvent faux.

Ce guide de terrain explique comment prouver ce qui se passe réellement quand MySQL ou PostgreSQL saturent le CPU : si vous êtes lié au CPU par des requêtes, par du travail en arrière-plan, par des blocages de verrous, ou si vous mesurez simplement la mauvaise chose. Il s’adresse à ceux qui doivent expliquer leurs conclusions à des ingénieurs sceptiques et à une finance encore plus sceptique.

Ce que « CPU 100% » signifie vraiment pour les bases de données

Quand quelqu’un dit « le CPU est à 100% », demandez : le CPU de qui ? Système total ? Un seul cœur ? Temps utilisateur ? Temps noyau ? Steal ? Un seul thread chaud ? Une douzaine de workers à 80 % chacun ? La différence décide si vous optimisez les requêtes, changez la configuration, ou appelez votre fournisseur cloud avec sérieux.

Lié au CPU vs lié à l’I/O

Les bases de données consomment du CPU pour quelques raisons prévisibles :

  • Travail d’exécution : balayage de lignes, évaluation des prédicats, calculs d’agrégats, tris, hachages, jointures.
  • Travail de planification : génération et estimation des plans de requête (généralement faible, mais peut monter sur des requêtes complexes ou des statistiques invalides).
  • Travail de concurrence : verrous, loquets, gestion MVCC, boucles d’attente active, overhead de changement de contexte.
  • Travail en arrière-plan : vacuum, purge/undo cleanup, checkpoints, application de réplication, analyse des statistiques.
  • Crypto/compression : TLS, chiffrement au repos, compression, hachage pour sommes de contrôle.

« Lié au CPU » signifie que la base de données exécute activement des instructions. « Lié à l’I/O » signifie qu’elle attend principalement des lectures/écritures. Les deux peuvent afficher une « charge élevée », mais une seule se résout en ajoutant du CPU.

Moyenne de charge (load average) n’est pas utilisation CPU. La charge compte les tâches exécutables plus les tâches en sommeil non interrompable (souvent I/O disque). Donc une charge élevée avec un CPU modéré est généralement une histoire d’I/O. Un CPU élevé avec une charge modeste peut être un worker chaud qui monopolise un cœur.

Une autre mine antipersonnel : le steal time (virtualisation). Votre VM pense être occupée, mais l’hyperviseur lui retire des cycles. Ce n’est pas « des requêtes », mais cela ressemblera à de la pression CPU tant que vous ne le mesurez pas.

Blague #1 : Si votre base de données est à 100% CPU et que la première solution est « ajouter un index », félicitations — vous avez rejoint la religion ancienne du culte du cargo des performances.

Mon critère de preuve

Vous n’avez pas besoin d’un doctorat en microarchitecture. Vous avez besoin de preuves reproductibles qui lient la consommation CPU à des sessions spécifiques et finalement à des empreintes de requête spécifiques. Si vous pouvez produire :

  1. Une preuve au niveau OS que le CPU est consommé par le processus de la base de données (pas irq, pas noyau, pas steal),
  2. Une preuve au niveau BD des sessions et motifs de requête responsables,
  3. Une preuve au niveau plan expliquant pourquoi ces requêtes sont maintenant coûteuses,

…alors vous pouvez dire « ce sont les requêtes » sans donner l’impression de deviner.

Une citation opérationnelle vaut plus que dix débats sur Slack. Comme l’a dit Werner Vogels (CTO d’Amazon) : « Everything fails, all the time. » Votre travail est d’échouer avec des reçus.

MySQL vs PostgreSQL : comment la saturation CPU diffère

MySQL et PostgreSQL peuvent tous deux faire fondre des CPU. Ils le font simplement différemment, et les leviers de diagnostic ne sont pas les mêmes.

PostgreSQL : processus par session, introspection claire

PostgreSQL exécute typiquement un processus backend par connexion (plus des processus d’arrière-plan). Cela signifie que quand le CPU est saturé, vous pouvez souvent cartographier rapidement : « PID 12345 est chaud » → « ce PID est un backend » → « il exécute la requête X ». C’est un cadeau. Profitez-en.

Postgres expose aussi une télémétrie riche centrée sur la requête : pg_stat_activity, pg_stat_statements, et auto_explain peuvent indiquer ce qui s’exécute, ce qui est coûteux dans le temps, et à quoi ressemblent les plans. Si vous êtes sérieux, activez-les en production (avec précaution).

MySQL : exécution basée sur des threads, performance_schema est le sérum de vérité

MySQL est plus centré sur les threads à l’intérieur d’un plus petit nombre de processus, donc « PID est chaud » est moins spécifique. Vous vous appuyez sur performance_schema, le slow query log, et les digests de statements. Si vous comptez encore sur « SHOW PROCESSLIST et l’intuition », vous volez sans instruments.

InnoDB introduit sa propre saveur CPU : contention sur mutex, churn du buffer pool, purge/undo cleanup, et vidage de pages peuvent tous créer une pression CPU qui n’est pas directement « mon SELECT est lent », mais qui est néanmoins causée par la charge et le schéma.

La grande différence pratique : la rapidité à isoler un coupable

Dans Postgres, isoler les principaux coupables commence souvent par pg_stat_statements et se termine par EXPLAIN (ANALYZE, BUFFERS). Dans MySQL, vous commencez souvent par les digests de statements et terminez par EXPLAIN ANALYZE (8.0+) ainsi que du travail d’index et de schéma. Les deux vous obligent à accepter une vérité inconfortable : la plupart des « problèmes matériels » sont en fait une amplification de travail — vos requêtes font plus de travail que vous ne le pensez.

Plan de diagnostic rapide (premier/deuxième/troisième)

Premier : confirmer que c’est vraiment le CPU, et de qui

  1. Vérifier la répartition CPU : utilisateur vs noyau vs iowait vs steal.
  2. Identifier les processus/threads chauds : est-ce mysqld/postgres, ou autre chose ?
  3. Vérifier la file d’exécution : les threads sont-ils exécutables (lié CPU) ou bloqués (I/O/verrous) ?

Deuxième : mapper la chaleur de l’OS à l’activité BD

  1. Postgres : PID chaud → pg_stat_activity pour obtenir la requête et l’état.
  2. MySQL : thread chaud → performance_schema pour obtenir le digest de statement, l’utilisateur, l’hôte.
  3. Vérifier la concurrence : êtes-vous saturé CPU à cause de nombreuses requêtes moyennes ou d’une seule requête monstrueuse ?

Troisième : décider si c’est une régression de plan, des stats, de la contention, ou du travail en arrière-plan

  1. Régression de plan : la même requête a obtenu un plan pire après croissance des données, dérive des stats, ou changement de paramètres.
  2. Index manquants/non utilisés : « il scanne parce qu’il peut », ou « il utilise le mauvais index parce que les stats mentent ».
  3. Contention verrous/loquets : le CPU peut monter à cause de boucles actives, plus un effondrement du débit.
  4. Travaux en arrière-plan : autovacuum (Postgres), purge/flush (InnoDB), application de réplication.

Si vous ne pouvez pas répondre à ces trois étapes en 10 minutes, vous n’avez pas un « problème de performance ». Vous avez un problème d’observabilité.

Construire une chaîne de preuves : de l’OS au texte de la requête

Quand le CPU plafonne, les gens proposent des correctifs aléatoires : redémarrer la BD, basculer, ajouter des replicas, scaler, « tuner le noyau », sacrifier une chèvre. Ne faites pas de choses aléatoires. Construisez une chaîne de preuves que vous pourrez montrer plus tard.

Ce qui compte comme preuve

  • OS : le CPU est occupé en espace utilisateur (ou noyau) et c’est le processus de la base de données.
  • BD : les principaux consommateurs CPU se corrèlent avec des motifs SQL spécifiques (digests) et des clients.
  • Plan : la requête coûteuse est coûteuse pour une raison (lignes lues, boucles, tris, débordements de hachage, ordre de jointure mauvais).
  • Corrélation de changement : un déploiement, suppression d’index, dérive des stats, croissance des données, ou changement de config a précédé le pic.

Ce qui ne compte pas comme preuve

  • « Le CPU est élevé, donc l’instance est trop petite. »
  • « Mon ami a dit que Postgres est plus lent que MySQL pour les lectures. »
  • « Nous n’avons rien changé. » (Si, vous avez changé. Les données ont changé. Le trafic a changé. Le monde a changé.)

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

Tout ce qui suit est destiné à être exécutable sur un hôte Linux typique avec MySQL ou PostgreSQL. Chaque tâche inclut : la commande, ce que vous recherchez dans la sortie, et quelle décision elle déclenche. Ne les exécutez pas tous en même temps sur une machine mourante. Choisissez le plus petit marteau qui répond à la question suivante.

Tâche 1 : Confirmer la répartition CPU (user/system/iowait/steal)

cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.1.0 (db01)  12/31/2025  _x86_64_  (16 CPU)

12:01:10 PM  CPU   %usr %nice  %sys %iowait %irq %soft %steal %idle
12:01:11 PM  all   92.40  0.00  6.80   0.20 0.00  0.10   0.00  0.50
12:01:11 PM    7   99.50  0.00  0.40   0.00 0.00  0.00   0.00  0.10

Sens : %usr élevé implique exécution de requêtes/calcul. %sys élevé suggère du travail noyau (réseau, système de fichiers, contention). %steal élevé suggère voisins bruyants / surallocation.

Décision : Si %steal est non négligeable, arrêtez de discuter SQL et validez la contention de l’hôte. Si %iowait domine, vous êtes probablement limité par l’I/O, pas le CPU.

Tâche 2 : Trouver le processus le plus chaud

cr0x@server:~$ top -b -n 1 | head -20
top - 12:01:22 up 12 days,  3:12,  2 users,  load average: 18.32, 17.90, 16.01
Tasks: 412 total,   2 running, 410 sleeping,   0 stopped,   0 zombie
%Cpu(s): 93.1 us,  6.5 sy,  0.0 ni,  0.3 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  64218.3 total,   2311.7 free,  21342.5 used,  40564.1 buff/cache

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 5123 mysql     20   0 6432108  12.8g  49288 S 1342.0  20.5  93:21.11 mysqld

Sens : C’est réellement le processus de la base de données qui consomme du CPU. Notez aussi : %CPU peut dépasser 100% car il est par cœur.

Décision : Si ce n’est pas le processus BD, arrêtez. Poursuivez le vrai coupable (outil de sauvegarde, expéditeur de logs, agent, threads noyau).

Tâche 3 : CPU au niveau thread pour voir si c’est « un mauvais acteur » ou « mort par mille coupures »

cr0x@server:~$ pidstat -t -p 5123 1 5
Linux 6.1.0 (db01)  12/31/2025  _x86_64_  (16 CPU)

12:01:31 PM   UID      TGID       TID    %usr %system  %CPU   CPU  Command
12:01:32 PM   112      5123      6120   98.00    1.00 99.00     7  mysqld
12:01:32 PM   112      5123      6121   76.00    2.00 78.00     3  mysqld
12:01:32 PM   112      5123      6177   10.00    0.00 10.00     9  mysqld

Sens : Quelques threads roulent à fond. Cela correspond souvent à des connexions spécifiques ou à des workers internes.

Décision : Si un thread domine, cherchez une seule requête/session. Si beaucoup de threads sont chauds, cherchez un changement de charge ou une contention globale.

Tâche 4 : Vérifier la pression sur la file d’exécution et les changements de contexte

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
18  0      0 236812  91872 41231840  0    0     3    44 4211 9802 92  6  1  0  0
21  0      0 232104  91872 41241432  0    0     0    32 4877 12120 93  6  1  0  0

Sens : r est le nombre de threads exécutables. Si r est constamment supérieur au nombre de CPU, vous faites la queue pour le CPU. Un cs élevé peut indiquer une concurrence excessive ou une contention de verrous.

Décision : Si les threads exécutables sont nombreux, envisagez des limites de connexions, le dimensionnement des pools, et la concurrence des requêtes. Si du swap apparaît, c’est une autre urgence.

Tâche 5 : Vérifier le steal time et la limitation CPU (réalité cloud)

cr0x@server:~$ sar -u 1 3
Linux 6.1.0 (db01)  12/31/2025  _x86_64_  (16 CPU)

12:01:55 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
12:01:56 PM     all     89.12      0.00      7.01      0.20      3.10      0.57
12:01:57 PM     all     88.90      0.00      7.30      0.10      3.20      0.50

Sens : %steal est du temps CPU que vous vouliez mais n’avez pas obtenu. Cela peut imiter « la BD est devenue plus lente » sans aucun changement de requête.

Décision : Si le steal est élevé, validez le placement des hôtes, la classe d’instance, et si les crédits de burst sont épuisés. Ne corrigez pas les requêtes pour compenser une mauvaise colocation.

Tâche 6 : Postgres — mapper le PID chaud à la requête et à l’état d’attente

cr0x@server:~$ sudo -u postgres psql -x -c "select pid, usename, application_name, client_addr, state, wait_event_type, wait_event, now()-query_start as age, left(query,200) as query from pg_stat_activity where state<>'idle' order by age desc limit 5;"
-[ RECORD 1 ]-----+--------------------------------------------
pid               | 28741
usename           | app_user
application_name  | api
client_addr       | 10.20.3.41
state             | active
wait_event_type   |
wait_event        |
age               | 00:02:14.12031
query             | SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= now()-interval '30 days' GROUP BY o.customer_id;

Sens : wait_event vide + state=active signifie qu’il est sur le CPU (ou au moins pas en attente d’un wait suivi). Le texte de la requête est votre suspect.

Décision : Si vous voyez beaucoup d’actifs exécutant le même motif, vous avez probablement un endpoint chaud. Si les wait events montrent des verrous, traitez cela comme de la contention, pas comme « il faut plus de CPU ».

Tâche 7 : Postgres — obtenir les principaux coupables style CPU par temps total

cr0x@server:~$ sudo -u postgres psql -c "select calls, round(total_exec_time::numeric,1) as total_ms, round(mean_exec_time::numeric,2) as mean_ms, rows, left(query,120) as query from pg_stat_statements order by total_exec_time desc limit 10;"
 calls | total_ms | mean_ms | rows  | query
-------+----------+---------+-------+------------------------------------------------------------
  8231 | 912345.4 | 110.81  | 15321 | SELECT o.customer_id, count(*) FROM orders o JOIN order_items...
 12001 | 610112.7 | 50.84   | 12001 | SELECT * FROM sessions WHERE token = $1

Sens : Vous avez maintenant des empreintes de requête classées par temps d’exécution total (un proxy de consommation CPU, mais pas identique).

Décision : Un total_exec_time élevé provoque des incidents. Un mean_exec_time élevé provoque la latence tail. Décidez ce que vous corrigez en premier.

Tâche 8 : Postgres — prouver où passe le temps avec EXPLAIN (ANALYZE, BUFFERS)

cr0x@server:~$ sudo -u postgres psql -c "explain (analyze, buffers, verbose) SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= now()-interval '30 days' GROUP BY o.customer_id;"
HashAggregate  (cost=... rows=... width=16) (actual time=2150.113..2150.901 rows=4821 loops=1)
  Buffers: shared hit=120344 read=8123
  ->  Hash Join  (cost=... ) (actual time=310.123..2011.221 rows=241233 loops=1)
        Hash Cond: (i.order_id = o.id)
        Buffers: shared hit=120344 read=8123
        ->  Seq Scan on public.order_items i  (actual time=0.021..1190.332 rows=5200000 loops=1)
              Buffers: shared hit=99844 read=6500
        ->  Hash  (actual time=305.110..305.111 rows=402113 loops=1)
              ->  Seq Scan on public.orders o  (actual time=0.030..221.900 rows=402113 loops=1)
                    Filter: (created_at >= (now() - '30 days'::interval))

Sens : Les scans séquentiels et les grands agrégats de hachage sont gourmands en CPU. Les Buffers montrent hits de cache vs lectures ; cette requête fait les deux et passe quand même beaucoup de temps en calcul.

Décision : Si elle scanne des millions de lignes pour répondre à une petite question, ajoutez/ajustez des index, réécrivez la requête, ou pré-agrégez. Si elle est déjà majoritairement en cache mais reste lente, la logique est gourmande en CPU (hachage, tri, fonctions).

Tâche 9 : MySQL — identifier les principaux digests de statement par temps CPU

cr0x@server:~$ mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s, ROUND(SUM_LOCK_TIME/1e12,2) AS lock_s, ROUND(SUM_ROWS_EXAMINED/COUNT_STAR,0) AS avg_rows_examined FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 5\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT o.customer_id, COUNT ( * ) FROM orders o JOIN order_items i ON i.order_id = o.id WHERE o.created_at >= ? GROUP BY o.customer_id
COUNT_STAR: 8120
total_s: 945.21
lock_s: 2.11
avg_rows_examined: 510220

Sens : Ce digest consomme le plus de temps de statement. avg_rows_examined énorme est un signal évident « j’analyse trop ».

Décision : Ciblez d’abord les pires digests. Si lock_s est élevé par rapport au total, il pourrait s’agir de contention de verrous, pas de CPU pur. Si rows examined est énorme, recherchez des indexes et voies d’accès.

Tâche 10 : MySQL — voir les threads actifs et ce qu’ils font

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;"
Id	User	Host	db	Command	Time	State	Info
9312	app	10.20.3.41:51844	prod	Query	132	Sending data	SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= NOW()-INTERVAL 30 DAY GROUP BY o.customer_id
9441	app	10.20.3.52:52011	prod	Query	98	Copying to tmp table	SELECT ... ORDER BY ...

Sens : Les requêtes actives de longue durée sont visibles. « Copying to tmp table » et « Sending data » signifient souvent de grands résultats intermédiaires, des tris, ou un mauvais index.

Décision : Si une requête domine, vous pouvez la tuer chirurgicalement (avec coordination). Si de nombreuses requêtes similaires sont longues, la solution est systémique.

Tâche 11 : MySQL — expliquer le plan et confirmer si vous scannez

cr0x@server:~$ mysql -e "EXPLAIN SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= NOW()-INTERVAL 30 DAY GROUP BY o.customer_id\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: o
type: range
possible_keys: idx_orders_created_at
key: idx_orders_created_at
rows: 401233
Extra: Using where; Using temporary; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: i
type: ref
possible_keys: idx_order_items_order_id
key: idx_order_items_order_id
rows: 12
Extra:

Sens : « Using temporary; Using filesort » est la taxe classique CPU+mémoire pour le groupement/tris sans chemin adapté.

Décision : Envisagez des index composites pour supporter le group by, réduire le nombre de lignes tôt, ou changer la forme de la requête. Si le range scan touche encore des centaines de milliers de lignes par appel, vous payez par requête.

Tâche 12 : MySQL 8 — utiliser EXPLAIN ANALYZE pour voir le comportement réel

cr0x@server:~$ mysql -e "EXPLAIN ANALYZE SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= NOW()-INTERVAL 30 DAY GROUP BY o.customer_id;"
-> Group aggregate: count(0)  (actual time=0.333..2150.221 rows=4821 loops=1)
    -> Nested loop inner join  (actual time=0.112..2010.011 rows=241233 loops=1)
        -> Index range scan on orders using idx_orders_created_at  (actual time=0.041..220.333 rows=402113 loops=1)
        -> Index lookup on order_items using idx_order_items_order_id (order_id=o.id)  (actual time=0.003..0.004 rows=12 loops=402113)

Sens : La boucle imbriquée multipliée par 402k lignes est la preuve évidente. Voilà comment des index « fins » créent encore une énorme facture CPU.

Décision : Envisagez de réécrire pour pré-agréger order_items, ou réduire l’ensemble d’orders. Parfois, une stratégie de jointure différente ou un index couvrant peut réduire drastiquement les boucles.

Tâche 13 : Vérifier la contention de verrous qui se déguise en problème CPU (Postgres)

cr0x@server:~$ sudo -u postgres psql -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where state='active' group by 1,2 order by 3 desc;"
 wait_event_type |     wait_event      | count
-----------------+---------------------+-------
 Lock            | relation            |    18
                 |                     |     6

Sens : Beaucoup d’actifs en attente de verrous relationnels signifie que vous pouvez voir un effondrement du débit et des comportements CPU bizarres (spin, changement de contexte, tempêtes de retry).

Décision : Arrêtez d’optimiser les SELECT. Identifiez les sessions bloqueuses et le DDL ou la transaction longue qui en est la cause.

Tâche 14 : Vérifier la pression interne InnoDB (MySQL)

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,140p'
------------
TRANSACTIONS
------------
Trx id counter 93211245
History list length 81234
...
--------
SEMAPHORES
--------
OS WAIT ARRAY INFO: reservation count 221234, signal count 221100
Mutex spin waits 912334, rounds 1823344, OS waits 22344

Sens : Une grosse history list length peut impliquer un retard de purge (souvent dû à des transactions longues). Des mutex spin waits élevés peuvent indiquer une contention qui brûle du CPU sans que des « requêtes lentes » ne sautent forcément aux yeux.

Décision : Si la purge est en retard, trouvez les transactions longues et corrigez le comportement applicatif. Si la contention est élevée, réduisez la concurrence, examinez les indexes/tables chauds, et envisagez des changements de config — après avoir identifié le déclencheur de charge.

Tâche 15 : Prouver si vous swappez ou thrash mémoire (ou les deux)

cr0x@server:~$ free -m
               total        used        free      shared  buff/cache   available
Mem:           64218       21342        2311        1024       40564       40112
Swap:              0           0           0

Sens : Pas d’utilisation de swap est bon. Peu de « free » n’est pas automatiquement mauvais ; « available » importe plus.

Décision : Si le swap est utilisé, le CPU peut monter à cause du paging et les requêtes ralentissent de façon imprévisible. Réglez la pression mémoire avant de réécrire le SQL.

Tâche 16 : Capturer un instantané de profil CPU (relativement sûr) avec perf

cr0x@server:~$ sudo perf top -p 5123
Samples: 42K of event 'cycles', 4000 Hz, Event count (approx.): 12000000000
Overhead  Shared Object        Symbol
  18.21%  mysqld               Item_func_like::val_int
  11.03%  mysqld               JOIN::exec
   9.88%  libc.so.6            __memcmp_avx2_movbe

Sens : Vous voyez où le CPU est dépensé. Ici c’est de la comparaison de chaînes et l’exécution de jointures, pas « disque ». Les profils peuvent rapidement valider des motifs de requêtes (LIKE ‘%…%’) et des jointures mauvaises.

Décision : Si les hotspots CPU correspondent à des opérateurs connus coûteux (sort, hash, LIKE, extraction JSON), priorisez les réécritures de requêtes et les index. Si vous voyez des hotspots noyau (pile réseau), regardez les tempêtes de clients ou l’overhead TLS.

Faits intéressants et contexte historique (pourquoi le comportement d’aujourd’hui existe)

  • PostgreSQL a commencé comme POSTGRES à l’UC Berkeley dans les années 1980, avec des idées précoces autour de l’extensibilité et des types riches — raison pour laquelle il privilégie encore les index riches et les opérateurs personnalisés.
  • MySQL a été conçu pour la simplicité et la vitesse pour les charges web courantes au milieu des années 1990 — raison pour laquelle il mettait l’accent sur des valeurs par défaut pragmatiques et la facilité de déploiement.
  • InnoDB est devenu le moteur par défaut de MySQL parce que l’intégrité transactionnelle et la récupération après crash l’ont emporté en pratique, même si cela signifiait plus de mécanique interne (et plus de façons de brûler du CPU).
  • Le MVCC de Postgres rend les lectures non bloquantes dans de nombreux cas, mais il déplace le coût vers le vacuum et les vérifications de visibilité des tuples — le CPU peut monter quand l’autovacuum est en retard ou que la bloat augmente.
  • Le performance_schema de MySQL est passé d’« agréable à avoir » à essentiel une fois que les systèmes ont grandi et que « SHOW STATUS » n’était plus suffisant pour l’attribution.
  • Les planificateurs de requêtes sont devenus plus intelligents et complexes dans les deux systèmes ; des planificateurs plus intelligents exigent des statistiques, et de mauvaises stats sont une dette de performance qui se cumule silencieusement.
  • La réplication a changé la forme des incidents : les replicas de lecture ont réduit le CPU de lecture sur les primaires mais ont introduit de nouveaux hotspots CPU sur les replicas (replay/apply, vacuum, ou maintenance d’index secondaires).
  • Le matériel est devenu plus rapide, et les requêtes sont devenues plus paresseuses : les équipes ont cessé de sentir le coût des scans jusqu’à ce que la croissance des données rende les plans « temporaires » permanents.

Trois mini-histoires d’entreprise issues du terrain

Incident n°1 : la panne causée par une mauvaise hypothèse (« CPU élevé signifie machines plus grandes »)

L’entreprise avait un cluster MySQL stable et un rituel trimestriel : si la latence montait, augmenter la taille des instances. Ça marchait jusqu’à ce que ça ne marche plus. Un vendredi, le CPU était bloqué et les taux d’erreur ont augmenté. La première réponse fut le réflexe corporatif type : scaler vers le haut.

Le scale up a aidé pendant une vingtaine de minutes. Puis le CPU a remonté, mais plus vite. Les ingénieurs ont commencé à blâmer le fournisseur cloud. Quelqu’un a suggéré de désactiver le binaire log « juste pour voir ». Les têtes plus froides ont prévalu — de justesse.

Nous avons extrait le CPU par thread et avons vu une poignée de threads très chauds, pas une surcharge uniforme. performance_schema montrait un digest dominé par un seul endpoint : une fonctionnalité d’« export » qui était récemment passée d’une pagination incrémentale à « donnez-moi tout du dernier mois ». Elle faisait le bon travail, de la mauvaise façon.

Le hic : l’ensemble de données avait doublé, et le plan de requête était dérivé vers « Using temporary; Using filesort ». L’instance n’était pas trop petite ; la requête était devenue un multiplicateur CPU. Nous avons limité le débit de l’endpoint, ajouté un index composite, et réécrit la requête pour pré-agréger. Le CPU a chuté, et la taille de l’instance est revenue en arrière le lendemain.

L’hypothèse erronée n’était pas technique. Elle était psychologique : traiter le matériel comme un palliatif pour l’absence de mesures.

Incident n°2 : l’optimisation qui a mal tourné (un « index utile » qui a empiré le CPU)

Une autre équipe utilisait PostgreSQL et subissait des pics de CPU périodiques aux heures de pointe. Ils ont repéré une requête lente, ajouté un index, et l’ont vu s’accélérer en staging. Changement approuvé, déployé, et célébré. Bref instant.

En production, le CPU a empiré et la p99 a augmenté. L’index était utilisé, oui — mais il a provoqué un plan qui effectuait beaucoup d’accès aléatoires puis triait un résultat intermédiaire bien plus grand. L’ancien plan était un scan séquentiel avec un filtre serré qui bénéficiait de la chaleur du cache.

Nous avons utilisé EXPLAIN (ANALYZE, BUFFERS) sur des données proches de la production (pas des jeux de tests). Le nouvel index a amené le planner à sous-estimer la sélectivité. Il a choisi des nested loops alors qu’un hash join était moins coûteux. Le CPU est monté sur le traitement de jointure et les comparaisons de tri, pas sur l’I/O.

La correction a impliqué d’admettre que « index == plus rapide » n’est pas une loi de la physique. Nous avons augmenté les objectifs de statistiques sur des colonnes clés, lancé analyze, et remplacé l’index par un index composite qui correspondait au filtre et au pattern de jointure. Les pics CPU n’ont pas disparu — ils sont redevenus proportionnels, ce qui est la meilleure chose qu’on puisse dire en production.

Incident n°3 : la pratique ennuyeuse qui a sauvé la mise (télémétrie de requêtes disponible à la demande)

Une plateforme de paiements utilisait à la fois MySQL et PostgreSQL pour des raisons historiques. Ils avaient une habitude qui paraissait ennuyeuse en réunion de planification : garder une télémétrie légère de requêtes activée en permanence. Pas la journalisation complète de chaque statement — juste assez pour attribuer le temps aux motifs.

Quand un pic CPU est arrivé, il n’y a pas eu de panique pour « activer le slow log et attendre ». MySQL avait déjà des digests de statements dans performance_schema. Postgres avait pg_stat_statements. Ils pouvaient répondre, en quelques minutes, quels motifs de requête avaient changé en temps total et en taux d’appels.

Le coupable n’était ni exotique ni glamour : un ordonnanceur de jobs s’était déclenché de travers et avait lancé trop de workers parallèles. Chaque worker exécutait une requête « raisonnable ». Ensemble, ils ont créé une tempête CPU et ont mis à mal les caches. L’équipe n’a pas argumenté sur le matériel. Elle a prouvé un changement de concurrence.

Ils ont corrigé l’ordonnanceur, plafonné la concurrence au niveau du pool, et ajouté une alerte garde-fou : « même digest, pic soudain d’appels par seconde ». L’incident était anticlimatique. C’est le but. Les pratiques ennuyeuses ne font pas de bonnes histoires de guerre, mais elles vous gardent en poste.

Erreurs courantes : symptôme → cause racine → correctif

1) Symptôme : CPU 100%, mais la latence n’augmente que parfois

Cause racine : requêtes par rafales ou jobs batch en concurrence avec le trafic OLTP ; la saturation CPU est partagée par tranche de temps.

Correctif : Séparez les charges (mettre les batchs en file), appliquez des limites de concurrence, et identifiez les digests principaux par temps total et taux d’appels.

2) Symptôme : CPU élevé et load average élevé, mais mpstat montre iowait

Cause racine : système lié à l’I/O avec de nombreux threads bloqués en sommeil non interrompable. La charge n’est pas le CPU.

Correctif : Mesurez la latence disque, les taux de hit du buffer cache, la pression de checkpoint, et les plans de requête qui déclenchent de grosses lectures.

3) Symptôme : CPU élevé en temps noyau (%sys), pas en temps utilisateur

Cause racine : overhead réseau (tempêtes de connexions), coût TLS, contention système de fichiers, ou changement de contexte excessif.

Correctif : Utilisez du pooling de connexions, réduisez les connexions par requête, validez les paramètres TLS, et cherchez des motifs lourds en appels système.

4) Symptôme : CPU élevé sur Postgres, beaucoup de sessions « actives », mais wait_event indique Lock

Cause racine : contention de verrous plus tempêtes de retry ou accumulation de backends bloqués.

Correctif : Identifiez les bloqueurs, raccourcissez les transactions, évitez les DDL longues en heures de pointe, et envisagez timeouts de verrou et des migrations plus sûres.

5) Symptôme : CPU élevé sur MySQL, history list length InnoDB qui grandit

Cause racine : transactions longues empêchent la purge ; le nettoyage interne prend du retard et gaspille du CPU.

Correctif : Trouvez et corrigez les transactions longues, assurez-vous des commits, et ajustez la charge. Parfois la solution est « arrêter de faire de l’analytics sur le primaire ».

6) Symptôme : Une requête qui allait bien est maintenant coûteuse

Cause racine : croissance des données + dérive des stats + régression de plan.

Correctif : Rafraîchissez les statistiques, validez les changements de plan, ajoutez des index composites alignés sur predicates/clefs de jointure, et envisagez des réécritures de requêtes.

7) Symptôme : CPU élevé après ajout d’un index

Cause racine : le planner choisit un plan pire, ou l’index augmente l’amplification d’écriture et l’overhead de maintenance.

Correctif : Comparez les plans avant/après sur des données proches de la production ; ajustez les stats ; supprimez/remplacez l’index par la bonne forme ; mesurez l’overhead d’écriture.

8) Symptôme : CPU élevé surtout sur les replicas

Cause racine : application de réplication, vacuum, maintenance d’index, ou trafic de lecture déplacé sans planification de capacité.

Correctif : Mesurez le lag d’application et l’activité des workers en arrière-plan ; ajustez la taille des replicas ; ne supposez pas que les replicas sont « gratuits ».

Blague #2 : Un graphe CPU à 100% est comme une réunion d’entreprise générale : tout le monde est occupé, et d’une façon ou d’une autre, rien n’avance plus vite.

Listes de contrôle / plan étape par étape

Étape par étape : prouver que ce sont les requêtes (pas le matériel) pendant un incident

  1. Capturer un instantané OS : mpstat, top, pidstat, vmstat.
  2. Confirmer que la BD est le processus chaud : identifier PID/TGID et threads principaux.
  3. Vérifier le bruit de virtualisation : steal time ; confirmer l’absence de throttling CPU/crédit burst épuisé.
  4. Chemin Postgres : mapper PID chaud → pg_stat_activity requête ; vérifier les wait events ; utiliser pg_stat_statements pour les principaux coupables.
  5. Chemin MySQL : utiliser les résumés de digests performance_schema ; corréler avec processlist ; confirmer les rows examined.
  6. Décider contention vs exécution : verrous/waits vs calcul pur.
  7. Lancer une preuve de plan : EXPLAIN (ANALYZE, BUFFERS) ou EXPLAIN ANALYZE ; capturer les comptes de lignes et les boucles.
  8. Appliquer la mitigation la moins risquée : limiter le débit, tuer les pires requêtes, réduire la concurrence, ou désactiver temporairement l’endpoint.
  9. Implémenter le correctif durable : index/réécriture de requête/stats, puis valider avec des métriques avant/après.
  10. Rédiger le postmortem avec la chaîne de preuves : « OS → BD → digest de requête → plan → changement. »

Garde-fous opérationnels que je recommande (et pourquoi)

  • Pooling de connexions : car les tempêtes de threads/processus sont une taxe CPU et une taxe d’ordonnancement.
  • Télémétrie de requêtes toujours activée (légère) : pour que votre prochain incident ne soit pas « attendre 30 minutes les logs ».
  • Plafonds de concurrence par workload : car le débit augmente jusqu’à un point, puis s’effondre.
  • Discipline de changement pour indexes et stats : car les changements de performance sont des changements production, pas « juste du schéma ».
  • Données de staging proches de la production pour tests de plan : car le planner se moque de votre jeu de données synthétique.

Comment choisir entre « corriger les requêtes » et « scaler le matériel »

Scalez le matériel lorsque vous avez des requêtes stables et efficaces et une augmentation légitime de la charge. Corrigez les requêtes lorsque le CPU par requête augmente, que des plans régressent, que les rows examined explosent, ou que la contention monte. Dès que vous voyez « rows examined par appel » gonfler, augmenter les ressources est un anesthésiant temporaire. Il se dissipe.

FAQ

1) Comment savoir que ce n’est pas un mauvais CPU ou un matériel défaillant ?

Les CPU modernes échouent rarement « à moitié » d’une manière qui affecte uniquement MySQL. Si mpstat montre un steal time élevé, c’est un problème de colocation. Si les logs noyau montrent des machine check errors, c’est du matériel. Sinon, supposez d’abord que c’est la charge et prouvez le contraire.

2) Pourquoi le CPU est élevé mais les requêtes individuelles ne semblent pas lentes ?

Parce que le débit peut vous tuer. Une requête rapide appelée 50 000 fois par minute peut dominer le CPU plus qu’une seule requête lente. Regardez le temps total et les appels, pas seulement la latence moyenne.

3) Pourquoi Postgres affiche-t-il beaucoup de sessions « actives » sans wait event ?

Souvent, elles tournent réellement sur le CPU. Parfois elles sont dans un état non représenté comme wait event. Utilisez les outils OS (pidstat/perf) pour confirmer que le backend consomme des cycles.

4) La contention de verrous peut-elle provoquer 100% CPU ?

Oui. Les spinlocks, les retries et le thrash de l’ordonnanceur peuvent pousser le CPU même lorsque le travail utile diminue. Dans MySQL, regardez les métriques de sémaphores InnoDB ; dans Postgres, regardez les wait events et les chaînes de blocage.

5) Dois-je activer le slow query log MySQL pendant un incident ?

Seulement si vous pouvez en supporter l’overhead et que vous réglez des seuils raisonnables. Préférez les digests performance_schema pour l’attribution. Les slow logs sont utiles, mais les activer en plein incendie produit souvent plus de fumée que de signal.

6) PostgreSQL est-il intrinsèquement plus gourmand en CPU que MySQL ?

Il n’y a pas de réponse générale. Postgres peut dépenser du CPU sur les vérifications MVCC et le comportement du vacuum ; MySQL peut dépenser du CPU sur la contention InnoDB et les patterns d’exécution de jointures. La charge et le schéma décident.

7) Quand dois-je utiliser perf ?

Quand vous devez prouver où vont les cycles (tri, hachage, fonctions de chaîne, traitement JSON) et que les métriques BD ne racontent pas une histoire cohérente. Utilisez-le pour valider des hypothèses, pas comme réflexe initial.

8) Quelle est la mitigation la plus rapide et sûre quand le CPU est saturé ?

Réduisez la concurrence et arrêtez l’hémorragie : limitez un endpoint chaud, suspendez un job batch, plafonnez la taille du pool de connexions, ou tuez le pire coupable. Ensuite appliquez le correctif durable après que le graphe se soit calmé.

9) Pourquoi ajouter un index n’a-t-il pas réduit le CPU ?

Parce que les index ne réduisent pas le travail si la requête touche encore d’énormes quantités de lignes, ou si l’index provoque une stratégie d’exécution pire pour la distribution de vos données. Validez avec EXPLAIN ANALYZE et des comptes de lignes réels.

10) Comment prouver une régression de plan ?

Capturez l’ancien plan (des logs, d’un explain stocké, ou d’un environnement de référence) et comparez au nouveau plan avec les lignes et boucles réelles. Dans Postgres, pg_stat_statements plus auto_explain aident. Dans MySQL, comparez EXPLAIN/EXPLAIN ANALYZE et les timings de digest avant/après la fenêtre de changement.

Conclusion : prochaines étapes pratiques

Si vous voulez prouver que « CPU 100% » vient des requêtes et non du matériel, faites-le comme un opérateur, pas comme un philosophe. Mesurez la répartition CPU. Identifiez les processus/threads chauds. Mappez-les aux sessions et aux digests de requêtes. Obtenez une preuve au niveau plan avec des comptes de lignes réels. Puis choisissez la plus petite mitigation qui vous donne du temps.

Prochaines étapes qui rapportent immédiatement :

  1. Assurez-vous de pouvoir interroger pg_stat_statements (Postgres) ou les performance_schema statement digests (MySQL) pendant un incident en direct.
  2. Rédigez un runbook d’une page avec le Plan de diagnostic rapide ci-dessus et gardez-le près des notes de rotation on-call.
  3. Ajoutez une alerte pour le taux d’appels par digest qui augmente soudainement ; elle détecte les mauvais déploiements et les tempêtes d’ordonnancement avant que le CPU n’atteigne le plafond.
  4. Faites de « EXPLAIN avec la forme réelle des données » une exigence pour les correctifs de performance. Les indexes sans plans ne sont que des opinions coûteuses.
← Précédent
Une mise à jour du pilote a tué mon FPS : comment diagnostiquer correctement
Suivant →
Proxmox Ceph Slow Ops : localiser le goulot (disque, réseau ou CPU)

Laisser un commentaire