Tout va bien jusqu’à ce que ça ne va plus. Votre site démarre sur SQLite, c’est rapide, le déploiement est sans friction et la base de données est littéralement un fichier. Puis le trafic monte, des jobs en arrière-plan apparaissent, l’analytics est ajouté, et soudain vous fixez database is locked comme si c’était un trait de personnalité.
C’est la ligne entre « SQLite est parfait » et « SQLite me gâche la journée ». L’objectif ici n’est pas de démolir SQLite — c’est de délimiter clairement où il brille, où il casse, et quoi vérifier avant de migrer en panique vers MySQL (ou, pire, avant d’essayer de « corriger » SQLite pour en faire MySQL).
La version courte : choisissez l’outil comme un adulte
Utilisez SQLite lorsque vous voulez une application mono‑nœud avec des opérations simples, une concurrence d’écritures modeste et un fort désir d’éviter d’exécuter un serveur de base de données. C’est excellent pour les prototypes, les outils internes, les appareils edge et beaucoup de sites en production qui sont majoritairement en lecture avec des écritures contrôlées.
Utilisez MySQL lorsque vous avez besoin d’une concurrence prévisible sous charge, de plusieurs serveurs applicatifs écrivant en même temps, de primitives de réplication/basculement, d’habitudes d’évolution de schéma en ligne, et de réglages opérationnels qui vous aident à récupérer d’erreurs sans arrêt de service.
Si vous ne pouvez pas décrire votre concurrence d’écritures, votre objectif de latence p95, votre RTO de sauvegarde/restauration et l’endroit physique où se trouve votre fichier de base de données, vous ne choisissez pas une base de données — vous choisissez un incident futur.
Ce que vous choisissez vraiment : une architecture, pas la syntaxe
SQLite est une bibliothèque qui écrit un fichier ; MySQL est un service qui parle sur le réseau
SQLite s’exécute en processus. Votre application appelle une bibliothèque, et cette bibliothèque lit/écrit un fichier de base de données sur le stockage local. Il n’y a pas de démon de base de données séparé acceptant des connexions, gérant la mémoire entre clients ou coordonnant l’accès distant. Ce n’est pas un inconvénient ; c’est la caractéristique.
MySQL s’exécute hors processus. Il possède les fichiers de données, le buffer pool, les journaux redo/undo, les threads d’arrière-plan et la réplication. Votre appli se connecte via TCP (ou un socket local), et MySQL arbitre la concurrence entre de nombreux clients.
Le vrai compromis : simplicité opérationnelle vs concurrence et réalité multi‑nœuds
La simplicité de SQLite est un multiplicateur de force jusqu’à ce que vous la dépassiez. Un fichier. Sauvegardes faciles (en majorité). Pas de prolifération d’identifiants. Pas de pools de connexions à régler. Si vous construisez un site où les écritures sont rares et contrôlées, SQLite reste ennuyeux dans le meilleur sens.
Mais dès que vous avez plusieurs écrivains depuis plusieurs processus (ou plusieurs hôtes), vous êtes passé du « choix de base » au « verrouillage distribué et aux sémantiques de défaillance ». SQLite peut encore fonctionner, mais vous devez respecter son modèle de concurrence et la physique du stockage en dessous.
Un principe de fiabilité à afficher au mur
Werner Vogels, dans le monde de la fiabilité AWS, a une phrase souvent paraphrasée : Tout échoue tout le temps ; concevez pour que le système continue de fonctionner quand même
(idée paraphrasée, Werner Vogels). SQLite et MySQL échouent tous les deux. Ils échouent différemment. Choisissez la défaillance que vous pouvez survivre.
Faits intéressants et historique utiles en réunion
- SQLite a été créé en 2000 par D. Richard Hipp pour un contrat de la marine américaine — la fiabilité embarquée était le but, pas le glamour web‑scale.
- SQLite est célèbre pour être « sans serveur » — pas « serverless cloud », mais « il n’y a pas de processus serveur ». C’est une bibliothèque.
- SQLite est présent dans plus d’appareils que vous ne pouvez compter : téléphones, navigateurs, applications desktop, systèmes d’info‑divertissement. Cette diffusion le rend conservateur et stable.
- Le mode WAL (Write‑Ahead Logging) est arrivé en 2010 et a changé la donne de la concurrence de SQLite pour les charges majoritairement en lecture.
- Les bases SQLite sont des fichiers uniques (plus des fichiers WAL et shm optionnels), ce qui facilite l’expédition et les snapshots — mais rend aussi la décision « mettre ça sur NFS » une tragédie récurrente.
- InnoDB est devenu le moteur par défaut à partir de MySQL 5.5, et ce changement a compté : transactions, récupération après crash, verrous au niveau ligne et des réglages par défaut sensés sont devenus l’expérience normale.
- La réplication MySQL est un motif central depuis des décennies — pas parfaite, mais opérationnellement comprise par un énorme écosystème (et la plupart des outils l’attendent).
- SQLite a une culture de tests étonnamment stricte : couverture de tests énorme, fuzzing et attentes de stabilité à long terme parce qu’il est embarqué partout.
- MySQL a une histoire de « footgun » autour de défauts non déterministes et de dérive de configuration — moins vrai aujourd’hui, mais le folklore existe pour une raison.
Jusqu’où SQLite peut aller (plus loin que votre équipe ne le pense)
SQLite est extrêmement rapide quand la charge correspond
SQLite peut être ridiculement rapide pour les lectures parce qu’il n’y a pas de saut réseau et que le moteur de requête se trouve dans le même processus que votre code. Pour une application mono‑nœud avec un cache de pages chaud, vous pouvez obtenir une faible latence avec moins d’éléments mobiles. Si votre application lit majoritairement un jeu de données modeste et effectue des écritures occasionnelles, SQLite n’est pas un compromis. C’est une solution propre.
Il gère aussi les transactions ACID, les clés étrangères (si activées), les index et un planificateur de requêtes correct. Ce n’est pas une base de jouet. Le problème est que les gens la traitent comme un jouet jusqu’à la mise en production, puis la traitent comme une base distribuée alors qu’elle ne l’est pas.
Où SQLite est un excellent choix par défaut
- Déploiement sur une VM unique / un conteneur unique avec un processus applicatif principal (ou un petit nombre de processus) et des écritures contrôlées.
- Charges majoritairement en lecture avec des écritures par lots périodiques, surtout avec WAL activé.
- Applications embarquées, desktop, edge et offline‑first où exécuter MySQL serait un surcoût absurde.
- Outils internes où vous voulez un maximum de « ça marche » et un minimum de « appelez le DBA ».
- Prototypes qui peuvent devenir réels, à condition de construire la discipline de migration et de sauvegarde tôt.
SQLite peut gérer du trafic réel — si vous canalisez les écritures
Si vous pouvez canaliser les écritures via un seul worker (ou un petit nombre de workers coordonnés) et garder les transactions courtes, SQLite peut servir une quantité surprenante de trafic en lecture. Le mode WAL permet aux lecteurs de continuer pendant qu’un écrivain ajoute au journal. C’est important.
Mais vous devez cesser de penser en « requêtes par seconde » et commencer à penser en « contentions d’écriture par seconde ». Dix mille requêtes en lecture peuvent aller bien. Dix requêtes d’écriture qui se percutent peuvent enflammer votre p95.
Blague #1 : SQLite est comme une bibliothécaire très compétente — silencieuse, rapide, organisée. Mais elle ne tamponne qu’un seul bon de prêt à la fois.
Où SQLite casse : modes de défaillance qui font mal en production
1) Concurrence : le verrou d’écriture est la vedette
SQLite autorise plusieurs lecteurs, mais la concurrence d’écritures est limitée. En mode journal de rollback, un écrivain bloque les lecteurs au moment du commit. En mode WAL, les lecteurs ne bloquent pas les écrivains et les écrivains ne bloquent pas les lecteurs — mais il y a toujours effectivement un seul écrivain à la fois.
Le symptôme classique est des requêtes qui échouent ou se bloquent lors de pics d’écritures. Vous verrez SQLITE_BUSY, « database is locked », ou une latence accrue qui ressemble à des pics aléatoires. Ce n’est pas aléatoire. C’est de la contention.
Certaines équipes essaient de « résoudre » ça avec des timeouts busy plus longs. Cela peut réduire les taux d’erreur, et cela peut aussi transformer un petit problème de contention en un incident de latence généralisé. Félicitations, vous avez échangé une erreur 500 contre un chargement de page de 30 secondes.
2) Sémantiques du stockage : disque local vs système de fichiers réseau
SQLite s’appuie sur des verrous de fichier et un comportement de système de fichiers prévisible. Sur un système de fichiers local (ext4, xfs, APFS, NTFS), c’est en grande partie OK. Sur un système de fichiers réseau, ça peut devenir un potage de performance ou un risque d’exactitude selon les sémantiques de verrouillage et le comportement de cache.
Mettre SQLite sur NFS parce que « nous voulons un stockage partagé pour plusieurs serveurs applicatifs » est un mouvement courant juste avant que la rotation on‑call devienne intéressante. Si vous avez besoin d’écrivains multi‑hôtes, il vous faut typiquement une base serveur, ou il vous faut implémenter un vrai modèle à écrivain unique avec une file et un service écrivain dédié.
3) Défaillance opérationnelle : des sauvegardes qui sont « une copie du fichier » (jusqu’à ce qu’elles ne le soient pas)
Les sauvegardes SQLite peuvent être simples : vous pouvez copier le fichier de base de données quand il est cohérent. Le problème, c’est que les gens le font quand il n’est pas cohérent, ou qu’ils ne copient que le fichier principal et oublient le WAL. Ou ils copient pendant une charge d’écritures intense sans utiliser les bons primitives.
MySQL a des outils structurés et une norme culturelle autour des sauvegardes. SQLite vous demande d’être prudent. Beaucoup d’organisations interprètent « soyez prudent » comme « on le fera plus tard ».
4) Changements de schéma : de petites apps deviennent grandes, et ALTER TABLE devient politique
SQLite supporte de nombreux changements de schéma, mais pas tous sont en ligne comme les gens s’y attendent. Certaines opérations nécessitent la reconstruction d’une table, ce qui peut verrouiller ou bloquer un site occupé. Si vous faites des migrations fréquentes sur un fichier de base de données chaud, planifiez‑les explicitement.
5) Observabilité : moins de boutons, moins de compteurs, plus de conjectures
Avec MySQL vous obtenez un processus avec des métriques : hit rate du buffer pool, pression sur le journal redo, attentes de verrou, retard de réplication, slow query logs, performance schema. Avec SQLite, vous instrumentez souvent depuis le côté application. Vous pouvez bien le faire, mais il faut le faire intentionnellement.
6) « On ajoutera juste un autre serveur applicatif » ne fonctionne pas de la même façon
SQLite scale verticalement sur un nœud : CPU plus rapide, SSD plus rapide, plus de RAM, PRAGMA mieux réglés, requêtes optimisées. Il ne scale pas horizontalement par défaut. Si votre prochaine étape est d’ajouter des nœuds derrière un load balancer, MySQL (ou une autre base serveur) devient le choix évident.
Ce que MySQL vous apporte (et ce que ça coûte)
MySQL vous apporte : une concurrence multi‑client prévisible
MySQL avec InnoDB vous donne des verrous au niveau ligne, plusieurs écrivains concurrents et des sémantiques d’isolation plus faciles à raisonner à grande échelle. Vous aurez toujours des contentions de verrous, mais c’est une bête différente : vous pouvez la voir, l’analyser et y remédier avec des index, la conception des requêtes et la discipline transactionnelle.
MySQL vous apporte : réplication et basculement comme compétences de base
Même si vous n’exécutez jamais une topologie sophistiquée, avoir un réplica pour les sauvegardes, les requêtes de reporting et la reprise après sinistre est un motif opérationnel mature. Cela vous donne aussi des options quand le primaire est malheureux : drainer le trafic, promouvoir, ou au moins lire depuis un réplica pendant que vous réparez.
MySQL coûte : surcharge opérationnelle et coins aigus
Vous exécutez maintenant un service à état avec configuration, mises à jour, posture de sécurité, monitoring, disposition des disques et planification de capacité. Vous avez besoin d’un pool de connexions. Vous avez besoin de vérification de sauvegarde. Vous devez penser aux changements de schéma et aux transactions longues. Vous adoptez un système qui peut bien vous servir, mais il exigera de la compétence.
Blague #2 : MySQL, c’est comme adopter un chien — loyal, capable et protecteur. Mais vous êtes maintenant responsable de son régime, de ses sautes d’humeur et de ses aboiements à 3 h du matin.
Playbook de diagnostic rapide : trouver le goulot vite
Ceci est la checklist « je suis on‑call et le site est lent ». La façon la plus rapide de perdre du temps est de débattre des bases de données philosophiquement pendant que votre p95 hurle.
Premier : décidez si vous avez de la contention, de l’I/O ou une pathologie de requête
- Vérifiez les motifs d’erreurs : voyez‑vous
SQLITE_BUSY/ « database locked » ou des attentes de verrou MySQL ? - Vérifiez la forme de la latence : lenteur constante (lié à l’I/O) vs pics de blocage (contention) vs « certains endpoints terribles » (problème de requête/index).
- Vérifiez le taux d’écritures : un déploiement a‑t‑il ajouté des écritures, des jobs en arrière‑plan, des événements analytics, des écritures de session ou des migrations ?
Deuxième : identifiez où le temps est passé
- Temps au niveau application : temps passé dans les appels DB vs temps ailleurs (rendu, API externes).
- Signaux au niveau DB :
- SQLite : comportement WAL/verrouillage, transactions longues, tables chaudes, problèmes vacuum/auto_vacuum.
- MySQL : slow query log, statut InnoDB, attentes de verrou, pression sur le buffer pool, retard de réplication.
- Niveau hôte — I/O : le disque est‑il saturé ou le système de fichiers sous tension ?
Troisième : choisissez l’atténuation à moindre risque
- Si SQLite est verrouillé : réduisez la concurrence d’écritures, raccourcissez les transactions, activez WAL (si sûr), regroupez les écritures, déplacez les écritures vers un worker unique.
- Si MySQL est verrouillé : ajoutez/ajustez des index, réduisez la portée des transactions, tuez les requêtes hors de contrôle, ajustez l’isolation si approprié.
- Si I/O bound : ajoutez de la RAM (pour le cache), passez à un SSD/NVMe plus rapide, réduisez la pression fsync, réglez les journaux redo MySQL, ajustez prudemment le mode synchronous de SQLite.
Tâches pratiques : commandes, sorties et décisions (12+)
Ce sont des tâches réelles que vous pouvez exécuter sur des hôtes Linux et des déploiements MySQL/SQLite courants. Chacune inclut ce que signifie la sortie et quelle décision prendre ensuite.
Task 1: Confirm where the SQLite database lives and what filesystem it uses
cr0x@server:~$ df -T /var/www/app/db/app.sqlite3
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 ext4 192152472 81324512 101245120 45% /
Sens : C’est sur ext4 local, ce qui est une bonne nouvelle. Si vous voyez nfs ou une surcouche FUSE avec des sémantiques étranges, considérez verrouillage et latence comme suspects par défaut.
Décision : Si c’est sur NFS et que vous avez plusieurs écrivains, stoppez et redesign : passez à MySQL/Postgres, ou imposez un service écrivain unique.
Task 2: Check SQLite file companions (WAL/shm) and size growth
cr0x@server:~$ ls -lh /var/www/app/db/
total 2.3G
-rw-r----- 1 www-data www-data 1.7G Dec 30 10:12 app.sqlite3
-rw-r----- 1 www-data www-data 512M Dec 30 10:12 app.sqlite3-wal
-rw-r----- 1 www-data www-data 32K Dec 30 10:12 app.sqlite3-shm
Sens : Le mode WAL est en place (vous avez un fichier -wal). Un gros fichier WAL peut signifier que les checkpoints ne s’effectuent pas, que l’application a des lecteurs long‑cours, ou que le checkpointing est mal configuré.
Décision : Si le WAL grossit sans fin, investiguez les transactions/lectures longues et la stratégie de checkpoint ; si vous ne pouvez pas le contrôler, MySQL devient attractif.
Task 3: Verify SQLite journal mode and synchronous settings
cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 'PRAGMA journal_mode; PRAGMA synchronous;'
wal
2
Sens : wal est activé. synchronous=2 signifie FULL. C’est plus sûr mais peut augmenter la latence fsync.
Décision : Si vous êtes lié par l’I/O et pouvez tolérer un certain risque de durabilité, vous pourriez envisager synchronous=NORMAL — mais faites‑le seulement avec un modèle de défaillance clair et une récupération testée.
Task 4: Identify “database is locked” events in application logs
cr0x@server:~$ grep -R "database is locked" -n /var/log/app/ | tail -n 5
/var/log/app/app.log:44182 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44190 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44201 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44222 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44228 sqlite error: database is locked (SQLITE_BUSY)
Sens : Ce ne sont pas des « glitches » aléatoires. C’est de la pression de concurrence ou des transactions longues.
Décision : Si cela corrèle avec des endpoints spécifiques ou des cron jobs, isolez les écrivains. Ajoutez une file, regroupez les écritures, ou migrez cette charge vers MySQL.
Task 5: Find long-running transactions holding SQLite back (via app process inspection)
cr0x@server:~$ ps -eo pid,etimes,cmd | grep -E "gunicorn|uwsgi|node|python" | head
2143 8123 /usr/bin/python3 /var/www/app/worker.py
2190 4201 /usr/bin/python3 /var/www/app/web.py
2211 233 /usr/bin/python3 /var/www/app/web.py
Sens : Les processus worker de longue durée gardent souvent des connexions DB ouvertes et peuvent maintenir des transactions de lecture ouvertes involontairement.
Décision : Auditez la portée des connexions/transactions. Assurez‑vous que chaque requête/job utilise des transactions courtes et ferme les curseurs rapidement. Si votre ORM ouvre des transactions implicites, forcez autocommit si c’est sûr.
Task 6: Check disk latency and saturation during spikes
cr0x@server:~$ iostat -xz 1 5
Linux 6.5.0 (server) 12/30/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
6.12 0.00 2.01 8.42 0.00 83.45
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 85.0 2048.0 0.0 0.0 3.20 24.10 210.0 8192.0 0.0 0.0 18.70 39.01 4.12 92.5
Sens : %util proche de 90% et w_await ~19ms suggèrent que le disque est un goulot pour les écritures. SQLite en synchronous FULL le ressentira.
Décision : Si le disque est saturé, réduisez la fréquence des fsync (prudemment), regroupez les écritures, déplacez la DB sur un stockage plus rapide, ou migrez vers MySQL avec meilleur buffering et réglage des journaux.
Task 7: Check open file locks behavior (useful when NFS is involved)
cr0x@server:~$ lsof /var/www/app/db/app.sqlite3 | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 2190 www-data 7u REG 259,2 1825368064 393226 /var/www/app/db/app.sqlite3
python3 2211 www-data 7u REG 259,2 1825368064 393226 /var/www/app/db/app.sqlite3
Sens : Plusieurs processus ont le fichier ouvert. C’est normal, mais si ces processus sont sur différents hôtes et que le fichier est sur un stockage partagé, vous jouez avec les sémantiques de verrouillage.
Décision : Si c’est un accès multi‑hôtes, arrêtez. Centralisez les écritures ou migrez vers une base serveur.
Task 8: Validate SQLite integrity after a crash or suspicious I/O event
cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 'PRAGMA integrity_check;'
ok
Sens : « ok » signifie que les structures internes sont cohérentes.
Décision : Si ce n’est pas ok, restaurez depuis des sauvegardes immédiatement et investiguez le stockage sous‑jacent et le comportement des processus au crash.
Task 9: Observe SQLite query plan for a known slow query
cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 "EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id=42 ORDER BY created_at DESC LIMIT 20;"
QUERY PLAN
`--SCAN orders
`--USE TEMP B-TREE FOR ORDER BY
Sens : Il scanne la table et trie avec un b‑tree temporaire. C’est coûteux.
Décision : Ajoutez un index comme (user_id, created_at). La plupart des rapports « SQLite est lent » sont en fait des « vous avez oublié un index ». Même chose pour MySQL, d’ailleurs.
Task 10: Check MySQL for slow queries (if you already migrated or you’re evaluating)
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';"
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| long_query_time | 1.000 |
+-----------------+-------+
Sens : Le slow query log est activé ; les requêtes > 1s sont journalisées.
Décision : Si vous ne l’avez pas activé en production, vous choisissez l’aveuglement. Activez‑le avec un seuil sensé et faites pivoter les logs.
Task 11: Inspect MySQL lock contention and active transactions
cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST;"
+----+------+-----------+------+---------+------+------------------------+-------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+------+---------+------+------------------------+-------------------------------+
| 17 | app | 10.0.1.12 | app | Query | 12 | Waiting for table lock | UPDATE sessions SET ... |
| 23 | app | 10.0.1.13 | app | Query | 0 | Sending data | SELECT * FROM orders WHERE... |
+----+------+-----------+------+---------+------+------------------------+-------------------------------+
Sens : Une requête attend un verrou de table depuis 12 secondes. Ce n’est pas une bonne ambiance.
Décision : Trouvez le bloqueur (souvent une migration ou une transaction longue). Corrigez la portée des transactions, utilisez des patterns de changement de schéma en ligne et ajoutez des index pour éviter les scans bloquants.
Task 12: InnoDB health snapshot for MySQL (the “tell me what hurts” command)
cr0x@server:~$ sudo mysql -e "SHOW ENGINE INNODB STATUS\G" | head -n 40
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-30 10:20:11 0x7f3b6c1fe700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 10 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 1120 srv_active, 0 srv_shutdown, 332 srv_idle
srv_master_thread log flush and writes: 1452
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 1021
OS WAIT ARRAY INFO: signal count 1004
Mutex spin waits 0, rounds 0, OS waits 0
RW-shared spins 112, OS waits 19
RW-excl spins 88, OS waits 25
Sens : Cette sortie peut exposer attentes de verrous, pression du buffer pool et problèmes de flush de log. Même un coup d’œil rapide montre si le moteur attend de l’I/O ou des verrous.
Décision : Si vous voyez de lourdes attentes de log ou des misses du buffer pool, réglez MySQL et le stockage. Si vous voyez des attentes de verrou, optimisez requêtes/transactions/schéma.
Task 13: Measure MySQL buffer pool hit rate directionally
cr0x@server:~$ sudo mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name | Value |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests | 982345678 |
| Innodb_buffer_pool_reads | 1234567 |
+---------------------------------------+------------+
Sens : Reads vs read_requests indique l’efficacité du cache. Ici, les lectures physiques sont une petite fraction, ce qui est bon.
Décision : Si les lectures physiques augmentent, ajoutez de la RAM, réglez la taille du buffer pool, ou corrigez des requêtes/index qui provoquent de gros scans.
Task 14: Confirm MySQL replication lag (if you rely on replicas)
cr0x@server:~$ sudo mysql -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: 27
Sens : 27 secondes de retard. C’est suffisant pour casser les hypothèses « read‑your‑writes ».
Décision : Si votre appli lit depuis des replicas, vous avez besoin de règles de consistance (stickiness, routage read‑after‑write) ou de réduire le lag via tuning et séparation des charges.
Task 15: Check that SQLite is not silently using multiple threads in a way you didn’t expect
cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 'PRAGMA compile_options;' | grep -E 'THREADSAFE|OMIT_WAL' | head
THREADSAFE=1
Sens : SQLite est compilé threadsafe (bien). Mais la sécurité thread n’est pas équivalente à une montée en charge par concurrence ; vous avez toujours la contrainte de l’écrivain unique.
Décision : Si votre appli suppose « threads = débit », redesignez les chemins d’écriture avant de blâmer SQLite.
Trois mini-histoires d’entreprise issues du terrain
1) Incident causé par une mauvaise hypothèse : « C’est un fichier, donc le stockage partagé résout la montée en charge »
Une entreprise de taille moyenne avait une appli web née sur une VM unique avec SQLite. Ça allait : quelques écritures par seconde, surtout des lectures, et un déploiement simple. Puis ils ont ajouté un second serveur web derrière un load balancer. L’idée était polie : plus de capacité, plus de redondance. La base de données est restée sous forme de fichier sur un système de fichiers réseau partagé parce que « les deux serveurs doivent y accéder ».
Le premier jour, tout semblait OK. Le trafic était léger et le système de fichiers partagé suffisait dans le cas heureux. Puis une campagne marketing a frappé. Le trafic d’écriture a augmenté : sessions, suivi d’événements et une petite table « last seen » mise à jour presque à chaque requête. Soudain l’appli a commencé à retourner des erreurs intermittentes. Le pire : les erreurs n’étaient pas constantes. Certaines requêtes étaient rapides ; d’autres se bloquaient et expiraient.
L’ingénieur on‑call a vu SQLITE_BUSY et a augmenté le busy timeout. Les erreurs ont chuté. La latence a doublé. Maintenant le load balancer a commencé à marquer des hôtes comme non‑sains parce que les requêtes prenaient trop de temps, ce qui a concentré le trafic sur moins de nœuds, augmentant la contention de verrou. C’est ainsi que vous transformez un petit problème de concurrence en une défaillance en cascade avec sérieux visage.
Après l’incident, la cause racine était double : le comportement de verrouillage de SQLite sur systèmes de fichiers en réseau n’était pas ce qu’ils supposaient, et la concurrence d’écriture multi‑hôtes n’avait jamais été un plan d’échelle supporté. La correction fut ennuyeuse et correcte : déplacer la base de données vers une instance MySQL et la traiter comme un service autoritaire unique, puis refactorer les écritures de session/événement pour qu’elles soient moins bavardes.
La leçon n’était pas « SQLite est mauvais ». La leçon était « le stockage partagé n’est pas un cluster de base de données », et les sémantiques de verrouillage de fichier ne sont pas une stratégie d’échelle.
2) Optimisation qui a eu l’effet inverse : « Désactivez la durabilité, c’est plus rapide »
Une équipe SaaS exécutait SQLite sur une grosse VM pour un tableau de bord interne à faible latence. Les écritures étaient plus fréquentes qu’ils ne l’aimaient, et les pics de latence disque étaient visibles. Quelqu’un a suggéré de changer PRAGMA synchronous vers un réglage moins strict pour réduire le coût des fsync. Les benchmarks étaient excellents. Les graphiques semblaient super. Tout le monde a apprécié l’illusion de victoire.
Deux semaines plus tard, l’hôte a rebooté de façon imprévue lors d’une mise à jour du noyau. L’appli est revenue rapidement. Puis les utilisateurs ont commencé à signaler des changements récents manquants. Pas des structures corrompues — pire. Des données correctes mais manquantes pour les derniers lots d’écritures qui avaient été « commit » du point de vue de l’application.
L’équipe avait accidentellement changé le contrat de durabilité sans changer les attentes produit. Les utilisateurs supposaient que dès que l’UI confirmait une mise à jour, elle survivrait à un crash. En pratique, ces mises à jour vivaient dans des buffers volatils et n’avaient jamais atteint le stockage stable.
Le nettoyage fut douloureux : reconstruire des données à partir des logs applicatifs, réconcilier les rapports utilisateurs et restaurer la confiance. Ils ont rétabli des réglages plus sûrs, ajouté des messages explicites côté utilisateur quand une durabilité éventuelle était acceptable, et introduit un backend MySQL pour les données les plus critiques.
Une optimisation qui change la correction n’est pas une optimisation. C’est une décision produit. Si vous la prenez accidentellement, la production vous fera remplir la paperasse.
3) Pratique ennuyeuse mais correcte qui a sauvé la journée : tests de restauration et discipline « un seul écrivain »
Une autre organisation a fait tourner un site de contenu sur SQLite pendant des années. Oui, des années. Ils avaient un nœud unique gérant le fichier de base, et ils étaient stricts sur la discipline d’écriture : un seul worker en arrière‑plan réalisait les écritures, et la couche web était majoritairement en lecture, avec des mises à jour en file.
Leur habitude opérationnelle la plus importante n’était pas un réglage exotique. C’était le test de restauration. Chaque semaine, un job tirait la dernière sauvegarde, la restaurait sur un hôte de staging, exécutait PRAGMA integrity_check et lançait une petite suite de requêtes applicatives. Si la restauration échouait, quelqu’un était paginé. C’était normal, pas héroïque.
Un jour, un déploiement a introduit un bug de migration qui a gonflé la taille de la base de données et poussé le disque presque plein. Le site a commencé à ralentir, puis les écritures ont commencé à échouer. Ils ont rollbacké le déploiement, mais le fichier de base avait déjà grossi. La pression disque a persisté.
Ils ont restauré depuis la dernière sauvegarde saine sur un système de fichiers neuf avec de l’espace, rejoué un petit ensemble d’écritures en file, et étaient de retour en ligne rapidement. Pas de drame de données. Pas de devinettes. La pratique ennuyeuse — restaurations vérifiées — a transformé une mauvaise journée en incident contenu.
SQLite ne les a pas sauvés. La discipline l’a fait. SQLite ne s’est simplement pas mis en travers du chemin.
Erreurs courantes : symptôme → cause racine → fix
- Symptôme : pics « database is locked » pendant les rafales de trafic
- Cause racine : Trop d’écrivains concurrents, transactions longues, ou un job en arrière‑plan percutant les écritures des requêtes.
- Fix : Activez WAL si approprié ; raccourcissez la portée des transactions ; déplacez les écritures dans un worker à file ; réduisez la fréquence d’écriture (debounce des mises à jour de session).
- Symptôme : SQLite est rapide localement mais lent en production
- Cause racine : Le stockage de production a une latence fsync plus élevée ; synchronous FULL amplifie cela ; le checkpoint WAL peut se bloquer.
- Fix : Mesurez la latence disque (
iostat) ; placez la DB sur SSD local ; réglez le checkpointing ; envisagez MySQL si la latence d’écriture doit être stable. - Symptôme : timeouts aléatoires, surtout pendant les sauvegardes
- Cause racine : copie naïve de fichier pendant des écritures actives ; interaction checkpoint/verrouillage ; lecteurs long‑cours empêchant le checkpoint.
- Fix : Utilisez l’API de sauvegarde en ligne SQLite via des outils ; assurez‑vous que les sauvegardes incluent l’état WAL ; planifiez les sauvegardes avec throttling d’écriture.
- Symptôme : le fichier WAL grossit énormément et reste volumineux
- Cause racine : Les checkpoints ne peuvent pas se compléter à cause de lecteurs long‑cours ou autocheckpoint mal configuré ; l’application garde des transactions de lecture ouvertes.
- Fix : Assurez‑vous de connexions/transactions courtes ; configurez autocheckpoint ; effectuez des checkpoints explicites en période de faible trafic ; révisez le comportement de l’ORM.
- Symptôme : Après avoir ajouté un second serveur applicatif, la performance s’effondre
- Cause racine : SQLite sur stockage partagé avec plusieurs écrivains ; contention de verrou entre processus/hôtes ; sémantiques du système de fichiers.
- Fix : Ne le faites pas. Centralisez les écritures, ou migrez vers MySQL. Si vous avez besoin de plusieurs serveurs applicatifs, une base serveur est la réponse normale.
- Symptôme : MySQL est « lent » après migration, pire que SQLite
- Cause racine : Pas d’index, pas de pool de connexions, mauvaises hypothèses d’isolation, ou schéma conçu pour des accès basés fichier.
- Fix : Activez le slow query log ; ajoutez des index basés sur les motifs de requêtes ; utilisez un pool de connexions ; dimensionnez correctement le buffer pool InnoDB.
- Symptôme : CPU MySQL OK, mais requêtes stagnent
- Cause racine : Attentes de verrou ou attentes I/O (pression sur les fsync du redo log, misses du buffer pool).
- Fix : Utilisez
SHOW ENGINE INNODB STATUS; raccourcissez les transactions ; réglez redo log et buffer pool ; déplacez les écritures chaudes hors du primaire si possible.
Checklists / step-by-step plan
Pas à pas : décider si SQLite est encore sûr pour votre site
- Cartographiez vos écrivains. Listez chaque chemin de code qui écrit : requêtes, cron, jobs en arrière‑plan, analytics, sessions, invalidation de cache, outils admin.
- Mesurez la concurrence d’écritures. Pas les écritures moyennes — les écritures simultanées de pointe. Si c’est « inconnu », supposez que c’est « trop élevé » jusqu’à preuve du contraire.
- Activez WAL (si pas déjà) et vérifiez‑le. Confirmez
PRAGMA journal_modeen WAL et comprenez le comportement de checkpoint. - Imposez des transactions courtes. Interdisez les « transaction ouverte pendant un appel réseau ». Si votre ORM facilite ça, ce n’est pas un compliment.
- Prouvez vos sauvegardes. Restaurez chaque semaine. Exécutez des checks d’intégrité. Entraînez le RTO, pas seulement le RPO.
- Gardez la DB sur un stockage local. Si vous ne pouvez pas, considérez‑le comme un risque majeur et planifiez la migration.
- Planifiez votre histoire d’échelle horizontale. Si vous avez besoin de plusieurs serveurs applicatifs écrivant, planifiez la migration MySQL pendant que vous êtes calme.
Pas à pas : migrer de SQLite vers MySQL sans en faire un événement de carrière
- Gelez la sémantique du schéma. Décidez explicitement des types, contraintes et valeurs par défaut. SQLite est permissif ; MySQL vous forcera à choisir.
- Choisissez une stratégie de migration. Pour petits jeux de données : migration avec downtime. Pour plus gros : dual‑write ou capture de changement (plus dur, mais possible).
- Construisez un export/import répétable. La première exécution est une répétition ; la deuxième est celle qui vous fera dormir la nuit.
- Validez les comptes et invariants. Comptes de lignes par table, checksum des champs critiques, exécutez des vérifications de cohérence applicative.
- Déplacez les lectures d’abord (optionnel). Parfois vous pouvez pointer des endpoints en lecture seule vers des replicas MySQL pendant que les écritures restent sur SQLite, mais méfiez‑vous des hypothèses de consistance.
- Basculez les écritures avec un plan de rollback clair. Le rollback n’est pas de la panique. C’est une procédure.
- Activez l’observabilité MySQL dès le jour un. slow query log, error log, métriques, sauvegardes et exercices de restauration.
FAQ
1) SQLite peut‑il gérer un « haut trafic » ?
Oui — si « haut trafic » signifie surtout des lectures, et que les écritures sont contrôlées. La question n’est pas le trafic ; c’est les écritures concurrentes et combien de temps elles gardent des verrous.
2) Le mode WAL est‑il toujours meilleur ?
Généralement pour les charges web, oui. Il améliore la concurrence en lecture pendant les écritures. Mais il introduit la dynamique WAL et checkpoint que vous devez comprendre, et certains cas limites (comme certains systèmes de fichiers ou outils) demandent de la prudence.
3) Quel est le signe le plus clair que je dois migrer vers MySQL ?
Plusieurs serveurs applicatifs écrivant sur la même base, ou une contention d’écritures fréquente que vous ne pouvez éliminer sans déformer le produit. Aussi : quand vous avez besoin de réplication/basculement comme outil standard, pas comme projet scientifique.
4) Puis‑je exécuter SQLite sur NFS si je suis prudent ?
Parfois, mais « prudent » nécessite une définition : sémantiques de verrouillage vérifiées, modes de défaillance testés, et généralement discipline d’écrivain unique. Si vous faites des écritures concurrentes multi‑hôtes, vous pariez votre site sur des détails de système de fichiers.
5) MySQL n’est‑il pas « plus lourd » et plus lent à cause du réseau ?
Il ajoute une surcharge réseau, oui. Mais il achète le contrôle de concurrence, le buffering et des outils opérationnels. Pour des charges multi‑clients à forte écriture, MySQL gagne souvent en latence réelle de bout en bout parce qu’il évite les blocages induits par les verrous.
6) Si SQLite se bloque sur les écritures, puis‑je simplement augmenter le busy timeout ?
Vous pouvez, et cela peut réduire les erreurs. Mais cela convertit aussi la contention en latence. Si votre site a des SLOs de temps de réponse stricts, de longs busy timeouts ne sont qu’un échec lent avec de meilleurs logs.
7) Qu’en est‑il d’utiliser SQLite pour les sessions ou les événements analytics ?
Ce sont des profils d’écriture classiques qui créent de la contention. Si vous tenez à SQLite, regroupez et mettez en file les écritures, et évitez de mettre à jour des lignes à chaque requête. Sinon, utilisez un système séparé (MySQL, Redis ou une pipeline de logs) conçu pour ce profil d’écritures.
8) Quel est le plus gros « piège » lors du passage de SQLite à MySQL ?
La flexibilité typage permissif de SQLite peut cacher des problèmes de qualité des données. MySQL vous forcera à choisir des types et collations, et exposera de mauvaises hypothèses dans le code et les données.
9) Puis‑je utiliser SQLite avec plusieurs processus sur une même machine ?
Oui. C’est un déploiement courant et valide. Mais vous devez toujours gérer la contention d’écritures : mode WAL, transactions courtes et planification prudente des jobs en arrière‑plan.
10) Si je reste sur SQLite, quelle est la discipline la plus importante ?
Gardez les écritures contrôlées et les transactions courtes. Ensuite, vérifiez les sauvegardes avec des tests de restauration. Ces deux habitudes évitent la plupart des histoires « SQLite a ruiné notre site ».
Conclusion : prochaines étapes qui ne vous feront pas honte
SQLite n’est pas « pour les démos ». Il sert des charges qui respectent sa conception : mono‑nœud, écritures contrôlées et stockage sensé. Quand il échoue, c’est généralement parce que vous lui avez demandé de se comporter comme une base de données serveur multi‑hôtes tout en gardant la commodité de « juste un fichier ». Ce n’est pas un plan technique ; c’est du souhait.
Faites le travail pratique :
- Si vous êtes sur SQLite : confirmez le mode WAL, mesurez la latence disque, auditez les écrivains, raccourcissez les transactions et testez les restaurations. Si vous avez besoin de plusieurs serveurs applicatifs écrivant, planifiez la migration pendant que vous avez encore du temps.
- Si vous êtes sur MySQL (ou que vous y migrez) : activez le slow query logging, surveillez les attentes de verrou, dimensionnez le buffer pool, et traitez sauvegardes/restaurations comme une fonctionnalité de production, pas une case de conformité.
- Dans les deux cas : basez votre choix de base de données sur la concurrence et la récupération après défaillance, pas sur des impressions.