Il existe un type particulier de panne où la base de données n’est pas « down ». Elle est en ligne, accepte des connexions, renvoie certaines requêtes et affiche fièrement des probes de santé au vert. Pendant ce temps, votre API subit des timeouts, les réplicas dérivent dans le futur, et chaque ingénieur regarde un tableau de bord qui indique que tout est « vert ».
C’est là que le MySQL que vous pensez exploiter et le MySQL que vous exploitez réellement — RDS MySQL — ne sont plus synonymes. Les différences sont subtiles jusqu’à devenir catastrophiques. La plupart des incidents ne proviennent pas d’une grosse erreur ; ils viennent d’une mauvaise hypothèse rencontrant une limite cachée à 2h13 du matin.
Le décalage fondamental : « MySQL » n’est pas un modèle de déploiement
MySQL autogéré est un empilement de choix. Système de fichiers, périphérique bloc, niveau RAID (ou pas), affinité CPU, comportement du cache de pages de l’OS, bizarreries du planificateur du noyau, réglages TCP, outils de sauvegarde, et le type de cron étrange qu’un ancien collègue a écrit en 2019 et que personne ne veut toucher. C’est flexible. C’est aussi votre responsabilité, ce qui signifie que c’est votre faute en cas d’incident.
RDS MySQL est un produit. Il se comporte comme MySQL au niveau SQL, mais il vit dans des garde-fous : stockage géré, sauvegardes gérées, patching géré, basculement géré, observabilité gérée. Cette gestion s’accompagne de contraintes que vous ne contrôlez pas et que parfois vous ne pouvez même pas voir. En période calme, c’est une fonctionnalité. En cas d’incident, c’est une négociation.
Le mode d’échec typique n’est pas « RDS est pire ». C’est « vous avez planifié comme si c’était autogéré », ou l’inverse. En production, le système que vous supposez est le système que vous déboguez. Si vos hypothèses sont fausses, vous déboguez avec confiance et ne réparez rien.
Une citation que vous devriez garder collée à votre écran vient de Werner Vogels : You build it, you run it.
Elle est courte, et elle fait mal parce qu’elle est vraie.
Faits et historique qui expliquent les arêtes vives d’aujourd’hui
- Le moteur de stockage par défaut de MySQL a changé la donne. InnoDB est devenu le moteur par défaut dans MySQL 5.5, et soudain « ACID » n’était plus une fonctionnalité premium. Cela a aussi fait de la taille et du dimensionnement des redo logs une préoccupation opérationnelle de première classe.
- Amazon a introduit RDS en 2009. La promesse était simple : arrêter de faire du babysitting de serveurs. Le compromis était plus simple : vous n’avez pas le root, et vous acceptez une infrastructure opinionnée.
- Performance Schema n’a pas toujours été la pratique standard. Il a mûri sur des années ; beaucoup « d’experts MySQL » ont appris à une époque où l’on suivait les slow logs et où l’on devinait. RDS rend les outils profonds au niveau OS plus difficiles, donc vous devriez maîtriser l’instrumentation moderne de MySQL.
- Le « doublewrite buffer » d’InnoDB existe parce que le stockage ment. Les pages déchirées arrivent. Le stockage géré réduit certains risques mais n’élimine pas le besoin de choix de conception cohérents en cas de crash.
- La réplication a plusieurs visages. La réplication asynchrone classique est différente de la semi-synchrone, et les deux diffèrent de la group replication. RDS supporte certains modes, en restreint d’autres, et ajoute ses propres comportements opérationnels autour du basculement.
- « General purpose SSD » n’a pas toujours suffi. gp2/gp3 et io1/io2 existent parce que l’IOPS est désormais une décision produit, pas juste un détail d’ingénierie. Sur site, vous combattez la physique ; dans le cloud, vous combattez votre dernier bon de commande.
- DDL en ligne a changé la réponse aux incidents. Les DDL de MySQL se sont améliorés, mais tous les ALTER ne se valent pas, et RDS ajoute des contraintes autour des opérations longues et des fenêtres de maintenance.
- Les sauvegardes sont passées des « fichiers » aux « snapshots ». Les dumps logiques sont portables mais lents ; les snapshots sont rapides mais couplés au modèle de stockage de la plateforme. RDS encourage les snapshots ; votre stratégie de récupération doit tenir compte de ce couplage.
Limites cachées qui frappent pendant les incidents (et pourquoi)
1) Le stockage n’est pas juste une « taille » : c’est latence, burst, et amplification d’écriture
Sur MySQL autogéré, vous pouvez attacher des disques plus rapides, régler le contrôleur RAID, ou balancer du NVMe sur le problème. Sur RDS, vous choisissez une classe de stockage basée sur EBS et vivez avec ses contraintes d’IOPS et de débit. Si vous avez choisi gp2 il y a des années et ne l’avez jamais revu, vous vivez peut‑être avec des « crédits burst » sans le savoir.
Pattern d’incident : la latence monte, le CPU semble correct, les requêtes ralentissent, et tout le monde blâme « un mauvais déploiement ». Pendant ce temps, le véritable coupable est la profondeur de la file d’attente disque. InnoDB aime l’I/O quand son working set dépasse le buffer pool ou quand il vidange des pages sales sous pression.
Ce qui est caché : le sous-système de stockage est géré. Vous ne pouvez pas lancer iostat sur l’hôte. Vous devez vous fier aux métriques RDS et aux variables de statut MySQL. Ce n’est pas pire, c’est différent : il faut avoir des réflexes différents.
2) « Stockage gratuit » est un mensonge lors des débordements de tables temporaires et des DDL en ligne
RDS peut redimensionner automatiquement le stockage dans certaines configurations, mais il n’auto‑scale pas forcément l’espace dont vous avez besoin tout de suite pour un gros débordement de table temporaire, un gros ALTER qui construit une copie, ou une longue transaction qui gonfle l’undo. Vous pouvez vous retrouver avec beaucoup de stockage alloué et pourtant toucher un mur « espace temporaire » qui semble aléatoire si vous avez seulement travaillé sur du bare metal.
Les environnements autogérés mettent souvent tmpdir sur un volume séparé et le dimensionnent intentionnellement. Sur RDS, l’usage de tmp interagit avec l’espace éphémère local de l’instance et sa configuration. Vous devez savoir ce que fait votre moteur sous pression d’espace et combien de marge vous avez avant que le système commence à échouer de façons créatives.
3) max_connections n’est pas un nombre ; c’est un périmètre d’impact
RDS fixe des valeurs par défaut et associe parfois les valeurs autorisées à la classe d’instance via des parameter groups. Sur une machine que vous possédez, vous pouvez pousser max_connections jusqu’à manquer de RAM, puis vous demander pourquoi l’OOM killer du noyau écrit votre post-mortem. Sur RDS, vous pouvez toujours tuer l’instance avec des connexions — juste de manière plus polie.
La limite cachée n’est généralement pas le max_connections configuré mais ce qui se passe avant que vous l’atteigniez : mémoire par connexion (sort buffers, join buffers), surcharge d’ordonnancement des threads, et contention sur les mutex dans MySQL. RDS ajoute une autre couche : les tempêtes de connexions lors du basculement, les retries applicatifs, et une mauvaise configuration du pooling peuvent s’additionner et transformer un petit événement de latence en un embouteillage complet.
4) Le basculement est une fonctionnalité produit, pas une solution magique
Le basculement autogéré peut être instantané ou catastrophique, selon l’investissement dans l’automatisation. Le basculement RDS est généralement bon, mais il n’est pas magique. Il y a un temps de détection, un temps de promotion, un temps de propagation DNS, et le comportement de reconnexion de l’application. Durant cette fenêtre, votre appli peut marteler le endpoint avec des retries comme un enfant qui appuie sur le bouton de l’ascenseur.
Limite cachée : l’abstraction du « writer endpoint » peut masquer les changements de topologie, mais elle n’enlève pas le besoin pour votre application de gérer les erreurs transitoires, les connexions obsolètes et l’idempotence. Si votre appli ne tolère pas une coupure de 30–120 secondes, vous n’avez pas de haute disponibilité. Vous avez une architecture basée sur l’espoir.
5) Le lag de réplication est souvent une histoire d’I/O qui porte un déguisement SQL
Les gens traitent le lag de réplication comme un problème de configuration de réplication. Parfois c’est le cas. Souvent, le replica applique plus lentement parce qu’il est saturé en stockage, CPU, ou à cause de contraintes d’application mono‑thread. RDS vous donne des métriques et quelques boutons ; il ne vous donne pas la permission de ssh et de « juste vérifier quelque chose ».
Limite cachée : la classe d’instance du replica peut être plus petite, la classe de stockage peut différer, ou le format du binlog et la charge peuvent rendre la réplication parallèle inefficace. En incident, la mauvaise solution est généralement « redémarrer la réplication ». La bonne solution est souvent « réduire la pression d’écriture » ou « corriger le chemin d’application lent ».
6) Les parameter groups rendent la configuration plus sûre — et plus lente à changer
Sur une machine autogérée, vous éditez my.cnf, vous faites un reload, et vous passez à autre chose. Sur RDS, des changements de paramètres peuvent nécessiter un reboot, être seulement dynamiques, ou être carrément interdits. Il faut aussi suivre quel parameter group est attaché à quelle instance, ce qui semble ennuyeux jusqu’à ce que vous découvriez que prod et staging sont « presque » identiques.
Limite cachée : la latence opérationnelle. Durant un incident, « on peut tuner X » n’est pas un plan à moins de savoir si X est modifiable sans downtime et si vous pouvez le faire rapidement et en sécurité.
7) Observabilité : vous n’avez pas l’hôte, donc vous devez maîtriser le moteur
Sur MySQL autogéré, vous pouvez utiliser des outils au niveau OS : perf, strace, tcpdump, stats cgroup, histogrammes de latence du système de fichiers, et le confort de savoir que vous pouvez toujours creuser plus loin. Sur RDS, vous utilisez les métriques CloudWatch, Enhanced Monitoring (si activé), Performance Insights (si activé), et les propres tables et variables de statut de MySQL.
Si vous n’activez pas ces fonctionnalités à l’avance, votre futur vous en incident regardera une photo floue de la scène du crime. Blague #1 : Une observabilité non activée est comme un extincteur toujours dans le panier Amazon — très abordable, très inutile.
8) Sauvegardes et restaurations : les snapshots sont rapides ; la remise en service reste un processus
Les snapshots RDS sont pratiques, et la récupération PITR est puissante. Mais le piège d’incident est de supposer « on peut juste restaurer rapidement ». Les restaurations prennent du temps, et la nouvelle instance doit se réchauffer, vérifier les paramètres, les groupes de sécurité, et effectuer le cutover applicatif. Si vous êtes habitué à restaurer un backup local sur une VM de secours, vous serez peut‑être choqué par l’overhead d’orchestration.
Limite cachée : temps jusqu’à l’état opérationnel, pas temps de création de l’instance.
9) Vous ne pouvez pas tout réparer avec « une instance plus grande »
Monter en puissance aide les charges CPU-bound et vous donne plus de RAM pour le buffer pool. Cela ne corrige pas automatiquement les limites d’I/O si le débit de stockage est le goulot d’étranglement. Cela ne résout pas la contention sur des lignes chaudes ou les verrous metadata. Cela ne corrige pas les requêtes pathologiques. Et en incident, le scale peut être lent ou disruptif.
Les environnements autogérés offrent souvent plus d’options « dangereuses » (vider les caches, redémarrer les services, détacher des volumes, lancer des scripts d’urgence). RDS réduit le nombre d’outils tranchants avec lesquels vous pouvez vous blesser. Il réduit aussi le nombre d’outils tranchants avec lesquels vous pouvez vous sauver. Planifiez en conséquence.
Playbook de diagnostic rapide : quoi vérifier en premier/deuxième/troisième
Premier : décidez si vous êtes CPU-bound, I/O-bound, ou lock-bound
- Signaux CPU-bound : CPU élevé, « active sessions » élevées dans Performance Insights, requêtes lentes même quand le working set tient en mémoire, beaucoup de fonctions/sorts, mauvais index.
- Signaux I/O-bound : CPU faible/modéré, latence des requêtes en hausse, lectures/écritures InnoDB accrues, file d’attente disque élevée (CloudWatch), misses du buffer pool, pics de flushing de pages sales.
- Signaux lock-bound : le CPU peut être bas, mais les threads attendent des verrous ; beaucoup de connexions bloquées ; quelques transactions bloquent tout ; le lag de réplication augmente à cause d’un apply bloqué.
Deuxième : confirmez le goulot avec deux vues indépendantes
Ne faites pas confiance à une métrique unique. Associez des données internes au moteur (status, processlist, performance_schema) avec des métriques plateforme (CloudWatch/Enhanced Monitoring) ou des preuves au niveau requête (top digests, slow log).
Troisième : choisissez l’intervention la plus sûre
- Si lock-bound : identifiez le bloqueur, tuez la bonne session, réduisez la portée des transactions, corrigez le comportement applicatif. Évitez « redémarrer MySQL » à moins de choisir le downtime volontairement.
- Si I/O-bound : arrêtez ce qui écrit/lit trop (job batch, gros rapport), réduisez la concurrence, scalez temporairement les IOPS/classe de stockage, puis corrigez le schéma/les requêtes.
- Si CPU-bound : trouvez le SQL top par temps, ajoutez ou corrigez les index, réduisez les requêtes coûteuses, envisagez de scaler l’instance, puis corrigez le plan de requête de façon durable.
Quatrième : prévenir la reprise en arrière
Les incidents aiment les rebonds : vous tuez la requête bloquante, la latence chute, les autoscalers ou les tempêtes de retries réintroduisent la charge, et vous êtes de retour au point de départ. Limitez les retries, mettez en pause les workers batch, et contrôlez le comportement des pools de connexions.
Tâches pratiques : commandes, sorties et décisions (12+)
Conçues pour la réponse aux incidents. Chaque tâche inclut une commande, ce que signifie la sortie, et la décision à prendre. Exécutez-les contre MySQL autogéré ou RDS MySQL (depuis une bastion ou un hôte applicatif). Remplacez les noms d’hôte/utilisateur si besoin.
Task 1: Confirm basic connectivity and server identity
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT @@hostname, @@version, @@version_comment, @@read_only\G"
Enter password:
*************************** 1. row ***************************
@@hostname: ip-10-11-12-13
@@version: 8.0.36
@@version_comment: MySQL Community Server - GPL
@@read_only: 0
Signification : Vous êtes sur le writer (read_only=0) et vous connaissez la version majeure. La version compte parce que le comportement et l’instrumentation diffèrent.
Décision : Si vous attendiez un réplica et êtes sur le writer (ou l’inverse), arrêtez‑vous et corrigez la cible avant d’appliquer des changements « utiles ».
Task 2: See what threads are doing right now
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW FULL PROCESSLIST;"
...output...
10231 appuser 10.0.8.21:51244 appdb Query 38 Sending data SELECT ...
10244 appuser 10.0.7.19:49812 appdb Query 38 Waiting for table metadata lock ALTER TABLE orders ...
10261 appuser 10.0.8.23:50111 appdb Sleep 120 NULL
...
Signification : Vous pouvez repérer des blocages évidents : « Waiting for table metadata lock » est une grande flèche rouge pointant vers un DDL ou des transactions longues.
Décision : Si vous voyez des attentes de verrous dominer, orientez‑vous vers le diagnostic des verrous au lieu de chasser CPU ou I/O.
Task 3: Identify the blocking transaction (InnoDB lock waits)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT * FROM sys.innodb_lock_waits\G"
*************************** 1. row ***************************
wait_started: 2025-12-30 01:12:18
wait_age: 00:01:42
waiting_trx_id: 321889203
waiting_pid: 10244
waiting_query: ALTER TABLE orders ADD COLUMN ...
blocking_trx_id: 321889199
blocking_pid: 10198
blocking_query: UPDATE orders SET ...
blocking_lock_mode: X
Signification : Cela montre qui est bloqué et qui bloque, avec des PIDs sur lesquels vous pouvez agir.
Décision : Si le bloqueur est une transaction applicative coincée en boucle, tuez le bloqueur (pas la victime) et atténuez au niveau applicatif.
Task 4: Kill the right session (surgical, not emotional)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "KILL 10198;"
Query OK, 0 rows affected (0.01 sec)
Signification : Le thread bloquant est terminé. Le verrou devrait se libérer ; les requêtes bloquées devraient poursuivre ou échouer rapidement.
Décision : Surveillez immédiatement les tempêtes de reconnexion et les pics de retries. Tuer une requête peut en inviter cent autres.
Task 5: Check InnoDB engine status for deadlocks, flushing, and history length
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW ENGINE INNODB STATUS\G"
...output...
History list length 51234
Log sequence number 89433222111
Log flushed up to 89433111822
Pending writes: LRU 0, flush list 128, single page 0
...
Signification : Une grande history list length suggère des transactions longues empêchant le purge. Une pending flush list élevée suggère une pression d’écriture.
Décision : Si la history list length explose, trouvez et terminez les transactions longues ; si le pending flush est élevé, réduisez la charge d’écriture et envisagez de tuner le stockage/IOPS.
Task 6: Confirm buffer pool health (memory vs I/O pressure)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name | Value |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests | 9812234432 |
| Innodb_buffer_pool_reads | 22334455 |
+---------------------------------------+------------+
Signification : Innodb_buffer_pool_reads sont des lectures physiques. S’ils augmentent par rapport aux requests, vous manquez de cache et touchez le stockage.
Décision : Si les lectures physiques montent, envisagez d’augmenter le buffer pool (classe d’instance plus grande) et/ou de réduire le working set de la charge (index, correction de requêtes).
Task 7: Detect temp table spills (the quiet disk killer)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Created_tmp_disk_tables | 18443321 |
| Created_tmp_tables | 22300911 |
+-------------------------+----------+
Signification : Beaucoup de tables temporaires sur disque signifie généralement des sorts/group by/joins qui débordent. Sur RDS, cela peut entrer en collision avec des contraintes d’espace temporaire et le débit I/O.
Décision : Identifiez les requêtes fautives (Performance Insights / slow log / statement digests) et corrigez‑les ; ne vous contentez pas d’augmenter tmp_table_size et d’appeler cela un remède.
Task 8: Find top queries by total time using Performance Schema digests
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,1) AS total_s, ROUND(AVG_TIMER_WAIT/1e9,1) AS avg_ms FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 5\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE customer_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 188233
total_s: 6221.4
avg_ms: 33.1
...
Signification : Vous obtenez une liste classée des formes SQL consommant du temps. C’est généralement le chemin le plus rapide vers la réalité.
Décision : Prenez les 1–2 digests en tête et analysez leurs plans. N’optimisez pas la 19ème requête parce qu’elle est « moche ». Optimisez ce qui brûle du temps.
Task 9: Explain the plan (and spot missing indexes)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "EXPLAIN FORMAT=TRADITIONAL SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC LIMIT 50;"
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| 1 | SIMPLE | orders | ALL | idx_customer | NULL | NULL | NULL | 932112 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
Signification : type=ALL et « Using filesort » indiquent un scan complet puis un tri. Sur de grandes tables, c’est un incident en gestation.
Décision : Ajoutez un index composite (par ex. (customer_id, created_at)) et vérifiez qu’il correspond aux patterns de requête. Planifiez le changement en sécurité (le comportement des DDL en ligne compte).
Task 10: Check replication lag and apply status (replica-side)
cr0x@server:~$ mysql -h prod-mysql-replica.aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW REPLICA STATUS\G"
...output...
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 187
Retrieved_Gtid_Set: ...
Executed_Gtid_Set: ...
Signification : Les threads IO et SQL tournent, mais le lag est de 187s. C’est un problème de performance, pas de lien cassé.
Décision : Vérifiez la saturation des ressources du réplica et les goulets d’application ; envisagez de scaler le réplica, d’améliorer le débit de stockage, ou de réduire temporairement la charge d’écriture.
Task 11: Validate whether you are hitting connection saturation
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Max_used_connections');"
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| Max_used_connections| 1980 |
| Threads_connected | 1750 |
| Threads_running | 220 |
+---------------------+-------+
Signification : Beaucoup de connexions existent, mais seulement 220 en exécution. Cela indique souvent des attentes de verrous, des attentes I/O, ou un mauvais comportement de pool de connexions.
Décision : Si le nombre de connectés est proche du maximum, protégez l’instance : limitez l’appli, activez le pooling, et envisagez de réduire les limites de connexion par service pour empêcher le client le plus bruyant de tout accaparer.
Task 12: Check for long transactions that bloat undo and block purge
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT trx_id, trx_started, trx_rows_locked, trx_rows_modified, trx_query FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 5\G"
*************************** 1. row ***************************
trx_id: 321889101
trx_started: 2025-12-30 00:02:11
trx_rows_locked: 0
trx_rows_modified: 812331
trx_query: UPDATE orders SET ...
Signification : Une transaction en cours depuis 00:02 modifiant 800k lignes n’est pas « un bruit de fond normal ». C’est un coût en durabilité et en latence.
Décision : Travaillez avec les responsables applicatifs pour morceler le travail, commit fréquemment, ou déplacer les grosses mises à jour hors‑pic. En incident, envisagez de la tuer si elle bloque ou déstabilise.
Task 13: Confirm binlog pressure and retention behavior
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW BINARY LOGS;"
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.012331 | 1073741824|
| mysql-bin.012332 | 1073741824|
| mysql-bin.012333 | 1073741824|
...
Signification : Beaucoup de binlogs volumineux suggèrent une forte activité d’écriture. Sur RDS, les réglages de rétention et la croissance du stockage peuvent devenir une facture surprise et un incident surprise.
Décision : Si les binlogs gonflent, vérifiez la santé des réplicas, la configuration de rétention, et si un réplica bloqué empêche la purge des logs.
Task 14: Check table/index bloat and cardinality drift (quick sanity)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT table_name, engine, table_rows, data_length, index_length FROM information_schema.tables WHERE table_schema='appdb' ORDER BY (data_length+index_length) DESC LIMIT 5;"
+------------+--------+-----------+------------+-------------+
| table_name | engine | table_rows| data_length| index_length|
+------------+--------+-----------+------------+-------------+
| orders | InnoDB | 93211234 | 90194313216| 32112201728 |
...
Signification : Les grosses tables dominent votre destin. Si les index des plus grandes tables sont énormes ou mal adaptés aux requêtes, vous paierez en I/O et en misses du cache.
Décision : Priorisez l’indexation et les politiques de cycle de vie des données (partitionnement, archivage) sur les tables principales, pas seulement celles que les gens critiquent bruyamment.
Trois mini-récits du terrain d’incident en entreprise
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Une entreprise SaaS de taille moyenne est passée d’un MySQL autogéré sur un serveur NVMe RAID optimisé à RDS MySQL. La migration a été propre. La latence applicative s’est améliorée. Tout le monde a décrété victoire et est retourné à livrer des fonctionnalités.
Trois mois plus tard, une campagne commerciale a trop bien fonctionné. Le trafic d’écriture a doublé pendant quelques heures. Rien ne « cassait » immédiatement ; à la place, le p95 a monté, puis le p99 a explosé. Le CPU du primaire stagnait autour de 35 %. Les ingénieurs le regardaient comme s’il mentait.
La mauvaise hypothèse était simple : « Si le CPU est correct, la base est correcte. » Sur leurs anciens serveurs, cette hypothèse tenait souvent parce que le stockage avait assez de marge et était directement monitoré. Sur RDS, l’équipe n’avait pas activé Performance Insights et surveillait à peine les métriques de stockage. Le volume gp2 brûlait ses crédits burst, la latence d’I/O montait, et InnoDB commençait à vider agressivement les pages sales. Le système était I/O-bound alors que le CPU semblait calme.
Ils ont tenté de scaler la classe d’instance. Cela a aidé un peu mais n’a pas réglé le problème racine : la configuration de stockage et l’amplification d’écriture de leur charge. La solution a été de passer à une option de stockage avec des IOPS prévisibles, d’optimiser les patterns d’écriture (transactions plus petites, moins d’index secondaires sur les tables chaudes), et d’ajouter des dashboards rendant la « dette I/O » visible avant qu’elle ne devienne un incident.
Par la suite, ils ont adopté une règle : chaque migration inclut une « répétition du nouveau goulot ». Si vous ne pouvez pas expliquer comment ça casse, vous n’avez pas fini de migrer.
Mini-récit 2 : L’optimisation qui a échoué
Une entreprise liée à la finance avait un job nocturne qui recalculait des agrégats. Il était lent, donc un ingénieur l’a « optimisé » en augmentant la concurrence : plus de workers, plus grosses batches, et un pool de connexions plus grand. En staging, cela semblait parfait. En production, c’était un générateur de brownout.
Le job faisait surtout des updates sur une table chaude avec plusieurs index secondaires. Plus de workers signifiait plus de maintenance d’index simultanée et plus de conflits de verrouillage au niveau ligne. Le redo log et le comportement de flushing sont devenus le goulot, et les tables temporaires ont débordé sur disque car le job faisait aussi des group-bys intermédiaires.
Sur MySQL autogéré, ils avaient l’habitude de regarder iostat et de tuner l’hôte. Sur RDS, ils regardaient le CPU et supposaient que c’était le limiteur. Ce ne l’était pas. L’« optimisation » a augmenté l’amplification d’écriture et la contention de verrous, poussant le sous-système de stockage dans une latence soutenue élevée. Le lag de réplication a monté, et le trafic en lecture a commencé à toucher le writer parce que les replicas étaient trop en retard pour être fiables.
La solution ennuyeuse a été de réduire la concurrence, morceler les mises à jour par plages de clés primaires, et ajouter un index qui a transformé un join coûteux en une recherche bon marché. Le job a fini un peu plus lent que la version « optimisée » en isolation, mais il a cessé de dévaster le reste de la plate-forme.
Blague #2 : En bases de données, « plus de parallélisme » signifie parfois « plus de personnes qui essaient de passer par la même porte en même temps ».
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une plateforme e‑commerce faisait tourner RDS MySQL en Multi‑AZ avec un réplica de lecture pour l’analytics. Leurs ingénieurs n’étaient pas réputés pour une architecture palpitante. Ce dont ils étaient réputés, c’était les runbooks, les game days routiniers, et une obsession presque agaçante pour les parameter groups.
Un après‑midi, une migration de schéma a introduit une régression du plan de requête. La latence du writer a grimpé. Puis le lag du réplica a augmenté. Puis l’appli a commencé à timeout et à retry. Spirale classique.
L’astreint n’a pas commencé par « tuner MySQL ». Il a exécuté un playbook pratiqué : confirmer CPU/I/O/verrous, identifier les top digests, vérifier si les retries amplifient, puis prendre l’action de délestage de charge la moins risquée. Ils ont temporairement désactivé le consommateur analytics, réduit la concurrence des workers, et activé un feature flag côté requête pour stopper le pire coupable. Le lag de réplication s’est stabilisé.
Voici où la pratique ennuyeuse a payé : ils avaient testé la restauration à partir d’un snapshot et la promotion des replicas, et avaient une procédure documentée pour détacher le trafic de lecture du writer. Ils n’ont pas eu besoin de basculer, mais ils auraient pu le faire calmement. L’incident a duré moins d’une heure, et le postmortem portait surtout sur la discipline de revue des requêtes, pas sur des actes héroïques.
Erreurs courantes : symptôme → cause racine → correctif
1) Symptom: CPU is low, latency is high
Cause racine : Saturation I/O (débit stockage/IOPS) ou attentes de verrous. Courant sur RDS quand les volumes gp perdent leur burst ou quand les tables temporaires débordent.
Correctif : Vérifiez les misses du buffer pool et les tables temporaires sur disque ; contrôlez la latence/queue de stockage dans CloudWatch ; réduisez la pression lecture/écriture ; passez à des IOPS prévisibles ; corrigez requêtes et index.
2) Symptom: “Too many connections” appears after failover
Cause racine : Tempête de connexions due aux retries applicatifs et au manque de pooling ; anciennes connexions non recyclées ; max_connections configuré sans comprendre la mémoire par thread.
Correctif : Imposer du pooling (ProxySQL/RDS Proxy/pools applicatifs), plafonner les connexions par service, ajouter des retries avec jitter, et définir des timeouts raisonnables. Préférez moins de connexions saines plutôt que des milliers d’inactives.
3) Symptom: Replicas lag steadily but never catch up
Cause racine : Le débit d’application du réplica est inférieur à la charge d’écriture, souvent à cause du stockage ou des contraintes d’application mono‑thread, ou d’une classe d’instance plus faible.
Correctif : Scalez les ressources du réplica et le débit de stockage, réduisez le volume d’écriture (throttling batch), et vérifiez les transactions longues ou les attentes de verrous sur le réplica.
4) Symptom: ALTER TABLE “hangs” and everything else slows
Cause racine : Verrous metadata et transactions longues ; ou DDL en ligne qui crée une forte pression sur tmp/redo ; parfois un DDL bloqué en attente d’une transaction qui doit finir.
Correctif : Identifiez les bloqueurs avec sys.innodb_lock_waits et processlist ; planifiez les DDL hors‑pic ; utilisez des méthodes sûres de changement de schéma en ligne ; raccourcissez les transactions.
5) Symptom: Disk space alarms but data size didn’t grow much
Cause racine : Binlogs, croissance de l’undo due à des transactions longues, débordements de tables temporaires, ou effets de rétention/snapshots.
Correctif : Inspectez le volume des binlogs et la rétention ; tuez ou refactorez les transactions longues ; corrigez les requêtes qui débordent ; confirmez les politiques de rétention des sauvegardes.
6) Symptom: After scaling instance, performance barely improves
Cause racine : Le goulot est le débit de stockage/IOPS ou la contention de verrous, pas le CPU/RAM.
Correctif : Passez à un stockage à plus haut débit, réduisez l’amplification d’écriture, et corrigez les points chauds de contention (index, patterns transactionnels applicatifs).
7) Symptom: “Free memory” looks high but system is slow
Cause racine : La mémoire MySQL n’est pas toute l’histoire ; InnoDB peut être sous‑dimensionné ou la charge peut être I/O-bound. Les métriques mémoire RDS peuvent induire en erreur si vous n’inspectez pas le buffer pool et le working set.
Correctif : Vérifiez les indicateurs de hit ratio du buffer pool, examinez les requêtes et index les plus coûteux, et ajustez la classe d’instance et innodb_buffer_pool_size de façon appropriée (là où c’est permis).
8) Symptom: A read replica is “healthy” but returns stale data
Cause racine : Lag de réplication ou apply retardé ; l’application suppose une consistance read‑after‑write depuis les replicas.
Correctif : Orientez les lectures post‑écriture vers le writer (session stickiness), imposez la consistance sur les chemins critiques, surveillez le lag, et définissez des seuils pour l’utilisation des replicas.
Listes de contrôle / plan étape par étape
Avant de migrer (ou avant le prochain incident, si vous avez déjà migré)
- Activez la bonne télémétrie maintenant : Performance Insights, slow query log (avec échantillonnage sensé), et Enhanced Monitoring où approprié.
- Définissez les SLO et les actions « arrêter l’hémorragie » : quels jobs batch peuvent être mis en pause, quels endpoints peuvent être dégradés, et qui peut actionner ces bascules.
- Cartographiez explicitement les limites : max connections, type de stockage/IOPS, comportement tmp, attentes de basculement, et sémantique des changements de parameter group (dynamique vs reboot).
- Faites une répétition de charge : simulez les deux pics de trafic principaux déjà observés et confirmez comment le système se dégrade.
- Rédigez le runbook comme si vous le liriez à moitié endormi : étapes courtes, requêtes exactes, points de décision, et critères de rollback.
Pendant un incident (séquence de triage qui fonctionne dans le monde réel)
- Arrêtez l’amplification : limitez les retries, mettez en pause les workers batch, et plafonnez les pools de connexions. Si vous ne faites rien d’autre, faites ceci.
- Classez le goulot : CPU vs I/O vs verrous en utilisant processlist + variables clés + métriques plateforme.
- Identifiez les formes SQL principales : digests / PI top waits / slow log. Choisissez le coupable principal, pas l’équipe la plus bruyante.
- Appliquez la mitigation à moindre risque : désactivez une fonctionnalité côté requête, réduisez la concurrence, ou redirigez soigneusement les lectures/écritures.
- Ce n’est qu’après que vous tunez ou scalez : le scaling d’instance et les changements de paramètres sont des outils valides, mais ce ne sont pas des soins d’urgence.
- Enregistrez les horodatages : chaque action, chaque variation de métrique, chaque changement. Votre postmortem en dépend.
Après l’incident (prévenir la récurrence, pas seulement l’embarras)
- Transformez la cause racine en garde‑fou : linting des requêtes, revue des migrations, alertes de capacité sur le véritable goulot (souvent I/O), et automatisation de délestage de charge.
- Réparez le cycle de vie des données : archivage, partitionnement, et hygiène des index sur les tables principales.
- Exercez les restaurations et les basculements : chronométrez-les de bout en bout, incluant le cutover applicatif et la vérification.
- Rendez les régressions de performance plus difficiles à livrer : capturez les plans de requêtes pour les requêtes critiques et comparez‑les entre releases.
FAQ
1) Is RDS MySQL “real MySQL”?
Au niveau SQL, oui. Opérationnellement, c’est MySQL dans un environnement géré avec des contraintes : pas d’accès root, comportements de stockage gérés, et mécanismes de basculement et de sauvegarde définis par le produit.
2) What’s the single biggest “hidden limit” difference?
La prévisibilité des performances de stockage. Sur des hôtes autogérés vous pouvez souvent voir et tuner toute la pile ; sur RDS vous devez choisir la bonne classe de stockage et surveiller l’I/O avec les outils fournis par RDS.
3) Why do incidents on RDS feel harder to debug?
Parce que vous ne pouvez pas descendre sur l’hôte et lancer des outils au niveau OS. Vous devez vous fier à l’instrumentation MySQL et à la télémétrie RDS. Si vous ne les avez pas activées, vous déboguez avec la moitié des lumières éteintes.
4) Should we just crank max_connections to avoid connection errors?
Non. C’est ainsi que vous transformez un petit pic de trafic en un désastre mémoire et de contention. Utilisez du pooling, plafonnez les connexions par service, et traitez les erreurs de connexion comme un signal de backpressure.
5) Why does scaling the instance class sometimes not fix performance?
Parce que vous pouvez être I/O-bound ou lock-bound. Plus de CPU ne corrige pas la latence disque, et plus de RAM ne corrige pas les verrous metadata. Diagnostiquez d’abord, puis scalez selon le véritable goulot.
6) Are read replicas a safe way to scale reads during incidents?
Seulement si vous surveillez le lag et que votre application est conçue pour ça. Les replicas sont utiles jusqu’à ce qu’ils soient en retard ; ensuite ils deviennent un problème de cohérence, pas une solution de capacité.
7) What’s the fastest safe intervention when latency spikes?
Arrêtez l’amplification : mettez en pause les jobs batch, réduisez la concurrence des workers, et limitez les retries. Puis identifiez si vous êtes en lock/I/O/CPU avant d’appliquer des changements profonds.
8) Do parameter group changes always require downtime?
Non, mais beaucoup nécessitent un reboot. Durant un incident, « on changera un paramètre » n’aide que si vous savez déjà s’il est dynamique et quels effets secondaires il a.
9) How do we avoid temp table incidents on RDS?
Corrigez les formes de requête qui débordent (index, réduire le coût des sorts/group by), limitez la concurrence des rapports lourds, et surveillez Created_tmp_disk_tables comme un avertissement précoce, pas une statistique trivia.
10) Is Multi-AZ enough for high availability?
C’est nécessaire, pas suffisant. Votre appli doit gérer les erreurs transitoires, se reconnecter correctement, éviter les tempêtes de retries, et tolérer des brownouts brefs. La HA est une propriété système, pas une case à cocher.
Conclusion : ce qu’il faut changer lundi
MySQL et RDS MySQL exécutent le même SQL, mais ils échouent différemment. Les pannes autogérées impliquent souvent un hôte que vous pouvez pousser jusqu’à obtenir une aveu. Les pannes RDS impliquent souvent une limite à laquelle vous avez consenti des mois plus tôt et que vous avez oublié de surveiller.
Prochaines étapes qui réduiront réellement le temps d’incident :
- Activez et vérifiez la visibilité au niveau du moteur (Performance Insights et usage de performance_schema), et exercez‑vous à l’utiliser sous charge.
- Construisez des tableaux de bord qui répondent vite à une question : le goulot est‑il CPU, I/O, ou verrous ?
- Rédigez un runbook de délestage : quels jobs sont mis en pause, quels endpoints se dégradent, et comment arrêter les tempêtes de retries.
- Revisitez les choix de stockage et la topologie de réplication en fonction de la charge réelle, pas des valeurs par défaut héritées.
- Faites des responsables des 5 formes de requête les plus coûteuses : index, contrôles de stabilité du plan, et voies sûres de déploiement.
Si vous faites ces cinq choses, le prochain incident restera stressant. Mais il ne sera plus mystérieux. Et les incidents mystérieux sont ceux qui vous vieillissent.