Les deadlocks sont le type de problème de base de données qui surgit exactement quand vous jonglez déjà avec un pager, un CEO « juste pour vérifier »,
et un tableau de bord tout en rouge. Les requêtes se bloquent, les workers s’accumulent, et soudain votre système « hautement disponible » est très disponible
pour recevoir des plaintes.
La question pratique n’est pas « qu’est-ce qu’un deadlock ». Vous le savez déjà. La question est : lorsque la production est en feu, quel moteur vous donne
des indices utiles plus rapidement — MySQL ou MariaDB — et que devez-vous faire en premier, deuxième et troisième pour arrêter l’hémorragie.
Le verdict sans détour : quel est le plus facile à déboguer
Si vous exécutez MySQL moderne (8.0+), MySQL est généralement plus facile à déboguer pour les deadlocks et les lock waits sur le moment,
parce que Performance Schema est une source de vérité plus cohérente et plus riche : qui tient quel verrou, qui attend, quel texte SQL,
quels index, quelles transactions. Ce n’est pas parfait, mais c’est opérationnellement cohésif. Vous pouvez créer une mémoire musculaire et des runbooks fiables.
MariaDB peut aussi être parfaitement débogable — surtout si vous êtes disciplinés pour activer la bonne instrumentation et les bons logs — mais
en pratique il est plus facile de se retrouver avec une observabilité partielle : assez pour savoir que vous êtes bloqué, pas assez pour savoir pourquoi.
Aussi : l’écosystème MariaDB est plus variable (différents forks/engines/couches de réplication comme Galera sont plus courants),
ce qui signifie que votre histoire « standard » de deadlock est moins standard.
Voici la règle pratique que j’utilise :
- Si vous avez déjà des tableaux de bord et des runbooks basés sur Performance Schema, MySQL 8.0 tend à gagner en rapidité pour trouver la cause racine.
- Si vous êtes sur MariaDB avec Galera, vous ne déboguez pas seulement des deadlocks InnoDB — vous déboguez souvent des conflits de certification, qui ressemblent à des deadlocks pour l’application et paraissent plus graves.
- Si vous vous fiez uniquement à « SHOW ENGINE INNODB STATUS », les deux finiront par vous trahir. C’est un artefact d’incident final, pas une stratégie de monitoring.
Conseil opinionné : pour les équipes qui n’ont pas déjà une observabilité base de données mature, choisissez la plateforme qui rend les réponses les plus faciles
à obtenir avec le moins de hacks sur-mesure. C’est généralement MySQL 8.0 avec Performance Schema, plus une journalisation raisonnable.
Une citation à garder en tête :
« L’espoir n’est pas une stratégie. »
— General Gordon R. Sullivan
Blague #1 : Un deadlock, c’est juste deux transactions qui font une réunion et s’accordent à ne jamais faire de compromis.
Faits intéressants et contexte historique (ce qui compte à 03:00)
- MariaDB a été créée en 2009 après qu’Oracle ait acquis Sun (et donc MySQL). Cette décision de fork résonne encore dans les outils et les valeurs par défaut.
- InnoDB est devenu le moteur par défaut dans MySQL 5.5. Les systèmes plus anciens « nés » avant 5.5 gardent souvent des habitudes MyISAM qui causent des douleurs de verrouillage modernes.
- MySQL 8.0 a fait de Performance Schema le centre de gravité pour l’instrumentation. C’est pourquoi c’est le premier endroit où atterrissent les runbooks MySQL modernes.
- MariaDB a remplacé historiquement l’InnoDB en amont par XtraDB (puis est revenu plus près ensuite). Selon la version, vous verrez différents noms de variables et comportements autour de l’instrumentation.
- Les deadlocks sont un signe de correction, pas d’échec : InnoDB détecte les cycles et annule une victime pour maintenir le système en mouvement. L’échec survient quand votre appli ne peut pas retenter en toute sécurité.
- Les gap locks et next-key locks sont une source fréquente de « deadlock surprise » sous REPEATABLE READ. Beaucoup d’équipes ne les découvrent qu’après un incident.
- La réplication de type Galera (courante dans les déploiements MariaDB) peut renvoyer des erreurs de conflit qui imitent des deadlocks côté application, même sans un cycle classique InnoDB.
- Le schéma sys de MySQL (une couche d’aide au-dessus de Performance Schema) a rendu les requêtes « ce qui se passe maintenant » plus lisibles pour les humains ; cela a changé la façon dont les opérateurs agissent sous pression.
À quoi ressemblent les deadlocks en production réelle
Les deux modes de défaillance que les opérateurs confondent (et ne devraient pas)
Dans le chat d’incident, vous entendrez « deadlock » utilisé pour deux choses différentes :
- Deadlock (cycle) : InnoDB détecte un cycle et abandonne rapidement une transaction. Vous voyez l’erreur 1213 :
Deadlock found when trying to get lock. - Timeout d’attente de verrou (file d’attente) : Il n’y a pas de cycle, juste une attente longue. Finalement l’attendant abandonne avec l’erreur 1205 :
Lock wait timeout exceeded.
Opérationnellement, ces cas nécessitent des instincts différents. Un vrai deadlock est souvent un pattern (deux chemins de code prenant des verrous dans des ordres différents).
Un timeout d’attente de verrou est souvent un problème de ligne chaude / index chaud / transaction longue, parfois combiné avec des index manquants.
Les deux peuvent survenir en même temps pendant une panne, ce qui vous fait vous énerver contre le mauvais graphe.
Pourquoi « mais ce sont juste deux UPDATE » est un piège
InnoDB ne verrouille pas les « lignes » comme les développeurs l’imaginent. Il verrouille des enregistrements d’index, peut verrouiller des gaps entre eux,
et le fait selon le niveau d’isolation et le chemin d’accès. Si vous scannez une plage d’index, vous pouvez verrouiller bien plus que prévu.
Si vous mettez à jour un index secondaire, vous pourriez prendre des verrous dont vous n’aviez pas conscience.
Ce qui rend les deadlocks douloureux sous charge
- Tempêtes de retry : votre appli retente les transactions deadlockées de façon agressive, aggravant la contention.
- Transactions longues : une seule transaction lente tient des verrous plus longtemps, augmentant la probabilité de cycles.
- Effondrement de la file d’attente : les threads s’entassent en attente de verrous ; la latence devient non linéaire ; l’épuisement des pools suit.
- Lag de réplication : sur des replicas asynchrones, les transactions longues et les lock waits font exploser le temps d’application ; sur du semi-sync, elles peuvent ralentir les commits.
Blague #2 : Si vous voulez que deux équipes s’alignent sur l’ordre des verrous, dites-leur que c’est obligatoire et planifiez une réunion — deadlock garanti.
Télémétrie et outils : MySQL vs MariaDB sous pression
L’avantage de MySQL : une histoire d’instrumentation unique
Performance Schema de MySQL 8.0 est l’endroit où aller pour répondre à « qui bloque quoi » sans deviner. L’avantage opérationnel clé
est la cohérence : mêmes tables, mêmes chemins de jointure, même modèle mental sur tous les hôtes. Quand le site brûle, la cohérence, c’est la vitesse.
Vous pouvez généralement obtenir :
- les waiters et blockers actuels (
performance_schema.data_lock_waits) - l’inventaire des verrous (
performance_schema.data_locks) - les métadonnées des transactions (
information_schema.innodb_trx) - le texte SQL et les digests de statements (
events_statements_current, résumés de digest)
MariaDB : peut être bon, mais surveillez la version et le parcours moteur
MariaDB a aussi Performance Schema, mais opérationnellement il est plus courant de le trouver désactivé, partiellement activé, ou moins exploité.
MariaDB s’appuie aussi sur des vues Information Schema comme INFORMATION_SCHEMA.INNODB_LOCK_WAITS dans de nombreuses boutiques (lorsqu’elles sont disponibles),
et bien sûr le vieux cheval de bataille : SHOW ENGINE INNODB STATUS.
Si votre parc MariaDB inclut Galera, vous avez besoin d’un modèle mental supplémentaire : toutes les « transactions abandonnées » ne sont pas des deadlocks InnoDB.
Certaines sont des conflits de write set au niveau réplication. La débogabilité dépend de si vous collectez ces stats et les cartographiez au comportement applicatif.
Différences de journalisation qui déterminent si vous trouvez le coupable en minutes ou en heures
Les logs de deadlock ne valent que par votre configuration. Dans MySQL comme dans MariaDB, vous voulez :
- la journalisation des deadlocks activée (InnoDB imprime des détails dans l’error log)
- horodatages + IDs de thread alignés avec vos logs applicatifs
- slow log et/ou données de statement digest pour relier le deadlock à la forme de la requête, pas seulement à un extrait SQL isolé
Réalité : un rapport de deadlock sans identification du chemin de code n’est qu’un biscuit de fortune coûteux.
Playbook de diagnostic rapide
C’est la séquence « arrêter de deviner ». Elle est volontairement courte. Sous charge, la première priorité est d’identifier :
(1) si vous avez une tempête de deadlocks ou une file d’attente de lock-wait, (2) quelles tables/index sont chauds, (3) quelle transaction est le brute.
Première étape : confirmez quel type de douleur vous avez
- Vérifiez les taux d’erreur dans l’appli : voyez-vous 1213 (deadlock) ou 1205 (timeout) ?
- Vérifiez les compteurs d’état InnoDB : les lock waits montent-ils vite ? La history list length augmente-t-elle (transactions longues) ?
- Confirmez la santé du thread pool / connection pool : des threads sont-ils bloqués sur « Waiting for table metadata lock » ou sur des row lock waits ?
Deuxième étape : trouvez le(s) bloqueur(s) et les objets chauds
- Listez les lock waits et identifiez les IDs de transactions bloquantes.
- Cartographiez les bloqueurs au texte SQL et au client/utilisateur/hôte.
- Identifiez la table et l’index impliqués. Si c’est un scan de second index en plage, supposez que le chemin d’accès est le bug.
Troisième étape : choisissez une action de confinement
- Tuez la transaction bloqueuse la plus impactante si c’est sûr (souvent un job batch long ou une requête de maintenance bloquée).
- Réduisez temporairement la concurrence de l’endpoint/du type de worker fautif.
- Ajustez le comportement de retry : backoff exponentiel avec jitter, et plafonnement des retries. Ne créez pas un DDOS de retries.
Ce n’est qu’après le confinement que vous faites le travail « joli » : indexation, réécriture de requête, ordre des verrous, revue du niveau d’isolation.
Tâches pratiques : commandes, sorties, décisions (12+)
Le but de ces tâches n’est pas de collecter des trivia. C’est de transformer le mystère en une décision : « tuer ceci », « limiter cela », « réécrire cette requête »,
« ajouter cet index », ou « changer la frontière de transaction ». Chaque tâche inclut : commande, ce que signifie la sortie, et ce que vous faites ensuite.
Task 1: confirm engine/version and whether you’re even playing the same game
cr0x@server:~$ mysql -uroot -p -e "SELECT VERSION() AS version, @@version_comment AS comment, @@innodb_version AS innodb_version\G"
*************************** 1. row ***************************
version: 8.0.36
comment: MySQL Community Server - GPL
innodb_version: 8.0.36
Signification : La version vous indique quelles tables d’instrumentation existent et quelles valeurs par défaut s’appliquent.
Décision : Si vous êtes sur MySQL 5.7 / une ancienne MariaDB, prévoyez moins d’options d’introspection en live ; comptez davantage sur les logs et SHOW ENGINE INNODB STATUS.
Task 2: check whether you’re dealing with deadlocks or timeouts at the server level
cr0x@server:~$ mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_timeouts';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Innodb_deadlocks| 482 |
+-----------------+-------+
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| Innodb_row_lock_timeouts | 91 |
+---------------------------+-------+
Signification : Une montée rapide des deadlocks pointe vers un ordre de verrouillage contradictoire ; des timeouts pointent vers des attentes longues et des hotspots.
Décision : Si Innodb_deadlocks grimpe, priorisez l’identification des deux formes SQL impliquées. Si les timeouts montent, chassez la transaction la plus longue et les index manquants.
Task 3: capture the latest deadlock narrative (fast, but not comprehensive)
cr0x@server:~$ mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G" | sed -n '/LATEST DETECTED DEADLOCK/,+80p'
LATEST DETECTED DEADLOCK
------------------------
2025-12-29 10:41:12 0x7f1c4c1fe700
*** (1) TRANSACTION:
TRANSACTION 123456789, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 9012, OS thread handle 139759, query id 882199 appdb 10.2.3.44 appuser updating
UPDATE orders SET status='paid' WHERE id=778812
*** (2) TRANSACTION:
TRANSACTION 123456790, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 9013, OS thread handle 139760, query id 882200 appdb 10.2.3.45 appuser updating
UPDATE orders SET status='shipped' WHERE id=778812
*** WE ROLL BACK TRANSACTION (1)
Signification : Vous obtenez le SQL victime et le pair en conflit, plus des détails d’index/verrou plus bas.
Décision : Si les deux statements proviennent du même chemin de code, vérifiez l’ordre non déterministe ou les lectures cachées (vérifications de clés étrangères, triggers). Si de chemins différents, alignez l’ordre des verrous ou séparez le modèle de propriété des lignes.
Task 4 (MySQL 8.0): list current blockers and waiters via Performance Schema
cr0x@server:~$ mysql -uroot -p -e "
SELECT
w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx,
dlw.OBJECT_SCHEMA, dlw.OBJECT_NAME, dlw.INDEX_NAME,
dlw.LOCK_TYPE, dlw.LOCK_MODE, dlw.LOCK_STATUS
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks dlw
ON w.REQUESTING_ENGINE_LOCK_ID = dlw.ENGINE_LOCK_ID
ORDER BY dlw.OBJECT_SCHEMA, dlw.OBJECT_NAME\G"
*************************** 1. row ***************************
waiting_trx: 123456799
blocking_trx: 123456650
OBJECT_SCHEMA: appdb
OBJECT_NAME: order_items
INDEX_NAME: idx_order_id
LOCK_TYPE: RECORD
LOCK_MODE: X
LOCK_STATUS: WAITING
Signification : Cela pointe vers la table/index chaud et l’ID de transaction bloqueuse.
Décision : Récupérez immédiatement le SQL et la durée des deux transactions. Si le bloqueur est « manifestement mauvais » (batch long), tuez-le. Si c’est du trafic normal, vous avez un problème structurel de contention.
Task 5 (MySQL 8.0): map transaction IDs to sessions and SQL text
cr0x@server:~$ mysql -uroot -p -e "
SELECT
t.trx_id, t.trx_started, TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_s,
p.ID AS processlist_id, p.USER, p.HOST, p.DB, p.COMMAND, p.TIME, p.STATE,
LEFT(p.INFO, 200) AS sql_sample
FROM information_schema.innodb_trx t
JOIN information_schema.PROCESSLIST p
ON p.ID = t.trx_mysql_thread_id
ORDER BY trx_age_s DESC
LIMIT 10\G"
*************************** 1. row ***************************
trx_id: 123456650
trx_started: 2025-12-29 10:38:01
trx_age_s: 191
processlist_id: 8441
USER: batch
HOST: 10.2.8.19:55312
DB: appdb
COMMAND: Query
TIME: 187
STATE: Updating
sql_sample: UPDATE order_items SET price=price*0.98 WHERE order_id IN (...)
Signification : Vous avez trouvé le bully : une transaction longue tenant des verrous.
Décision : Si ce n’est pas critique, tuez-la. Si c’est critique, limitez les écrivains concurrents et réduisez l’empreinte de verrou (indexation, découpage, prédicats plus étroits) après confinement.
Task 6: inspect lock wait timeout setting (and stop cargo-culting it)
cr0x@server:~$ mysql -uroot -p -e "SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';"
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
Signification : C’est la durée pendant laquelle une transaction attend avant d’abandonner. Cela ne prévient pas les deadlocks.
Décision : Ne « réglez » pas les deadlocks en mettant cette valeur à 1 ou 500. Ajustez-la selon vos budgets de latence utilisateur et la tolérance des jobs en arrière-plan, puis corrigez le pattern de contention.
Task 7: check isolation level (deadlocks are sensitive to it)
cr0x@server:~$ mysql -uroot -p -e "SELECT @@transaction_isolation AS isolation;"
+--------------+
| isolation |
+--------------+
| REPEATABLE-READ |
+--------------+
Signification : REPEATABLE READ peut introduire des next-key/gap locks pour des scans de plage et des recherches d’index.
Décision : Si les deadlocks impliquent des prédicats de plage (par ex. WHERE created_at BETWEEN...), envisagez READ COMMITTED pour la charge, mais seulement avec une revue explicite de la cohérence.
Task 8: detect metadata lock pileups (not deadlocks, but the outage looks similar)
cr0x@server:~$ mysql -uroot -p -e "SHOW PROCESSLIST;" | sed -n '1,20p'
Id User Host db Command Time State Info
8123 appuser 10.2.3.12:51221 appdb Query 45 Waiting for table metadata lock ALTER TABLE orders ADD COLUMN x INT
8124 appuser 10.2.3.13:51222 appdb Query 44 Waiting for table metadata lock SELECT * FROM orders WHERE id=...
8125 appuser 10.2.3.14:51223 appdb Query 44 Waiting for table metadata lock UPDATE orders SET ...
Signification : Un DDL bloque les reads/writes via MDL. Ce n’est pas un deadlock InnoDB ; c’est un incident de changement de schéma.
Décision : Mettez en pause/tuez le DDL si c’est sûr, ou déplacez-le vers une stratégie de online schema change. Ne perdez pas de temps à « déboguer des deadlocks » ici.
Task 9: check for long history list length (undo pressure, long transactions)
cr0x@server:~$ mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G" | sed -n '/History list length/,+3p'
History list length 987654
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 123456650, ACTIVE 191 sec
...
Signification : Une history list length massive signifie souvent des transactions longues empêchant le purge. Cela corrèle avec l’agitation des verrous et l’effondrement des performances.
Décision : Trouvez et corrigez les transactions longues : jobs batch, patterns « read in transaction », ou du code applicatif qui garde une transaction ouverte pendant qu’il appelle des services externes (oui, ça arrive).
Task 10: confirm indexes used by the deadlocking queries (the access path is often the bug)
cr0x@server:~$ mysql -uroot -p -e "EXPLAIN UPDATE order_items SET price=price*0.98 WHERE order_id IN (101,102,103)\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: order_items
partitions: NULL
type: range
possible_keys: idx_order_id
key: idx_order_id
key_len: 8
ref: NULL
rows: 45000
filtered: 100.00
Extra: Using where
Signification : Un range scan verrouillant beaucoup d’enregistrements est un contributeur classique de deadlock.
Décision : Changez la forme de la requête (découpez par primary key, joignez à une table temporaire d’IDs, ou assurez-vous d’un accès par égalité). Si elle doit toucher beaucoup de lignes, sérialisez ce type de workload.
Task 11 (MySQL 8.0): get statement digests to spot the top locking offenders
cr0x@server:~$ mysql -uroot -p -e "
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_LOCK_TIME/1000000000000 AS sum_lock_time_s,
SUM_TIMER_WAIT/1000000000000 AS sum_total_time_s
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_LOCK_TIME DESC
LIMIT 5\G"
*************************** 1. row ***************************
DIGEST_TEXT: UPDATE `order_items` SET `price` = `price` * ? WHERE `order_id` IN ( ... )
COUNT_STAR: 11922
sum_lock_time_s: 843.1200
sum_total_time_s: 910.5523
Signification : Cela vous donne la famille de requêtes récidivistes, pas un échantillon aléatoire.
Décision : Corrigez d’abord le digest avec le plus grand temps de verrou agrégé. C’est là que vous réduisez la fréquence des incidents.
Task 12: verify deadlock logging configuration
cr0x@server:~$ mysql -uroot -p -e "SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';"
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_print_all_deadlocks| OFF |
+---------------------------+-------+
Signification : Si OFF, vous n’obtenez que le dernier deadlock dans la sortie InnoDB status ; vous pouvez perdre l’historique en cas de tempête.
Décision : Activez-le dans les environnements où la forensique des deadlocks compte et où le volume de log est acceptable. Si vous ne pouvez pas supporter le bruit dans l’error log, appuyez-vous sur l’historique Performance Schema quand disponible.
Task 13: identify and kill a blocking session (surgical, not panicked)
cr0x@server:~$ mysql -uroot -p -e "KILL 8441;"
Query OK, 0 rows affected (0.00 sec)
Signification : La session 8441 est terminée ; sa transaction est rollbackée, libérant les verrous.
Décision : Ne faites cela que lorsque vous avez confirmé que c’est le bloqueur et que le coût du rollback est acceptable. Si la transaction a modifié des millions de lignes, la tuer peut aggraver temporairement l’I/O et l’undo churn.
Task 14: check replication lag and apply thread state (deadlocks can be upstream symptoms)
cr0x@server:~$ mysql -uroot -p -e "SHOW REPLICA STATUS\G" | sed -n '1,35p'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 420
Last_SQL_Error:
Replica_SQL_Running_State: Waiting for dependent transaction to commit
Signification : Le lag suggère que les transactions sont lentes à s’appliquer, souvent à cause de lock waits ou de grosses transactions.
Décision : Si le lag augmente pendant la tempête de deadlocks, attendez-vous à des lectures obsolètes sur les replicas et envisagez de temporairement diriger moins de lectures vers eux, ou d’arrêter les jobs non essentiels intensifs en écriture.
Task 15: check OS-level saturation (because the “deadlock” might be a slow disk pretending)
cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (db01) 12/29/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.31 0.00 6.88 34.22 0.00 46.59
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 120.0 980.0 4096.0 32768.0 48.10 0.85 92.30
Signification : Un high iowait et des await élevés indiquent une latence de stockage. Un I/O lent allonge le temps des transactions, augmentant la durée des verrous et la probabilité de deadlock.
Décision : Si le stockage est le goulot, votre « problème de deadlock » est en partie un problème d’infra : réduisez l’amplification d’écriture (batching, revue d’index) et corrigez la contention I/O sous-jacente.
Trois mini-histoires du monde de l’entreprise
Incident n°1 : la panne causée par une mauvaise hypothèse
Une plateforme commerce de taille moyenne a migré d’un déploiement MySQL legacy vers un cluster MariaDB parce que « c’est à peu près la même chose et nous aimons la gouvernance open source ».
La migration s’est bien passée. Les tests de performance semblaient corrects. Tout le monde a célébré de manière responsable en planifiant la bascule un matin de semaine.
L’hypothèse erronée était subtile : l’équipe a supposé que leur logique de retry — écrite des années auparavant — gérait les deadlocks « robustement » parce qu’elle retentait sur
des erreurs génériques de base de données. Elle retentait. Immédiatement. Sans backoff. Et elle retentait des transactions qui n’étaient pas idempotentes quand un travail partiel avait
déjà été effectué côté applicatif.
Le jour de la bascule, ils ont rencontré un pattern de conflit d’écriture qu’ils n’avaient pas vu auparavant. Quelques lignes chaudes (compteurs d’inventaire et états de commandes) sont devenues des aimants de contention.
La base de données a fait ce que font les bases : détecter les deadlocks et abandonner des victimes. L’appli a fait ce que font les applis mal supervisées : retenter tout, vite, en parallèle, sans considérer les effets visibles pour l’utilisateur.
Le site n’est pas tombé instantanément. Il est devenu plus lent. Puis les pools de workers se sont saturés. Puis le pool de connexions s’est rempli. Puis les health checks ont commencé à échouer.
De l’extérieur, cela ressemblait à « la base de données deadlocke constamment ». De l’intérieur, c’était « l’application amplifie un mécanisme de sécurité normal en un déni de service ».
La correction n’a pas été « revenir à MySQL ». La correction a été de l’ingénierie ennuyeuse : rendre les retries conditionnels, utiliser un backoff exponentiel avec jitter, plafonner les retries,
et rendre la transaction véritablement idempotente (ou concevoir une action compensatrice). Ils ont aussi introduit un budget de deadlock par endpoint : si le budget est dépassé,
l’endpoint se bride avant que la base de données ne soit forcée.
Incident n°2 : l’optimisation qui s’est retournée contre eux
Une société SaaS d’analytics avait un job de génération de rapports lent. Il mettait à jour une table « report_status » pour des millions d’utilisateurs.
Quelqu’un a repéré l’inefficacité classique : « On fait trop de commits. Emballons tout le job dans une seule transaction pour gagner en vitesse. »
Sur une base de staging tranquille, ça paraissait brillant. Une transaction, un commit, moins d’overhead. Les graphiques hochaient poliment la tête.
En production, cette « optimisation » s’est transformée en monstre tenant des verrous. La transaction tournait pendant des minutes, parfois plus.
Pendant ce temps elle tenait des verrous sur des entrées d’index secondaires et causait l’enfilade des autres workflows — surtout les updates orientés utilisateur.
Les deadlocks ont augmenté parce que toute autre transaction avait maintenant plus de temps pour rentrer en collision avec le long runner.
Le pattern d’incident était sournois : pas une seule panne dramatique, mais une spirale de latence qui s’amplifiait. Les requêtes clients ralentissaient.
Les tâches en arrière-plan retentaient. Le lag de réplication augmentait. Les opérateurs ont tué le job ; le rollback a pris du temps ; le système a empiré avant d’aller mieux.
L’équipe a ensuite essayé la pire « correction » possible : augmenter innodb_lock_wait_timeout. Maintenant les transactions attendaient plus longtemps avant d’échouer, la concurrence restait élevée alors que le système faisait moins de progrès.
La vraie correction a été opérationnellement peu sexy et techniquement correcte : découper le job en petits lots déterministes, commit tous les N enregistrements,
et ordonner les mises à jour par primary key pour garder l’acquisition des verrous cohérente. Ils ont accepté un peu plus d’overhead de commit en échange d’une durée de verrou prévisible.
Le job est devenu légèrement plus lent isolément et dramatiquement plus rapide pour le business parce que le site est resté réactif.
La leçon : toute optimisation qui augmente la durée d’une transaction est une optimisation contre votre propre uptime.
Incident n°3 : la pratique ennuyeuse qui a sauvé la journée
Une équipe fintech tournait MySQL 8.0 avec une hygiène opérationnelle stricte. Rien d’exotique : Performance Schema activé, error logs centralisés et indexables,
dashboards pour les lock waits, et un « incident query pack » standard dans un repo partagé. Chaque on-call avait exécuté ce pack au moins une fois lors d’un game day.
Un après-midi, une fonctionnalité anodine a causé une augmentation soudaine des deadlocks sur une table qui suivait l’état de vérification utilisateur.
Les clients ne pouvaient pas terminer l’onboarding. Les tickets support s’empilaient. L’on-call est intervenu.
Ils n’ont pas commencé par débattre des niveaux d’isolation ou accuser l’ORM. Ils ont lancé la requête de lock-wait contre Performance Schema, identifié le digest bloqueur,
et ont immédiatement vu une nouvelle forme de requête : un UPDATE avec un prédicat de plage sur une colonne timestamp, manquant d’index, exécuté dans une transaction qui lisait aussi une autre table.
Cette requête n’était pas seulement lente ; elle verrouillait des plages.
Le confinement a été simple : couper la fonctionnalité via feature flag et brider un worker en arrière-plan qui martelait la même table.
Avec la pression réduite, ils ont ajouté l’index composite manquant et réécrit la transaction pour acquérir les verrous dans un ordre cohérent.
L’incident s’est terminé rapidement, et le postmortem a été agréablement court.
La pratique qui les a sauvés n’était pas héroïque. C’était de l’instrumentation routinière, des runbooks routiniers, et la discipline de garder la portée des transactions petite.
L’ennuyeux, c’est bien. L’ennuyeux scale.
Prévenir les deadlocks sans se mentir
1) Faites des deadlocks une condition gérée, pas une exception surprise
Les deadlocks arriveront dans tout système suffisamment concurrent. Votre travail est de s’assurer qu’ils sont :
(a) rares, (b) peu coûteux, et (c) sans conséquence grave. Cela signifie que les retries doivent être sûrs.
Si vous ne pouvez pas retenter en toute sécurité, votre modèle de données et les frontières de transaction sont inadaptés à la concurrence.
2) Gardez les transactions courtes, déterministes et locales
L’amplificateur de deadlock le plus courant est le temps. Chaque milliseconde où vous tenez des verrous est un ticket de loterie supplémentaire pour une collision.
Réduisez la portée des transactions. N’ouvrez pas de transaction pendant que vous :
- appelez des services externes
- rendez des templates
- attendez sur une queue
- effectuez des lectures non indexées que vous « prévoyez de mettre à jour plus tard »
3) Utilisez un ordre de verrou cohérent entre les chemins de code
Si deux transactions touchent le même ensemble de lignes mais les verrouillent dans des ordres différents, vous avez une usine à deadlocks.
Corriger cela coûte souvent moins cher que d’essayer de « sur-indexer » le problème. Patterns courants :
- Mettez toujours à jour le parent avant l’enfant (ou l’inverse), mais choisissez-en un et appliquez-le.
- Quand vous mettez à jour plusieurs lignes, triez les IDs et mettez à jour dans cet ordre.
- Privilégiez les mises à jour single-row par primary key quand c’est possible.
4) Éliminez les hotspots dans la conception
Certaines tables sont nées chaudes : compteurs, lignes d’« état courant », leaderboards, stores de session.
Les hotspots causent à la fois deadlocks et timeouts parce qu’ils concentrent les écritures.
Options :
- shard par clé (même au sein d’une DB via partitioning ou bucketing côté appli)
- utiliser des tables append-only et calculer les agrégats de façon asynchrone
- éviter les compteurs « globaux » ; utiliser des compteurs par entité et agréger
5) Ne « réparez » pas les deadlocks en baissant l’isolation comme réflexe
Passer à READ COMMITTED peut réduire certains deadlocks liés aux gap-locks, oui. Cela peut aussi changer la sémantique applicative.
Si votre logique métier suppose des lectures consistantes dans une transaction, vous venez d’acheter une nouvelle classe de bugs.
Faites-le intentionnellement, avec revue de correction et tests.
Erreurs fréquentes : symptôme → cause racine → correction
1) Symptom : les deadlocks grimpent juste après un déploiement
Cause racine : nouvelle forme de requête touchant les lignes dans un ordre différent, ou ajout d’un scan de plage dû à un index manquant.
Correction : comparez les statement digests / top lock time avant et après ; revenez en arrière ou feature-flaggez ; ajoutez/ajustez des index ; appliquez l’ordre des verrous dans le code.
2) Symptom : de nombreuses sessions « Waiting for table metadata lock »
Cause racine : DDL bloquant via MDL, pas un deadlock InnoDB.
Correction : arrêtez/tuez le DDL si c’est sûr ; utilisez des méthodes de online schema change ; planifiez le DDL avec des garde-fous et du monitoring MDL.
3) Symptom : timeouts d’attente de verrou, mais peu d’erreurs deadlock
Cause racine : transaction longue ou job batch lourd tenant des verrous ; lignes chaudes ; latence de stockage étirant le temps des transactions.
Correction : identifiez la transaction la plus longue, découpez le job, ajoutez les index manquants, réduisez la contention I/O, et assurez-vous que l’appli ne garde pas les transactions ouvertes inutilement.
4) Symptom : les deadlocks arrivent « au hasard » sous charge
Cause racine : ordre de verrou non déterministe (IN lists non ordonnées, workers parallèles mettant à jour des ensembles qui se chevauchent), ou cascades de foreign key verrouillant des lignes additionnelles.
Correction : triez les clés avant les updates ; sérialisez les workers par partition key ; révisez les cascades de foreign key et les triggers ; envisagez un SELECT … FOR UPDATE explicite pour ordonner.
5) Symptom : augmenter innodb_lock_wait_timeout aggrave les choses
Cause racine : vous avez augmenté le temps d’attente des threads, consommant concurrence et mémoire, sans augmenter le débit.
Correction : baissez-le pour correspondre à votre budget de latence ; corrigez le bloqueur ; réduisez la contention ; implémentez du backpressure côté appli.
6) Symptom : les deadlocks disparaissent après l’ajout d’un index, mais les performances se dégradent
Cause racine : l’index a réduit la contention mais augmenté l’amplification d’écriture ; vous avez créé un nouvel index chaud ou rendu les updates plus lourds.
Correction : validez le coût d’écriture ; supprimez les index inutilisés ; envisagez des index composites qui correspondent aux patterns d’accès ; conservez l’ensemble minimal qui supporte les lectures/écritures critiques.
7) Symptom : dans les setups Galera, des aborts « semblables à des deadlocks » sans traces claires InnoDB
Cause racine : conflits de certification de réplication (deux nœuds valident des écritures conflictuelles).
Correction : réduisez le chevauchement d’écritures multi-master ; routez les écritures pour une entité donnée vers un nœud ; révisez la logique de retry et le monitoring du taux de conflit.
Checklists / plan étape par étape
Pendant l’incident (les 15 premières minutes)
- Classifiez l’erreur : 1213 deadlock vs 1205 timeout vs MDL wait. Ne les mélangez pas.
- Capturez les preuves rapidement : extrait InnoDB status, requête top lock-wait, et les sessions coupables les plus impactantes.
- Contenez : tuez le plus gros bloqueur si c’est sûr ; bridez les workers/endpoints fautifs ; désactivez la nouvelle fonctionnalité si corrélée.
- Stabilisez les retries : plafonnez les retries, ajoutez du backoff, et arrêtez de retenter les workflows non idempotents.
- Vérifiez les contraintes infra : latence de stockage, saturation CPU, pression sur le buffer pool. Les deadlocks sont plus faciles à déclencher sur un système lent.
Après le confinement (même jour)
- Identifiez les digests coupables principaux (pas des SQL uniques). Corrigez le plus gros contributeur.
- Réduisez la portée des transactions et supprimez les patterns « transaction pendant du travail ».
- Appliquez l’ordre des verrous dans le code et les tests (oui, des tests).
- Revue d’index : assurez-vous que les prédicats correspondent aux index ; évitez les scans de plage non voulus.
- Validez le coût du rollback : si vous tuez des bloqueurs régulièrement, comprenez l’undo churn et le comportement I/O.
Renforcement (c’est comme ça qu’on arrête les pages répétées)
- Activez une observabilité durable des deadlocks : logs acheminés, Performance Schema configuré, dashboards pour lock waits et âge des transactions.
- Créez un budget de deadlock par service : si le seuil est dépassé, auto-throttle ou shed load.
- Game day : entraînez-vous avec exactement les queries et commandes ci-dessus dans un scénario de contention synthétique.
- Revue des patterns de schéma : hotspots, compteurs, machines à états. Redesign si nécessaire.
FAQ
1) Un deadlock est-ce un bug de la base de données ?
En général non. C’est la base qui fait ce qu’il faut quand deux transactions créent un cycle. Le bug est typiquement dans la conception de la concurrence ou la gestion des retries.
2) Lequel est plus facile à déboguer : MySQL ou MariaDB ?
Avec des defaults modernes et un usage mature de Performance Schema, MySQL 8.0 tend à être plus rapide à déboguer. MariaDB peut être tout aussi exploitable, mais l’observabilité varie plus selon le déploiement.
3) Dois-je simplement activer innodb_print_all_deadlocks ?
Si vous pouvez supporter le volume de logs et que vous avez besoin d’historique forensique, oui. Mais ne le traitez pas comme votre seul outil. Sous forte agitation, les logs peuvent devenir du bruit et manquer quand même le pattern global.
4) Pourquoi des deadlocks arrivent-ils quand nous ne mettons à jour que par primary key ?
Parce que des index secondaires et des vérifications de foreign key peuvent toujours introduire des verrous. Aussi, « par primary key » n’est parfois pas réellement par primary key une fois que l’ORM s’en mêle.
5) Quelle est la différence pour l’appli entre deadlock et lock wait timeout ?
Les erreurs de deadlock sont des aborts immédiats pour briser un cycle ; les timeouts sont le système qui abandonne après attente. Les deux doivent être gérés, mais les timeouts indiquent souvent un bloqueur long ou des index manquants.
6) READ COMMITTED résout-il les deadlocks ?
Il peut réduire certains deadlocks liés aux gap-locks, mais il ne « résout » pas la catégorie des deadlocks. Il change aussi ce que vos transactions peuvent supposer. Traitez-le comme un changement d’ingénierie, pas comme un interrupteur.
7) Puis-je prévenir les deadlocks en augmentant innodb_lock_wait_timeout ?
Non. Cela affecte les timeouts, pas la détection des deadlocks. L’augmenter aggrave souvent les pannes en laissant les threads attendre plus longtemps et s’accumuler.
8) Quelle est la mitigation immédiate la plus sûre pendant une tempête ?
Brider la concurrence du chemin de code fautif et tuer la seule transaction bloqueuse la pire si le coût du rollback est acceptable. Puis corriger la forme de la requête et la portée de la transaction.
9) Comment savoir si c’est un problème de changement de schéma à la place ?
Si les sessions affichent « Waiting for table metadata lock », c’est du MDL. Les outils de deadlock n’aideront pas ; vous devez gérer la stratégie d’exécution du DDL.
Conclusion : prochaines étapes qui réduisent vraiment les pages
Si vous voulez moins d’incidents de deadlock, arrêtez de traiter les deadlocks comme de la lore effrayante et commencez à les traiter comme
un coût de concurrence observable et classifiable. MySQL 8.0 rend cela plus simple par défaut avec Performance Schema.
MariaDB peut aussi le faire, mais vous devrez être plus strict sur l’activation et la standardisation de la bonne télémétrie — surtout dans des topologies mixtes.
Prochaines étapes pratiques :
- Construisez un runbook en deux voies : deadlock (1213) vs timeout (1205) vs MDL. Outils différents, corrections différentes.
- Activez et vérifiez votre observabilité : journalisation des deadlocks, tables Performance Schema que vous interrogez réellement, et logs indexables.
- Corrigez le digest coupable principal par temps de verrou agrégé, pas la requête la plus effrayante que vous remarquez.
- Rendez les retries sensés : transactions idempotentes, backoff avec jitter, et plafonds. Arrêtez les tempêtes de retries avant qu’elles ne commencent.
- Raccourcissez les transactions et appliquez l’ordre des verrous. C’est le travail. C’est aussi la victoire.