Votre pod MySQL « a l’air correct » jusqu’à ce que le nœud redémarre, que Kubernetes commence à faire ce pour quoi il est conçu, et que soudainement la base de données se retrouve dans une boucle de redémarrages avec un volume qui refuse de se monter. Pendant ce temps, l’équipe applicative dit que c’est « un problème Kubernetes », et l’équipe plateforme dit que c’est « un problème de base de données ». Félicitations : vous êtes l’adulte dans la pièce maintenant.
Ceci est un guide pratique, légèrement subjectif, pour faire tourner MySQL et MariaDB sur Kubernetes sans transformer les probes en déni de service, les redémarrages en corruption, ou la « mentalité sans état » en perte de données. Nous expliquerons ce qui casse, pourquoi ça casse, et les commandes que vous taperez réellement à 2 h du matin.
Ce que Kubernetes fait réellement à votre base (et pourquoi ça fait mal)
Kubernetes est constant sur un point : il est indifférent. Il redémarrera votre processus si le probe échoue. Il déplacera votre pod si le nœud est drainé. Il tuera des conteneurs lorsque les ressources sont serrées. Il fera tout cela alors même que votre base est en plein flush, en pleine récupération ou au milieu d’une transaction, parce qu’il n’a aucun concept de « vache sacrée avec un journal d’écriture préalable ».
Les bases de données sont des machines à états avec des invariants coûteux. Les invariants d’InnoDB tournent autour des redo/undo logs, du doublewrite (selon les paramètres) et d’un fsync durable. MariaDB et MySQL partagent beaucoup de code génétique ici, mais la pression Kubernetes expose les bords différemment — surtout au démarrage, dans le comportement des probes et selon les piles de réplication que vous utilisez (réplication asynchrone, semi-sync ou Galera).
Les trois comportements Kubernetes qui comptent le plus pour les services d’état :
- Les probes sont des portes de trafic et des interrupteurs mortels. Readiness décide si vous recevez du trafic. Liveness décide si vous avez le droit de vivre. Startup existe parce que liveness est sinon un tyran.
- Les redémarrages sont « normaux ». Vous ne pouvez pas les traiter comme exceptionnels. Votre base doit pouvoir redémarrer de manière sûre, rapide et répétée.
- Le stockage est attaché/détaché par des humains (et des contrôleurs). Les délais de montage de volume, les attachements obsolètes et les fsync lents peuvent dominer absolument tout.
Si vous ne retenez qu’une vérité opérationnelle : pour les systèmes stateful, Kubernetes n’est pas « auto-réparateur », il est « auto-répétitif ». Votre travail est de rendre les répétitions sûres.
MySQL vs MariaDB : ce qui compte sous Kubernetes
MySQL et MariaDB sont assez proches pour endormir les équipes et les amener à supposer une équivalence opérationnelle. Cette hypothèse est la graine de plusieurs incidents très coûteux.
Comportement du moteur : globalement similaire, mais surveillez les valeurs par défaut et les cas limites
Dans des déploiements Kubernetes typiques, vous utiliserez InnoDB sur les deux. La récupération après crash existe sur les deux. Les deux peuvent récupérer d’une terminaison abrupte. Mais le diable est dans :
- La variance de durée de démarrage. Après un arrêt non propre, le temps de récupération InnoDB peut aller de quelques secondes à « votre liveness probe devient une arme ». La récupération dépend de la taille des redo logs, de l’âge du checkpoint, du débit IO et du comportement de fsync sur votre StorageClass.
- Les différences d’outillage client. Certaines images fournissent
mysqladmin, d’autres des wrappersmariadb-admin, et d’autres encore des clients minimaux. Les probes et scripts d’init qui hardcodent un outil échouent souvent sur l’autre au pire moment. - Détails GTID et réplication. Le GTID de MariaDB n’est pas le GTID de MySQL ; ils ne sont pas compatibles en drop-in. Si vous migrez ou mélangez des outils, le « c’est juste du GTID » devient une longue nuit.
Piles de réplication : asynchrone vs Galera modifient la manière dont les probes doivent se comporter
« MySQL sur Kubernetes » signifie souvent un primaire plus des réplicas (réplication asynchrone), ou un cluster géré par un opérateur. « MariaDB sur Kubernetes » peut être la même chose, ou Galera (wsrep) parce qu’il est facile de vendre le « multi-primary » à la direction.
Les probes pour la réplication asynchrone peuvent être simples : est-ce que mysqld est vivant et accepte des connexions, et (pour la readiness) la réplication est-elle suffisamment rattrapée ? Les probes pour Galera doivent considérer l’état du cluster : un nœud peut accepter du TCP et être quand même dangereux pour les écritures (ou même les lectures) selon qu’il est donor/desync.
Opérateurs et images : la préparation Kubernetes est une décision produit
Votre expérience opérationnelle dépend souvent plus du choix d’opérateur/image que de la marque de la base. Certains opérateurs implémentent des startupProbe sensés, des hooks d’arrêt gracieux et des verrous de récupération. D’autres livrent des valeurs par défaut optimistes et vous laissent apprendre par le feu.
Mon opinion : si vous faites tourner des services stateful sans opérateur opinionated (ou sans chart interne soigneusement entretenu), vous avez effectivement choisi « base de données pet sur orchestration cattle ». Ce n’est pas une stratégie ; c’est une ambiance.
Faits intéressants et contexte historique (les parties qu’on oublie)
- MariaDB a été créée en 2009 comme fork communautaire après l’acquisition de Sun par Oracle (et donc de MySQL).
- Le jeu de caractères par défaut de MySQL a évolué avec le temps (notamment vers
utf8mb4dans les versions récentes), ce qui impacte la compatibilité de schéma et la longueur des index lors de vraies mises à niveau. - L’implémentation GTID de MariaDB est différente de celle de MySQL ; des outils supposant la sémantique GTID MySQL peuvent mal diagnostiquer la santé de réplication sur MariaDB.
- La réplication Galera est devenue une « fonctionnalité phare » pour de nombreuses entreprises, mais c’est un modèle de défaillance différent de la réplication asynchrone : la membership et le quorum sont des exigences opérationnelles, pas des options agréables.
- Kubernetes a ajouté startupProbe relativement tard (par rapport à liveness/readiness), en grande partie parce que trop de workloads réels avaient un démarrage lent-mais-correct qui se faisait tuer.
- Le coût de la récupération InnoDB n’est pas linéaire avec la taille des données ; il est lié au volume des redo et au comportement des checkpoints, c’est pourquoi une « petite base » peut tout de même redémarrer douloureusement après des blocages IO.
- Le comportement de fsync a changé entre générations de stockage cloud ; ce qui était « acceptable » sur un SSD local peut devenir une tempête de redémarrages sur des volumes réseau avec une forte variance de latence.
- MySQL 8 a introduit un dictionnaire de données transactionnel, ce qui a amélioré la cohérence mais a aussi rendu certains comportements d’upgrade/downgrade et de récupération différents des lignes MySQL plus anciennes et de MariaDB.
Readiness, liveness et startup probes qui ne vous sabotent pas
Les probes sont l’endroit où Kubernetes touche votre base toutes les quelques secondes. Bien configurées, elles empêchent le trafic d’atteindre un pod malade. Mal configurées, elles créent la maladie.
Règles empiriques (opinionnées, parce que vous en avez besoin)
- N’utilisez jamais la liveness pour vérifier la « santé logique » de la base. La liveness doit répondre : « Le processus est-il bloqué au-delà d’un point de récupération ? » Si vous la rendez dépendante du lag de réplication ou d’une requête SQL complexe, vous tuerez des pods parfaitement récupérables.
- La readiness peut être stricte. La readiness est l’endroit pour bloquer le trafic basé sur « peut servir des requêtes maintenant » et (optionnellement) « est suffisamment rattrapé ».
- Utilisez startupProbe pour les fenêtres de récupération. Si vous ne le faites pas, la liveness tuera votre récupération InnoDB. Le processus redémarrera, réentrera en récupération et sera de nouveau tué. Cette boucle est classique.
- Privilégiez les probes exec utilisant le socket local quand c’est possible. Les probes TCP peuvent réussir alors que le SQL est bloqué. Les probes SQL peuvent échouer quand le DNS est lent. Les vérifications locales via socket réduisent les parties mouvantes.
À quoi ressemble une bonne vérification de readiness
La readiness doit être peu coûteuse et déterministe. Un modèle courant : se connecter localement et exécuter SELECT 1. Si vous êtes un réplique, vérifiez optionnellement le lag de réplication. Si vous êtes en Galera, vérifiez l’état wsrep.
Évitez les requêtes de « vérité coûteuse » comme scanner de grandes tables ou interroger des métadonnées qui verrouillent. Vous voulez « peut accepter une requête triviale et répondre vite ». Cela se corrèle bien avec l’expérience utilisateur, et ne fera pas s’effondrer votre pod sous la charge des probes.
À quoi ressemble une bonne vérification de liveness
La liveness doit être conservatrice. Pour MySQL/MariaDB, une liveness décente est un ping local utilisant les outils d’administration avec un timeout court. Si elle ne peut répondre au ping pour N vérifications consécutives, quelque chose est bloqué.
Startup probes : achetez du temps pour la récupération
Après un arrêt non propre, InnoDB peut légitimement prendre plusieurs minutes à récupérer. Vos probes doivent accorder ce temps. Ce n’est pas de la « tolérance », c’est de la correction.
Blague #1 : Une liveness qui tue mysqld pendant la récupération InnoDB, c’est comme vérifier le pouls d’un patient en débranchant le respirateur.
Timeouts des probes : le tueur silencieux
Beaucoup d’échecs de probes ne signifient pas « base tombée », mais « timeout de probe trop court pour la latence de stockage occasionnelle ». Si votre PVC est sur un stockage réseau, un timeout d’une seconde est un générateur d’incidents.
Redémarrages, sémantique d’arrêt et récupération InnoDB
La terminaison Kubernetes est un signal plus un délai. Il envoie SIGTERM, attend terminationGracePeriodSeconds, puis SIGKILL. Votre base a besoin d’assez de grâce pour flush, mettre à jour l’état et sortir proprement.
Un arrêt gracieux vous achète un démarrage plus rapide
Arrêt propre : moins de redo logs à appliquer, démarrage plus rapide, moins de messages d’avertissement effrayants, moins de temps en « not ready ». Arrêt non propre : temps de récupération qui dépend de l’IO, que vous ne pouvez pas contrôler pendant un événement de nœud.
Les boucles de redémarrages sont généralement des bugs de politique de probes
Si un pod redémarre répétitivement et que les logs montrent la récupération InnoDB démarrant à chaque fois, vos probes sont trop agressifs ou vous n’utilisez pas startupProbe. Kubernetes n’est pas en train de « guérir » votre BD ; il interrompt la guérison.
Idée de fiabilité (paraphrasée)
« L’espoir n’est pas une stratégie. » — idée paraphrasée souvent attribuée aux ingénieurs et opérateurs en fiabilité. Prenez-la comme consigne : concevez pour la défaillance, ne la laissez pas au hasard.
Stockage et sécurité des données : PVCs, systèmes de fichiers et le coût du fsync
Si vous voulez qu’une base de données soit durable, vous devez payer pour des écritures durables. Kubernetes ne change pas cela. Il rend juste la facturation plus confuse parce que la partie lente est maintenant une StorageClass.
Sémantiques de PVC qui comptent
- Mode d’accès (RWO vs RWX) : La plupart des volumes de bases doivent être RWO. Les systèmes de fichiers réseau RWX peuvent fonctionner, mais les performances et la sémantique de verrouillage varient ; ne mettez pas MySQL sur NFS en pensant que tout ira bien.
- Politique de reclaim : « Delete » a sa place, mais vous ne devriez pas la découvrir en production après avoir supprimé un StatefulSet.
- Délais d’attachement/montage : Un pod peut être schedule rapidement mais attendre longtemps l’attachement du volume. Les probes et timeouts doivent en tenir compte.
fsync et voisins
Les boutons de durabilité (comme innodb_flush_log_at_trx_commit et sync_binlog) interagissent avec le stockage sous-jacent. Sur un SSD local à faible latence, fsync par commit est gérable. Sur un volume réseau occupé avec des pics de latence, cela peut devenir un goulet d’étranglement de débit.
Baisser la durabilité pour « corriger les performances » est une décision métier, même si vous prétendez que c’est technique. Si vous relaxez le fsync, vous choisissez la quantité de données que vous êtes prêt à perdre sur une défaillance de nœud.
Système de fichiers et options de montage
ext4 et XFS sont les choix habituels. Ce qui compte davantage, c’est la cohérence et l’observabilité : vous devez savoir exactement sur quoi vous tournez. Aussi, si vous utilisez des systèmes de fichiers overlay de manière étrange, arrêtez. Les bases veulent du stockage bloc ennuyeux.
Blague #2 : Il y a deux types de stockage : celui que vous mesurez, et celui qui vous mesure en production.
Tâches pratiques : commandes, sorties attendues et la décision que vous prenez
Voici des tâches réelles que vous pouvez exécuter depuis une machine admin avec kubectl, plus quelques vérifications depuis l’intérieur du conteneur. Chaque tâche inclut ce que la sortie implique et quelle décision prendre ensuite.
Tâche 1 : Vérifier si vous avez un problème de probe ou un crash
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mysql-0 0/1 CrashLoopBackOff 7 18m 10.42.3.17 node-7
Sens : CrashLoopBackOff avec plusieurs redémarrages suggère soit que le processus sort, soit que la liveness le tue. Pas encore assez d’informations.
Décision : Inspectez d’abord les events et les logs précédents ; ne touchez pas aux réglages MySQL au hasard.
Tâche 2 : Lire les events du pod pour attraper la liveness/readiness qui vous tue
cr0x@server:~$ kubectl -n prod describe pod mysql-0 | sed -n '/Events:/,$p'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 2m (x12 over 16m) kubelet Liveness probe failed: command timed out: context deadline exceeded
Normal Killing 2m (x12 over 16m) kubelet Container mysql failed liveness probe, will be restarted
Sens : Le conteneur est tué par un timeout de liveness, pas nécessairement en train de crasher.
Décision : Ajoutez/ajustez startupProbe et augmentez timeoutSeconds des probes. Vérifiez aussi la latence du stockage avant d’accuser MySQL.
Tâche 3 : Comparer les logs actuels et précédents du conteneur
cr0x@server:~$ kubectl -n prod logs mysql-0 -c mysql --previous --tail=60
2025-12-31T01:12:17.812345Z 0 [Note] InnoDB: Starting crash recovery from checkpoint LSN=187654321
2025-12-31T01:12:46.991201Z 0 [Note] InnoDB: 128 out of 1024 pages recovered
2025-12-31T01:12:48.000901Z 0 [Note] mysqld: ready for connections.
2025-12-31T01:12:49.103422Z 0 [Warning] Aborted connection 12 to db: 'unconnected' user: 'healthcheck' host: 'localhost' (Got timeout reading communication packets)
Sens : Il est effectivement arrivé à « ready for connections » puis a été tué/échoué par des timeouts.
Décision : La requête de probe est probablement trop lente ou le timeout trop court sous pression IO. Corrigez d’abord les probes ; puis mesurez l’IO.
Tâche 4 : Confirmer les définitions de probes (vous seriez surpris de combien sont erronées)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.spec.containers[0].livenessProbe.exec.command}{"\n"}{.spec.containers[0].readinessProbe.exec.command}{"\n"}{.spec.containers[0].startupProbe.exec.command}{"\n"}'
[mysqladmin ping -h 127.0.0.1 -uroot -p$(MYSQL_ROOT_PASSWORD)]
[mysql -h 127.0.0.1 -uroot -p$(MYSQL_ROOT_PASSWORD) -e SELECT 1]
[]
Sens : Pas de startupProbe configuré. La liveness utilise une expansion de mot de passe qui peut ne pas fonctionner selon le shell.
Décision : Ajoutez startupProbe et préférez un script wrapper qui gère les secrets en toute sécurité. Évitez aussi de mettre des mots de passe directement dans les args de commande.
Tâche 5 : Vérifier la période de grâce de terminaison (l’arrêt propre compte)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.spec.terminationGracePeriodSeconds}{"\n"}'
30
Sens : 30 secondes est souvent trop court pour une instance InnoDB chargée afin de s’arrêter proprement.
Décision : Augmentez-la (souvent 120–300s selon la charge) et assurez-vous que votre conteneur gère correctement SIGTERM.
Tâche 6 : Vérifier le PVC et si le volume est bloqué en attachement
cr0x@server:~$ kubectl -n prod get pvc -l app=mysql
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
data-mysql-0 Bound pvc-3d5b2c9a-6c62-4a9c-a0c2-1f2a2e6b9b8a 200Gi RWO fast-ssd
Sens : Le PVC est bound ; l’attachement peut être lent, mais au moins le provisioning ne rate pas.
Décision : Si le pod est Pending, examinez les events d’attachement ; si en Running mais lent, passez au diagnostic IO.
Tâche 7 : Vérifier la pression au niveau du nœud qui déclenche évictions ou throttling
cr0x@server:~$ kubectl describe node node-7 | sed -n '/Conditions:/,/Addresses:/p'
Conditions:
Type Status LastHeartbeatTime Reason Message
---- ------ ----------------- ------ -------
MemoryPressure False 2025-12-31T01:15:10Z KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure True 2025-12-31T01:15:10Z KubeletHasDiskPressure kubelet has disk pressure
PIDPressure False 2025-12-31T01:15:10Z KubeletHasSufficientPID kubelet has sufficient PID available
Sens : DiskPressure True peut provoquer des évictions et se corrèle avec des blocages IO.
Décision : Corrigez la pression disque du nœud (GC des images, nettoyage des logs, disque root plus grand). Ne réglez pas MySQL pour « résoudre » la famine de nœud.
Tâche 8 : Regarder les redémarrages du conteneur avec codes de sortie (crash process vs kill)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{" "}{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
137 Error
Sens : Le code de sortie 137 signifie généralement SIGKILL (souvent kill de liveness ou OOM kill).
Décision : Vérifiez les events pour OOMKilled ; sinon considérez un problème de probe/période de grâce.
Tâche 9 : Vérifier si mysqld est OOM-killé pendant la récupération
cr0x@server:~$ kubectl -n prod describe pod mysql-0 | grep -E 'OOMKilled|Reason:|Last State' -n
118: Last State: Terminated
119: Reason: OOMKilled
Sens : La limite mémoire est trop basse pour la charge ou la phase de récupération ; InnoDB peut allouer en pics (buffer pool, caches, buffers de tri selon la config).
Décision : Augmentez les limites mémoire, réduisez le buffer pool InnoDB, et assurez-vous de ne pas utiliser des buffers par connexion agressifs avec un max connections élevé.
Tâche 10 : Inspecter rapidement l’état MySQL/MariaDB en direct (dans le pod)
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysqladmin -uroot -p"$MYSQL_ROOT_PASSWORD" ping && mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE '\''Uptime'\'';"'
mysqld is alive
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Uptime | 43 |
+---------------+-------+
Sens : Le serveur est up et répond. Si la readiness échoue encore, la probe de readiness est incorrecte ou trop stricte.
Décision : Alignez la logique de readiness avec ce que vous venez de prouver : connectivité basique plus verrous minimaux d’exactitude.
Tâche 11 : Vérifier les indicateurs de récupération InnoDB et les réglages de durabilité
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES WHERE Variable_name IN (\"innodb_flush_log_at_trx_commit\",\"sync_binlog\",\"innodb_doublewrite\");"'
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_doublewrite | ON |
| innodb_flush_log_at_trx_commit | 1 |
| sync_binlog | 1 |
+------------------------------+-------+
Sens : C’est le mode de durabilité maximal. Bon pour la sécurité des données, potentiellement brutal sur du stockage lent.
Décision : Si la latence est inacceptable, corrigez d’abord le stockage. Ne relâchez ces paramètres que si le métier accepte explicitement le risque de perte.
Tâche 12 : Mesurer le lag de réplication (cas réplique asynchrone)
cr0x@server:~$ kubectl -n prod exec -it mysql-1 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"'
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 187
Sens : La réplique est en retard. Elle peut être vivante mais pas prête à servir des lectures si votre appli attend des données fraîches.
Décision : Utilisez readiness pour bloquer le trafic selon un seuil de lag approprié pour votre appli. N’utilisez pas la liveness pour cela.
Tâche 13 : Mesurer l’état wsrep (cas MariaDB Galera)
cr0x@server:~$ kubectl -n prod exec -it mariadb-0 -c mariadb -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW STATUS LIKE '\''wsrep_%'\'';" | egrep "wsrep_local_state_comment|wsrep_cluster_status|wsrep_ready"'
wsrep_cluster_status Primary
wsrep_local_state_comment Synced
wsrep_ready ON
Sens : Le nœud est dans la composante Primary, synchronisé et prêt. C’est l’état souhaité avant de déclarer readiness.
Décision : Si wsrep_ready est OFF ou que l’état est Donor/Joining, gardez le pod NotReady pour éviter des lectures/écritures inconsistantes.
Tâche 14 : Vérifier que le système de fichiers est bien ce que vous pensez
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'df -T /var/lib/mysql | tail -n +2'
/dev/nvme1n1 ext4 205113212 83412228 111202456 43% /var/lib/mysql
Sens : ext4 sur un bloc device est typique. Si vous voyez nfs ou quelque chose de surprenant, ajustez vos attentes et vos probes.
Décision : Si c’est un système de fichiers réseau, soyez beaucoup plus conservateur sur les timeouts et envisagez de changer de StorageClass pour de l’OLTP en production.
Tâche 15 : Surveiller les symptômes d’un fsync lent dans le statut MySQL
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE \"Innodb_os_log_fsyncs\"; SHOW GLOBAL STATUS LIKE \"Innodb_log_waits\";"'
+---------------------+--------+
| Variable_name | Value |
+---------------------+--------+
| Innodb_os_log_fsyncs| 983211 |
+---------------------+--------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_log_waits | 1249 |
+------------------+-------+
Sens : Innodb_log_waits indique une contention sur le flush du log ; souvent corrélé avec la latence IO ou des fichiers logs/taille de buffers trop petits.
Décision : Examinez la latence du stockage et considérez des changements de configuration des logs. Si vous êtes sur des volumes réseau bruyants, corrigez cela d’abord.
Playbook de diagnostic rapide : trouvez le goulot avant de blâmer le mauvais élément
Quand un pod de base de données clignote ou est lent sur Kubernetes, vous pouvez passer des heures à débattre « BD vs plateforme ». Ne le faites pas. Suivez une séquence qui vous indique rapidement où se situe la réalité.
Première étape : déterminer si Kubernetes le tue, ou s’il plante
- Events du pod : échecs de liveness, OOMKilled, éviction, problèmes de montage de volume.
- Code de sortie : 137 (kill) vs une stack trace de crash mysqld.
- Logs précédents : est-ce qu’il a atteint « ready for connections » ?
Deuxième étape : déterminer si le stockage est le goulot
- Conditions du nœud : DiskPressure, indices de saturation IO.
- Temps de démarrage/récupération : si ça échoue seulement lors des fenêtres de récupération après crash, suspectez une IO lente et des probes trop agressifs.
- Indicateurs base : log waits, checkpoints bloqués, lag d’application de réplication corrélé à la charge d’écriture.
Troisième étape : déterminer si votre logique de probes est sur-ajustée
- La readiness dépend-elle du lag de réplication ? Bien. Mais le seuil est-il sensé pour votre appli ?
- La liveness dépend-elle du lag de réplication ? Mauvais. Changez-le.
- La probe invoque-t-elle le bon client ? Les images MariaDB et MySQL ne sont pas toujours symétriques.
Quatrième étape : vérifier l’arrêt et la politique de redémarrage
- terminationGracePeriodSeconds est-elle suffisante ?
- preStop hook pour arrêter d’accepter du trafic avant SIGTERM ?
- startupProbe existe-t-elle et est-elle réglée pour le pire cas de récupération ?
Trois mini-histoires du monde corporate (toutes trop réelles)
Mini-histoire 1 : L’incident causé par une mauvaise supposition
Une entreprise SaaS de taille moyenne a migré des VM vers Kubernetes par phases. Les premiers services migrés étaient sans état, et tout s’est bien passé. La confiance a augmenté. Puis ils ont migré leur « simple réplique MySQL » utilisée pour le reporting. Le plan : copier la configuration VM, la mettre dans un StatefulSet et ajouter une readiness probe qui vérifiait le lag de réplication pour protéger les tableaux de bord des données périmées.
La mauvaise supposition : « Si la réplication est en retard, le pod est unhealthy. » La probe de readiness a été implémentée par erreur comme une liveness — même script, copié au mauvais endroit. Ça fonctionnait en staging car le jeu de données était petit et le lag dépassait rarement quelques secondes.
En production, il y avait un pic quotidien d’écritures et un job analytics qui créait des pics IO périodiques. Pendant le pic, le lag a augmenté. La liveness a échoué. Kubernetes a redémarré la réplique. MySQL a démarré la récupération après crash, plus lente sous la même pression IO. La liveness a de nouveau échoué. Boucle de redémarrages. Les dashboards ont cassé, puis l’application a commencé à échouer car elle utilisait silencieusement cette réplique pour certains chemins de lecture.
La correction fut ennuyeuse : déplacer les vérifs de lag vers readiness seulement, ajouter un startupProbe avec un budget généreux, et arrêter de router des lectures critiques vers une réplique sans tolérance explicite de staleness. Le postmortem a appris une leçon inconfortable : la « réplique reporting » n’a jamais été vraiment isolée. Elle était juste isolée socialement.
Mini-histoire 2 : L’optimisation qui s’est retournée contre eux
Une grande entreprise voulait un débit d’écriture plus rapide sur MariaDB dans Kubernetes. Ils utilisaient des réglages durables et se plaignaient de latence de commit pendant les heures de pointe. Quelqu’un a proposé de relaxer la durabilité : innodb_flush_log_at_trx_commit=2 et sync_binlog=0, parce que « le stockage est redondant de toute façon ».
Le débit s’est amélioré. Les graphiques de latence étaient meilleurs. On a fêté et oublié. Quelques semaines plus tard, un nœud est tombé pendant une forte période d’écriture. Kubernetes a reschedulé le pod, MariaDB a démarré, la récupération a été rapide — parce qu’il y avait moins d’état durable à réconcilier.
Puis la partie silencieuse : un ensemble de transactions récentes avait disparu. Pas corrompu. Manquant. L’application agissait de façon incohérente parce que certains événements métiers n’avaient jamais eu lieu. Les logs ne hurlaient pas. Ils chuchotaient. L’entreprise a passé des jours à réconcilier les données via des flux d’événements en amont et des tickets support client. C’était le pire type de panne : la base était « saine », mais le métier ne l’était pas.
La politique finale : si vous relaxez la durabilité, documentez la fenêtre exacte de perte de données acceptée et mettez en place des contrôles compensatoires (écritures idempotentes, event sourcing, jobs de réconciliation). Sinon, gardez 1/1 et payez pour du vrai stockage. L’optimisation « a marché » jusqu’à devenir un problème d’audit financier.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une fintech faisait tourner MySQL sur Kubernetes avec une discipline stricte : chaque StatefulSet avait un startupProbe réglé sur le pire cas de récupération, une termination grace suffisante pour un arrêt propre, et un preStop hook qui marquait le pod NotReady avant SIGTERM. Ils faisaient aussi des tests périodiques de restauration depuis les backups dans un namespace scratch. Personne n’aimait le faire. Ce n’était pas glamour.
Un après-midi, une mise à jour de cluster de routine a drainé des nœuds plus vite que prévu à cause d’un budget de disruption mal configuré ailleurs. Plusieurs pods MySQL ont été terminés sous charge. Certains ont dû faire une récupération après crash au redémarrage. Ce n’était pas joli, mais c’était contrôlé : les pods sont passés NotReady avant termination, le trafic a été évacué, et la période de grâce a permis à la plupart des instances de s’arrêter proprement.
Le vrai sauvetage : une réplique est revenue avec un répertoire de données cassé à cause d’un bug du backend de stockage non lié. Au lieu d’improviser, l’astreinte a suivi le runbook qu’elle avait pratiqué : cordonner le nœud affecté, détacher le volume, provisionner un PVC neuf et restaurer depuis la dernière sauvegarde connue bonne. Le service a subi une dégradation mais est resté en ligne. Personne n’a eu à « juste supprimer le pod et voir ».
Leur avantage n’était pas de meilleurs ingénieurs. C’était moins de surprises. Ils avaient répété les mouvements ennuyeux jusqu’à ce qu’ils deviennent automatiques.
Erreurs courantes : symptômes → cause racine → correction
1) CrashLoopBackOff pendant la récupération
Symptômes : Pod redémarre toutes les 30–90 secondes ; les logs montrent InnoDB recovery qui redémarre sans cesse.
Cause racine : La liveness probe échoue pendant une récupération légitime ; pas de startupProbe ; timeout trop court ; pics de latence stockage.
Correction : Ajoutez startupProbe avec un failureThreshold×periodSeconds couvrant le pire cas. Augmentez le timeout de liveness. Gardez la liveness peu coûteuse (admin ping), pas une SQL lourde.
2) Pod en Running mais jamais Ready
Symptômes : STATUS Running, mais READY 0/1 ; l’appli ne voit aucun endpoint.
Cause racine : La readiness vérifie le lag de réplication avec un seuil irréaliste ; ou la probe se connecte via DNS/service qui n’est pas prêt ; ou mauvais credentials/outils dans l’image.
Correction : Utilisez le socket localhost ; assurez-vous que le binaire de probe existe ; assouplissez les critères de readiness à ce que l’appli peut tolérer ; séparez « servir des lectures » vs « servir des écritures » si nécessaire.
3) « server has gone away » aléatoire pendant les drains de nœud
Symptômes : Pics courts d’erreurs client pendant déploiement/upgrade ; logs montrent des déconnexions abruptes.
Cause racine : Pas de preStop pour retirer le pod des endpoints avant termination ; termination grace trop courte ; aucun drain de connexion implémenté.
Correction : Ajoutez un preStop pour basculer la readiness (ou dormir après avoir marqué NotReady). Augmentez la termination grace. Envisagez une couche proxy pour le draining des connexions.
4) Réplicas qui restent définitivement en retard après un redémarrage
Symptômes : Seconds_Behind_Master croît et ne revient pas ; threads IO/SQL sont en marche mais lents.
Cause racine : Débit de stockage insuffisant pour le taux d’application ; limites CPU qui throttlent ; application mono-thread de l’appli ; transactions longues sur le primaire.
Correction : Corrigez la StorageClass/IOPS d’abord ; augmentez les limites CPU ; tunez le parallélisme d’application de réplication si possible ; réduisez les transactions longues.
5) Corruption du répertoire de données après un « simple redeploy »
Symptômes : mysqld ne démarre plus ; erreurs sur tablespaces manquants ou mismatch de redo log.
Cause racine : Deux pods ont monté le même volume RWO suite à intervention manuelle ou contrôleur cassé ; ou l’image conteneur a changé les permissions du datadir ; ou des init containers ont réinitialisé par erreur.
Correction : Faites respecter l’identité StatefulSet et les claims de volume ; évitez les hacks manuels « attacher ailleurs » ; verrouillez la logique d’init pour qu’elle ne réécrase jamais un datadir existant.
6) « C’est lent » mais seulement sur Kubernetes
Symptômes : Même configuration rapide sur VM, lente sur k8s ; commits sont en pics.
Cause racine : Variance de latence du stockage réseau ; throttling CPU dû à des limites basses ; effets de noisy neighbor ; buffer pool mal dimensionné pour la limite mémoire.
Correction : Benchmarquez la StorageClass ; réservez CPU/mémoire adéquatement ; assurez-vous que le buffer pool tient dans la limite mémoire ; gardez les knobs de durabilité stables sauf si vous acceptez la perte.
Listes de contrôle / plan étape par étape
Checklist A : Conception des probes pour MySQL/MariaDB (faire avant la prod)
- Utilisez startupProbe pour protéger la récupération InnoDB (budgez pour le pire cas, pas la moyenne).
- Liveness : ping local simple sans SQL avec timeouts conservateurs.
- Readiness : connexion locale +
SELECT 1; optionnellement vérification du lag ou de wsrep readiness. - Évitez les secrets dans argv si possible ; préférez des vars d’environnement ou un fichier de config lisible uniquement par l’utilisateur mysql.
- Réglez les timeouts des probes pour votre stockage, pas pour votre optimisme.
Checklist B : Sécurité au redémarrage et correction d’arrêt
- terminationGracePeriodSeconds dimensionné pour flush/arrêt sous charge.
- preStop hook qui arrête le routage du trafic avant SIGTERM (readiness gate ou drain proxy).
- PDBs qui empêchent la perturbation simultanée de plusieurs pods critiques.
- Anti-affinité de pod pour ne pas placer primaire et réplique dans le même domaine de défaillance.
Checklist C : Posture de sécurité des données (décidez explicitement)
- Paramètres de durabilité : choisissez
innodb_flush_log_at_trx_commitetsync_binlogselon la fenêtre de perte acceptable. - StorageClass : choisissez une classe avec une latence fsync prévisible ; testez-la avec le même pattern d’accès.
- Sauvegardes : automatisées, chiffrées, avec exercices de restauration dans un namespace scratch.
- Migrations de schéma : planifiées et réversibles quand possible ; n’exécutez pas de migrations longues et bloquantes en période de pointe.
FAQ
1) Dois-je utiliser MySQL ou MariaDB sur Kubernetes ?
Si vous avez besoin d’une compatibilité maximale avec l’écosystème MySQL (et les fonctionnalités de MySQL 8), choisissez MySQL. Si vous êtes engagé envers des fonctionnalités MariaDB comme certains modèles basés sur Galera, choisissez MariaDB. Opérationnellement, les deux peuvent être exécutés en toute sécurité — mais uniquement si vous concevez correctement probes et stockage.
2) Ai-je besoin d’un opérateur ?
Si vous exécutez des écritures en production, oui — soit un opérateur mature, soit un chart interne très discipliné avec runbooks. La fiabilité stateful vient de l’automatisation des bords laids : bootstrap, basculement, sauvegardes et upgrades sûrs.
3) La liveness peut-elle vérifier le lag de réplication ?
Non. C’est une préoccupation de readiness. La liveness qui tue une réplique en retard transforme un retard temporaire en mort permanente.
4) Pourquoi le redémarrage de ma base prend-il parfois si longtemps ?
Un arrêt non propre déclenche la récupération après crash. Le temps de récupération dépend des redo à appliquer et du débit/latence IO du volume. Sur un stockage lent ou variable, cela peut varier énormément.
5) Est-il sûr d’exécuter MySQL/MariaDB sur un stockage réseau ?
Ça peut l’être si le stockage fournit une latence prévisible et des sémantiques correctes, mais la performance et la latence en queue sont souvent dégradées. Pour de l’OLTP lourd, un SSD local ou un block storage de haute qualité avec de bonnes caractéristiques fsync est généralement plus sûr.
6) Dois-je mettre innodb_flush_log_at_trx_commit=2 pour les performances ?
Seulement si le métier accepte de perdre jusqu’à ~1 seconde de transactions sur crash (et si vous comprenez aussi la durabilité du binlog). Si vous ne pouvez pas tolérer cela, ne « optimisez » pas. Corrigez le stockage et l’isolation des ressources à la place.
7) Comment la readiness devrait-elle se comporter pour les nœuds Galera (MariaDB) ?
Gatez la readiness sur l’état wsrep : cluster status Primary, local state Synced, et wsrep_ready=ON. Sinon, vous routerez du trafic vers des nœuds en joining ou donor et obtiendrez des comportements clients bizarres.
8) Quelle est la cause numéro 1 de perte de données sur des bases Kubernetes ?
Des décisions humaines déguisées en valeurs par défaut : durabilité relâchée sans acceptation explicite, suppression de StatefulSets avec politique de reclaim « Delete », ou discipline de sauvegarde/restauration cassée. Kubernetes supprime rarement vos données par accident ; les humains le font.
9) Les probes créent-elles de la charge sur la base ?
Oui. Si vous probez trop fréquemment, exécutez des requêtes lourdes ou utilisez trop de connexions healthcheck concurrentes, vous pouvez provoquer la latence qui ferait échouer la probe. Gardez les checks peu coûteux et limités en taux.
10) Quelle est l’approche la plus simple et sûre pour les probes ?
StartupProbe : généreux mysqladmin ping. Liveness : même ping avec timeout et period conservateurs. Readiness : connexion locale + SELECT 1, plus gating optionnel de réplication/wsrep.
Prochaines étapes que vous pouvez faire cette semaine
Si vous exécutez actuellement MySQL ou MariaDB sur Kubernetes, voici une séquence pratique qui améliore la sécurité sans exiger une migration ou un nouvel opérateur demain :
- Ajoutez startupProbe à chaque pod de base, calibrée pour le pire cas de récupération sur votre StorageClass.
- Auditez les liveness probes et retirez tout ce qui vérifie la santé logique (lag de réplication, wsrep, SQL long).
- Augmentez la termination grace et ajoutez un preStop hook pour arrêter le routage du trafic avant la terminaison.
- Mesurez la latence tail du stockage pendant les pics et corrélez avec les échecs de probes et la latence de commit ; si c’est instable, corrigez le stockage avant de tuner MySQL.
- Faites un test de restauration dans un namespace scratch. Si c’est pénible, ce n’est pas une sauvegarde ; c’est un vœu.
- Rédigez votre posture de durabilité (1/1 vs relâchée), avec l’acceptation explicite du responsable métier si vous choisissez un risque de perte.
L’objectif n’est pas « ne jamais redémarrer ». L’objectif est « les redémarrages sont supportables, prévisibles et ennuyeux ». Kubernetes continuera à réessayer. Assurez-vous qu’il réessaie quelque chose de sûr.