Vous ajoutez un tableau de bord « simple » en temps réel. Le produit adore. Les dirigeants adorent. Puis le paiement devient bizarre : la latence p95 double, le CPU plafonne, et MySQL commence à cracher des « too many connections » comme s’il passait une audition pour un drame médical.
Le tableau de bord n’est pas le méchant. L’architecture l’est. Les bases OLTP et les charges analytiques obéissent à des physiques différentes, et ignorer cela, c’est expliquer ensuite aux finances pourquoi le « taux de conversion » s’est temporairement transformé en « zéro ».
Le vrai problème : charges mixtes et domaines de défaillance partagés
Les tableaux de bord en temps réel échouent de manière étonnamment prévisible : ils demandent aux systèmes OLTP de se comporter comme des OLAP. Parfois on peut forcer le passage avec des index, des réplicas et du cache. Mais si le tableau de bord compte, il va croître. Il va accumuler des filtres, des jointures, encore une « dimension de plus ». Puis un jour il s’exécute lors d’un pic de campagne et votre flux de paiement a une place au premier rang de vos ambitions analytiques.
Le problème central n’est pas la vitesse de la requête. C’est l’interférence. OLTP et analytics se disputent les mêmes ressources rares :
- CPU (l’analytics l’adore ; l’OLTP en a besoin pour la concurrence)
- Buffer pool / page cache (l’analytics le fait tourner ; l’OLTP profite de la stabilité)
- I/O (l’analytics scanne ; l’OLTP a besoin de lectures/écritures aléatoires à faible latence)
- Verrous et latches (pas toujours évidents, mais ils apparaissent quand on s’y attend le moins)
- Slots de connexion (les tableaux de bord sont souvent « bavards »)
Les tableaux de bord sont aussi des charges socialement privilégiées. Personne ne dit « coupez le paiement pendant une heure ». En revanche on dit absolument « pourquoi le tableau de bord n’est-il pas en temps réel ? » C’est comme ça qu’on se met à optimiser la mauvaise chose.
Une vérité sèche : « temps réel » signifie généralement « assez frais pour prendre des décisions ». Ça peut être 2 secondes. Ça peut être 2 minutes. Si vous ne le définissez pas, vous implémenterez la pire forme : un temps réel coûteux.
MySQL vs ClickHouse : pour quoi chaque moteur est vraiment bon
MySQL : le cheval de bataille du checkout
MySQL est une base OLTP avec un écosystème mature, des outils d’exploitation solides et un comportement prévisible sous des charges transactionnelles orientées écriture. InnoDB est optimisé pour :
- Beaucoup de petites lectures/écritures
- Recherches ponctuelles par clé primaire ou index secondaires bien choisis
- Garanties transactionnelles et lectures cohérentes
- Haute concurrence avec des requêtes courtes
MySQL peut exécuter des requêtes de type analytics. Il peut même bien le faire si votre jeu de données est modeste et vos requêtes disciplinées. Mais quand les tableaux de bord commencent à faire de gros scans, des group-bys lourds, des jointures larges et des « 90 derniers jours par X et Y et Z », MySQL paye en I/O, en churn du buffer pool et en CPU. La facture apparaît à l’endroit où vous ne voulez pas : la latence en queue.
ClickHouse : le moteur de tableau de bord qui dévore les scans au petit-déjeuner
ClickHouse est une base OLAP en colonnes conçue pour des requêtes analytiques à haut débit sur de grands jeux de données. Il excelle quand vous avez besoin de :
- Agrégations rapides sur des milliards de lignes
- Dimensions à haute cardinalité (dans la mesure du raisonnable et avec un bon design)
- Compression et stockage efficace pour des données d’événements append-only
- Exécution de requêtes en parallèle
Les forces de ClickHouse s’accompagnent d’un ensemble différent d’attentes : vous modélisez pour la lecture, vous acceptez des patterns de cohérence éventuelle, et vous apprenez les particularités des merges, des parts et du travail en arrière-plan. Si vous essayez de l’utiliser comme MySQL (mises à jour ligne par ligne, workflows transactionnels, nombreuses petites mutations), il vous le rappellera — poliment au début — que vous l’utilisez mal.
Voici la ligne de décision que j’utilise en production : si la forme de la requête est « scan + filtre + group-by + fenêtre temporelle », cela appartient à ClickHouse. Si c’est « lire/mettre à jour un petit nombre de lignes, appliquer des invariants, prendre de l’argent », gardez-le dans MySQL.
Blague #1 : des tableaux de bord en temps réel sur MySQL, c’est comme remorquer un bateau avec un scooter. On peut le faire brièvement, mais tout le monde apprend quelque chose d’inconfortable.
Faits et contexte historique intéressants (ce qui explique les compromis actuels)
- Les magasins en colonnes sont devenus courants quand les jeux de données analytiques ont dépassé les hypothèses de mise en cache des row-stores ; la compression et l’exécution vectorisée ont changé la donne.
- ClickHouse a démarré chez Yandex pour alimenter des analyses web à grande échelle ; il a été conçu pour répondre rapidement à « qu’est‑ce qui s’est passé ? », pas à « cet achat a-t-il réussi ? »
- Le design du buffer pool d’InnoDB est excellent pour les working sets chauds ; les scans de tableaux de bord peuvent évincer les pages dont le checkout a besoin.
- La réplication MySQL a été historiquement utilisée pour délester les lectures ; cela aide, mais les patterns de lecture analytiques peuvent toujours saturer les réplicas et faire croître le lag.
- L’essor du CDC (change data capture) est devenu courant car les équipes voulaient découpler l’OLTP de l’OLAP sans maintenir des « jobs d’export » faits maison chaque semaine.
- Les vues matérialisées ne sont pas nouvelles ; ce qui a changé, c’est le coût de leur maintien grâce au stockage et au calcul modernes, surtout dans les moteurs en colonnes.
- Les dimensions à haute cardinalité étaient autrefois considérées comme une pénalité ; ClickHouse les a rendues faisables, mais pas gratuites — l’encodage dictionnaire et l’ordre restent importants.
- « Temps réel » signifiait autrefois batch toutes les heures pour beaucoup d’entreprises ; aujourd’hui les attentes sont en minutes ou secondes parce que l’alerte et les boucles marketing se sont resserrées.
Architectures qui fonctionnent (et pourquoi)
Pattern A : MySQL pour l’OLTP, ClickHouse pour les tableaux de bord (recommandé pour le vrai temps réel)
C’est l’architecture adulte. Vous conservez la vérité transactionnelle dans MySQL, vous streammez les changements, et vous interrogez ClickHouse pour les tableaux de bord. Le bénéfice clé n’est pas la vitesse. C’est le contrôle du rayon d’explosion. Les tableaux de bord peuvent mal se comporter sans suffoquer le checkout.
Composants principaux :
- MySQL primaire pour les écritures
- Réplique(s) MySQL pour le trafic de lecture opérationnel (optionnel)
- Pipeline CDC (basé sur le binlog) vers ClickHouse
- Tables ClickHouse optimisées pour les requêtes (famille MergeTree)
- Pré-agrégations quand elles rapportent (vues matérialisées ou tables rollup)
La partie difficile : décider quoi transférer et comment le modéliser. Si vous répliquez naïvement des tables normalisées OLTP dans ClickHouse et attendez les mêmes patterns de jointure, vous finirez avec des requêtes lentes et une équipe confuse. Dénormalisez de manière stratégique.
Pattern B : « Utiliser une réplique MySQL pour les tableaux de bord » (ça marche jusqu’à ce que ça casse)
Une réplique est tentante parce que ça ressemble à un déjeuner gratuit. Ce n’en est pas un. Le lag de réplication sous de fortes lectures est réel, et si les tableaux de bord frappent la réplique avec de gros scans, vous construisez essentiellement une fournaise I/O. Cela peut convenir si :
- Le jeu de données est relativement petit
- Les tableaux de bord sont limités et stables
- Les exigences de fraîcheur sont lâches
- Vous pouvez tolérer des pics de lag occasionnels
Ce pattern échoue quand l’usage du tableau de bord croît (et il va croître) ou quand vos événements commerciaux les plus importants se corrèlent avec des pics de trafic (ce qu’ils font).
Pattern C : Double écriture (l’application écrit dans MySQL et ClickHouse)
La double écriture peut atteindre une faible latence. Elle introduit aussi un risque de cohérence : écritures partielles, différences d’ordre, retries, et le phénomène « ça marche en staging ». Si vous la faites, vous avez besoin d’idempotence, de backfills et d’une stratégie de réconciliation. Sinon le tableau de bord devient un exercice de créativité.
Pattern D : ETL batch vers ClickHouse (ennuyeux, bon marché, parfois parfait)
Si votre activité peut vivre avec une fraîcheur de 5–15 minutes, le batch gagne par sa simplicité. Vous pouvez lancer des extractions périodiques depuis MySQL (idéalement depuis une réplique), charger dans ClickHouse, et garder le pipeline compréhensible. Les gens sous-estiment la valeur de “compréhensible” à 3 h du matin.
Modélisation des données : la partie que tout le monde saute, puis paie
Choix de schéma MySQL qui sabotent les tableaux de bord
En OLTP, la normalisation aide la cohérence et la performance d’écriture. En analytics, les schémas normalisés transfèrent le coût dans les jointures et les recherches répétées. Une requête de tableau de bord qui joint orders, order_items, users, payments, shipments et promotions est une excellente manière de brûler CPU et I/O.
L’astuce n’est pas « dénormaliser tout ». L’astuce est de créer un modèle d’événements ou de faits qui corresponde aux questions que vous posez :
- Faits : order_created, payment_captured, shipment_delivered, refund_issued
- Dimensions : merchant, country, device type, campaign, payment method
- Temps : toujours de première classe ; les tableaux de bord sont des séries temporelles avec des opinions
Conception des tables ClickHouse : ORDER BY est votre index, et c’est un engagement
Dans ClickHouse, le ORDER BY des tables MergeTree est le levier principal pour la vitesse des requêtes. Il définit comment les données sont organisées physiquement. Choisissez-le en fonction de vos filtres les plus fréquents. Pour des tableaux de bord temps réel, c’est généralement :
- La fenêtre temporelle en premier (par ex. event_date, event_time)
- Un petit ensemble de dimensions communes (par ex. merchant_id, region, event_type)
Si vous choisissez ORDER BY user_id parce que ça ressemble à une clé primaire, vous le regretterez quand tous les tableaux de bord filtreront par temps et scanneront tout de toute façon.
Pré-agrégation : quand ça aide, quand ça trompe
Les pré-agrégations (vues matérialisées, rollups) sont puissantes. Elles créent aussi un risque subtil : les gens font confiance aux chiffres du tableau de bord comme s’ils étaient la vérité alors que la logique de pré-agrégation dérive silencieusement des sémantiques source. C’est particulièrement courant avec les remboursements, les captures partielles et les concepts d’« état final ».
Ma règle : pré-agréger seulement quand vous pouvez définir la métrique précisément, y compris les événements arrivant en retard et les corrections. Sinon, gardez les faits bruts et acceptez un coût de requête plus élevé (dans ClickHouse, souvent possible).
Pipelines d’ingestion : CDC, batch, streaming et le mensonge du « temps réel »
CDC depuis MySQL : à quoi vous vous engagez réellement
Le CDC est le choix habituel pour des tableaux de bord quasi temps réel parce qu’il donne une faible latence sans marteler MySQL avec des requêtes fréquentes. Mais le CDC n’est pas magique ; c’est un système distribué. Vous devez répondre à :
- Ordre : les événements arrivent-ils dans l’ordre des commits ? par table ? par partition ?
- Changements de schéma : comment sont gérés les DDL et les changements de type ?
- Suppressions/mises à jour : les modélisez-vous comme de nouveaux événements, ou comme des mutations ?
- Backfills : comment ré-ingérer des données historiques en toute sécurité ?
- Idempotence : pouvez-vous rejouer sans double comptage ?
Pour ClickHouse, beaucoup d’équipes préfèrent un flux d’événements append-only même quand la source est mise à jour en ligne. Plutôt que « mettre à jour la ligne », on écrit « état modifié » en tant qu’événement. Ce n’est pas toujours possible, mais c’est souvent plus propre.
Budgets de fraîcheur : définissez-les ou souffrez
« Temps réel » doit être un budget avec des SLO : p95 du lag d’ingestion < 30 secondes, p99 < 2 minutes, par exemple. Sans cela, les équipes chassent le mauvais goulot. Aussi : vous aurez finalement besoin d’une bannière « données retardées » dans l’UI du tableau de bord. Ce n’est pas optionnel ; c’est de l’honnêteté.
Citation (idée paraphrasée) : Werner Vogels a souvent poussé l’idée que vous devriez choisir les modèles de cohérence intentionnellement ; la fiabilité vient des compromis explicites.
Playbook de diagnostic rapide (trouver le goulet en minutes)
Quand les tableaux de bord sont lents ou que le checkout souffre, ne commencez pas par des opinions. Commencez par où le temps est passé et quel système est saturé. Cet ordre fonctionne bien sous pression.
1) La douleur vient-elle de MySQL, ClickHouse ou de la couche applicative ?
- Vérifiez la latence p95 des requêtes MySQL et le CPU/iowait
- Vérifiez la latence des requêtes ClickHouse et la charge des merges/travaux en arrière-plan
- Vérifiez la saturation du service de tableau de bord (threads, limites de pool, GC si pertinent)
2) Si le checkout est lent : confirmez d’abord l’interférence MySQL
- Cherchez des SELECTs longues provenant d’utilisateurs du tableau de bord
- Cherchez le churn du buffer pool et des lectures disque
- Cherchez des pics de connexion et la contention des threads
3) Si les tableaux de bord sont lents : déterminer si c’est la fraîcheur des données, la forme de la requête ou la disposition du stockage
- Le lag d’ingestion augmente ? Alors c’est le pipeline ou la capacité d’insert/merge de ClickHouse.
- Si l’ingestion va bien mais la requête est lente : c’est l’ORDER BY / le partitioning / des scans trop larges.
- Si les requêtes sont rapides mais l’UI lente : c’est l’application et la stratégie de cache.
4) Décidez l’atténuation immédiate
- Limiter le débit des requêtes du tableau de bord (rate limit, snapshots mis en cache)
- Déplacer les tableaux de bord hors de MySQL maintenant (même vers une réplique) si le checkout brûle
- Réduire temporairement la fenêtre temporelle et les dimensions des requêtes
- Scaler ClickHouse ou ajuster les merges si c’est le goulot
Tâches pratiques : commandes, sorties et quelle décision prendre
Ci‑dessous des tâches concrètes que j’exécute réellement durant les incidents et les cycles d’optimisation. Chacune inclut une commande, une sortie d’exemple, ce que cela signifie, et la décision qu’elle entraîne. Vous pouvez exécuter la plupart depuis un bastion ou directement sur les hôtes DB avec l’accès approprié.
Tâche 1 : Repérer les requêtes de tableau de bord qui nuisent à MySQL maintenant
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,120p'
*************************** 1. row ***************************
Id: 83421
User: dashboard_ro
Host: 10.22.8.14:51122
db: checkout
Command: Query
Time: 37
State: Sending data
Info: SELECT date(created_at) d, count(*) FROM orders WHERE created_at > now() - interval 7 day GROUP BY d
*************************** 2. row ***************************
Id: 83455
User: checkout_app
Host: 10.22.3.7:60318
db: checkout
Command: Query
Time: 1
State: updating
Info: UPDATE inventory SET reserved = reserved + 1 WHERE sku = 'A17-44'
Signification : vous avez un scan/group-by long sur le primaire, en concurrence avec des mises à jour transactionnelles.
Décision : tuer ou limiter la requête du tableau de bord ; migrer les tableaux de bord vers ClickHouse ou au moins vers une réplique ; ajouter des garde-fous de requête immédiatement.
Tâche 2 : Tuer l’élément coupable spécifique (chirurgical, pas héroïque)
cr0x@server:~$ mysql -e "KILL 83421;"
Query OK, 0 rows affected (0.00 sec)
Signification : la requête fautive est terminée.
Décision : suivre avec une politique d’accès : l’utilisateur du tableau de bord ne doit pas interroger le primaire, point final.
Tâche 3 : Vérifier la charge actuelle de MySQL et les statements les plus lents via Performance Schema
cr0x@server:~$ mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s, ROUND(AVG_TIMER_WAIT/1e9,2) AS avg_ms FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT date(created_at) d, count(*) FROM orders WHERE created_at > ? GROUP BY d
COUNT_STAR: 1821
total_s: 614.35
avg_ms: 337.41
*************************** 2. row ***************************
DIGEST_TEXT: SELECT * FROM cart_items WHERE cart_id = ?
COUNT_STAR: 92144
total_s: 211.02
avg_ms: 2.29
*************************** 3. row ***************************
DIGEST_TEXT: UPDATE inventory SET reserved = reserved + ? WHERE sku = ?
COUNT_STAR: 40122
total_s: 88.44
avg_ms: 2.20
Signification : le digest du tableau de bord domine le temps total de la DB.
Décision : prioriser le déplacement de cette métrique vers ClickHouse ou la réécrire pour utiliser un index/table de résumé, et appliquer des limites de requête.
Tâche 4 : Confirmer s’il y a churn du buffer pool
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 19433211 |
+-------------------------+----------+
+----------------------------------+------------+
| Variable_name | Value |
+----------------------------------+------------+
| Innodb_buffer_pool_read_requests | 8821132441 |
+----------------------------------+------------+
Signification : les lectures disque sont non triviales ; si le premier nombre augmente rapidement pendant l’utilisation du tableau de bord, les scans évinceraient des pages chaudes.
Décision : isoler l’analytics hors du primaire ; envisager d’augmenter le buffer pool seulement après isolation (une plus grosse fournaise reste une fournaise).
Tâche 5 : Vérifier le lag de réplication MySQL avant d’accuser « le serveur de tableau de bord »
cr0x@server:~$ mysql -h mysql-replica-1 -e "SHOW REPLICA STATUS\G" | egrep "Seconds_Behind_Source|Replica_IO_Running|Replica_SQL_Running"
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 187
Signification : la réplique est ~3 minutes derrière ; les tableaux de bord qui lisent depuis elle seront datés et peuvent déclencher des tickets « pourquoi les chiffres ne correspondent pas ? ».
Décision : soit accepter des SLA de fraîcheur explicites, soit ajouter plus de réplicas, soit déplacer l’analytics vers ClickHouse avec un ingest contrôlé.
Tâche 6 : Vérifier que le format du binary log MySQL est compatible avec les besoins CDC
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'gtid_mode';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode | ON |
+---------------+-------+
Signification : les binlogs ROW et les GTID simplifient la correction et la reprise du CDC.
Décision : si ce n’est pas ROW, planifiez une migration ; le CDC sur STATEMENT est un piège à moins d’aimer les cas limites.
Tâche 7 : Inspecter les requêtes en cours sur ClickHouse (trouver le glouton du tableau de bord)
cr0x@server:~$ clickhouse-client --query "SELECT query_id, user, elapsed, read_rows, formatReadableSize(read_bytes) rb, query FROM system.processes ORDER BY elapsed DESC LIMIT 2"
d8e2c6c1-8d1b-4c3f-bc45-91b34a6c12de dashboard 12.941 812334112 34.21GiB SELECT merchant_id, count() FROM events WHERE event_time > now() - INTERVAL 30 DAY GROUP BY merchant_id
a1a01f2a-9d72-4d85-9b43-423a1c91a8f1 internal 1.120 182991 17.02MiB INSERT INTO events FORMAT JSONEachRow
Signification : une requête de tableau de bord lit 34 GiB pour répondre à une question qui a probablement besoin d’une fenêtre temporelle plus étroite ou d’un meilleur ordering.
Décision : corriger l’ORDER BY/partitioning, ajouter des rollups, et limiter les valeurs par défaut de lookback du tableau de bord. Envisager aussi des quotas de requêtes.
Tâche 8 : Vérifier la pression des merges ClickHouse (le travail en arrière-plan vous pique votre déjeuner)
cr0x@server:~$ clickhouse-client --query "SELECT database, table, sum(merge_type = 'Regular') AS regular_merges, sum(merge_type = 'TTL') AS ttl_merges, round(sum(elapsed),1) AS total_s FROM system.merges GROUP BY database, table ORDER BY total_s DESC LIMIT 5"
analytics events 4 0 912.4
analytics sessions 1 2 211.9
Signification : des merges sont actifs et de longue durée sur la table hot events.
Décision : ajuster les tailles de batch d’insert, le partitioning et les paramètres de merge ; ajouter de la capacité si les merges s’accumulent constamment.
Tâche 9 : Vérifier l’explosion de parts ClickHouse (classique « trop petits inserts »)
cr0x@server:~$ clickhouse-client --query "SELECT table, count() parts, formatReadableSize(sum(bytes_on_disk)) disk FROM system.parts WHERE active AND database='analytics' GROUP BY table ORDER BY parts DESC LIMIT 3"
events 12844 1.92TiB
sessions 2211 204.11GiB
rollup_minute 144 9.87GiB
Signification : 12k parts actives suggèrent que les inserts sont trop granulaires, causant une dette de merge et un overhead de requête.
Décision : batcher les inserts, utiliser du buffering, ou ajuster votre outil d’ingestion ; réduire les parts avant de scaler le hardware à l’aveugle.
Tâche 10 : Mesurer le lag d’ingestion comme métrique de première classe (ne pas deviner)
cr0x@server:~$ clickhouse-client --query "SELECT max(event_time) AS max_event, now() AS now_ts, dateDiff('second', max_event, now_ts) AS lag_s FROM analytics.events"
2025-12-30 11:58:29 2025-12-30 12:00:03 94
Signification : ClickHouse a environ 94 secondes de retard sur « now » pour cette table.
Décision : si votre SLO est 30 secondes, concentrez-vous sur le pipeline et la capacité de merge ; si votre SLO est 2 minutes, tout va bien et arrêtez de paniquer.
Tâche 11 : Valider l’efficacité du pruning ClickHouse avec EXPLAIN (scannez-vous tout ?)
cr0x@server:~$ clickhouse-client --query "EXPLAIN indexes=1 SELECT count() FROM analytics.events WHERE event_date >= today()-1 AND merchant_id=42"
Expression ((Projection + Before ORDER BY))
ReadFromMergeTree (analytics.events)
Indexes:
MinMax
Keys: event_date
Condition: (event_date in [today() - 1, +Inf))
PrimaryKey
Keys: (event_date, merchant_id)
Condition: (event_date in [today() - 1, +Inf)) AND (merchant_id in [42, 42])
Signification : à la fois MinMax et les conditions de clé primaire s’appliquent ; le pruning devrait être correct.
Décision : si vous ne voyez pas de conditions d’index utiles, revoyez l’ORDER BY et la clé de partition.
Tâche 12 : Confirmer que le tableau de bord ne fait pas un DDoS de votre DB avec des polls courts
cr0x@server:~$ sudo ss -tnp | awk '$4 ~ /:3306$/ {print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr | head
422 10.22.8.14
38 10.22.8.15
21 10.22.3.7
Signification : un hôte de tableau de bord a 422 connexions TCP vers MySQL.
Décision : ajouter du pooling de connexions, appliquer un max connections, mettre en cache les résultats, et stopper le « refresh toutes les 1s » à la source.
Tâche 13 : Vérifier l’iowait au niveau hôte sur la machine MySQL (vérif rapide de réalité)
cr0x@server:~$ iostat -x 1 3
Linux 6.8.0 (mysql-primary) 12/30/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.12 0.00 6.33 24.51 0.00 51.04
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await aqu-sz %util
nvme0n1 812.0 98304.0 0.0 0.00 8.12 121.1 244.0 16384.0 3.91 6.41 92.3
Signification : iowait élevé et ~92% d’utilisation disque ; MySQL est lié par les I/O sous la charge actuelle.
Décision : arrêter les scans sur le primaire, puis revoir le stockage/IOPS ; ajouter des disques plus rapides à un mélange de charges est une manière coûteuse de rester dans l’erreur.
Tâche 14 : Vérifier l’usage disque de ClickHouse et si les merges sont bloqués par l’espace
cr0x@server:~$ df -h /var/lib/clickhouse
Filesystem Size Used Avail Use% Mounted on
/dev/nvme1n1p1 3.5T 3.3T 140G 96% /var/lib/clickhouse
Signification : ClickHouse est à 96% d’utilisation ; les merges peuvent échouer ou se throttler, augmentant les parts et ralentissant tout.
Décision : libérer de l’espace maintenant (rétention/TTL), ajouter du disque, ou déplacer des partitions froides. Ne pas « attendre le week-end » ; le week-end est quand les merges décident de paniquer.
Trois mini-histoires d’entreprise issues du terrain
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Un détaillant de taille moyenne voulait des tableaux de bord « en direct » pendant une promotion de vacances. L’équipe a supposé, raisonnablement sur le papier, qu’une réplique de lecture isolerait l’analytics du checkout. Ils ont créé une réplique, pointé le tableau de bord dessus, et passé à autre chose.
Le premier problème fut la fraîcheur. Lors de la première grosse vague de trafic, le lag de réplication est passé de secondes à minutes. Les dirigeants ont vu le tableau de bord « se figer » et ont exigé une correction. Quelqu’un a suggéré, avec une confiance admirable et sans sens du danger, que le tableau de bord lise depuis le primaire « juste pour la campagne ».
Cinq minutes plus tard, la latence du checkout a grimpé. Pas parce que les SELECTs prennent des verrous de façon spectaculaire, mais parce que ces scans ont fait churner le buffer pool et saturé l’I/O. Les commandes se validaient toujours, mais la latence en queue est devenue moche et des timeouts sont apparus.
L’hypothèse erronée était subtile : « les lectures sont sûres. » Les lectures ne sont pas sûres quand elles sont grosses, fréquentes et non mises en cache. Les lectures peuvent absolument faire tomber un système orienté écriture en le privant des ressources dont il a besoin.
La correction n’a pas été un tuning héroïque de requêtes. La correction a été architecturale : ils ont retiré les tableaux de bord de MySQL entièrement, accepté 60–120 secondes de fraîcheur pendant la campagne, et déployé un pipeline CDC-vers-ClickHouse ensuite. L’incident s’est terminé par une politique : les tableaux de bord n’interrogent pas le primaire, même quand quelqu’un de haut placé hurle.
Mini-histoire 2 : L’optimisation qui s’est retournée contre eux
Une entreprise SaaS a déplacé l’analytics vers ClickHouse et a été excitée. Les requêtes étaient rapides. Tout le monde adorait la flexibilité. Puis quelqu’un a optimisé l’ingestion pour « réduire la latence » en envoyant chaque événement comme son propre insert. Ça marchait en test. En production, ça a créé une explosion de parts.
En quelques jours, les merges d’arrière-plan tournaient constamment. Le CPU semblait « ok » à première vue, mais le disque chauffait, et la latence des requêtes est devenue sporadique. Les tableaux de bord étaient rapides, puis lentement lents au hasard. L’équipe a commencé à ajouter des nœuds. Ça a aidé un moment, puis les merges ont rattrapé comme un auditeur fiscal.
Ils ont essayé de tuner les merges et ont même envisagé de changer d’engines de table. Le vrai problème était en amont : trop petits inserts. ClickHouse peut ingérer beaucoup, mais il veut des lots. De petites parts coûtent en overhead metadata, en merges et en requêtes. C’est la mort par mille commits.
La correction a été ennuyeuse : bufferiser les événements pendant quelques secondes et insérer par lots. La latence a augmenté légèrement, mais le système est devenu stable et moins cher. L’optimisation qui s’est retournée contre eux poursuivait quelques secondes de fraîcheur au prix d’une performance soutenue.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la journée
Une autre équipe utilisait à la fois MySQL et ClickHouse pour les tableaux de bord. Ils avaient aussi quelque chose de rare : un SLO de fraîcheur écrit, une métrique de lag d’ingestion affichée sur le tableau de bord, et un runbook on-call qui commençait par « vérifier le lag avant de déboguer les requêtes ».
Un après-midi, les tableaux de bord ont commencé à afficher des graphiques « plats ». Des ventes avaient lieu, mais le graphique semblait mort. Slack s’est rempli de panique. L’on-call a suivi le runbook et a vérifié le max event time de ClickHouse par rapport à maintenant. Le lag était de 18 minutes. Ce n’est pas un problème de requête ; c’est un problème d’ingestion.
Ils ont vérifié le consumer CDC et l’ont trouvé bloqué sur une seule ligne problématique à cause d’un changement de schéma inattendu : une colonne est passée de nullable à non-nullable, et le mapping d’ingestion rejetait les enregistrements. Parce qu’ils avaient des métriques explicites, ils n’ont pas perdu une heure à tuner des requêtes déjà rapides.
Ils ont appliqué la correction du mapping de schéma, rejoué l’arriéré, et les tableaux de bord se sont rétablis. Personne n’a loué le runbook. Personne n’écrit des poèmes sur les « alertes de lag ». Mais la pratique ennuyeuse a sauvé un après-midi de confusion et a empêché quelqu’un de « corriger » en pointant les tableaux de bord sur MySQL.
Blague #2 : un tableau de bord qui interroge MySQL de production est essentiellement un test de performance, sauf que les résultats sont notés par des clients en colère.
Erreurs courantes : symptôme → cause racine → correction
1) Spike de latence p95 du checkout au lancement d’un tableau de bord
- Symptôme : CPU MySQL et iowait augmentent ; les requêtes lentes montrent des SELECTs de type analytics.
- Cause racine : tableaux de bord interrogeant le primaire ou une réplique partagée qui sert aussi les lectures applicatives ; churn du buffer pool et saturation I/O.
- Correction : isoler les tableaux de bord dans ClickHouse ; appliquer des permissions au niveau utilisateur ; ajouter des limites et timeouts de requête ; mettre en cache les réponses du tableau de bord.
2) Les tableaux de bord sont « temps réel » mais les chiffres ne correspondent pas à MySQL
- Symptôme : ClickHouse montre moins de commandes que MySQL pour les dernières minutes ; des backfills « réparent » le passé.
- Cause racine : lag CDC, événements hors ordre, ou sémantique update/delete manquante.
- Correction : suivre le lag d’ingestion ; concevoir des clés d’événement idempotentes ; modéliser explicitement les changements d’état ; ajouter des jobs de réconciliation pour les métriques critiques.
3) Les requêtes ClickHouse ralentissent aléatoirement même si le hardware est correct
- Symptôme : même tableau de bord parfois 200ms, parfois 8s ; merges en activité.
- Cause racine : trop de parts, backlog de merges, ou disque presque plein provoquant du throttling.
- Correction : batcher les inserts ; monitorer le nombre de parts ; augmenter la marge disque ; ajuster le partitioning ; ajouter des nœuds seulement après réduction de la fragmentation d’ingestion.
4) Les tableaux de bord basés sur des réplicas montrent des données obsolètes pendant les pics
- Symptôme : « Seconds_Behind_Source » saute ; la fraîcheur du tableau de bord est incohérente.
- Cause racine : le réplica ne peut pas appliquer le relay log assez vite sous la pression des lectures ; la réplication n’est pas une voie analytique gratuite.
- Correction : déplacer les tableaux de bord vers ClickHouse ; ajouter une réplique analytique dédiée ; réduire la charge de requêtes ; scaler l’I/O et le CPU du réplica.
5) Le stockage ClickHouse croît plus vite que prévu
- Symptôme : l’usage disque augmente ; les politiques de rétention ne fonctionnent pas ; les merges tournent en boucle.
- Cause racine : TTL/partition drops manquants ; stockage de colonnes trop verbeuses ; duplication d’événements due à une ingestion non idempotente.
- Correction : appliquer des TTL ; compacter le schéma ; ajouter des clés de déduplication et viser une ingestion exactly-once (ou au moins effectively-once via idempotence).
6) « On peut juste ajouter un index » devient un rituel hebdomadaire
- Symptôme : MySQL a des dizaines d’index ; les écritures ralentissent ; on ne satisfait toujours pas les requêtes du tableau de bord.
- Cause racine : tenter de résoudre des scans OLAP avec du indexing OLTP ; les index aident les recherches ponctuelles, pas l’analyse dimensionnelle illimitée.
- Correction : arrêter d’indexer votre chemin vers la tristesse ; déplacer l’analytics vers ClickHouse et modéliser pour lui.
Listes de contrôle / plan étape par étape
Plan étape par étape : construire des tableaux de bord temps réel sans détruire le checkout
-
Définir les SLO de fraîcheur.
- Notez les cibles de lag d’ingestion p95 et p99.
- Décidez ce qui se passe en cas de non-respect (bannière, cache fallback, mode dégradé).
-
Classifier les requêtes de tableau de bord par forme.
- Scan + group-by + fenêtre temporelle → ClickHouse.
- Recherches ponctuelles pour drill-down → peuvent rester dans MySQL (ou un service séparé).
-
Mettre des garde-fous sur MySQL maintenant.
- Pas d’accès tableau de bord au primaire.
- Définir des timeouts et un temps d’exécution max pour les utilisateurs analytics.
- Ajouter du rate limiting au niveau API.
-
Choisir votre approche d’ingestion.
- CDC pour faible latence et simplicité raisonnable.
- Batch pour la simplicité quand la fraîcheur peut être en minutes.
- Éviter la double écriture sauf si vous avez une forte maturité opérationnelle.
-
Modéliser une table de faits dans ClickHouse.
- Préférez des événements append-only avec ids stables.
- Choisir un
ORDER BYbasé sur le temps + dimensions communes.
-
Implémenter les pré-agrégations seulement pour les requêtes chaudes prouvées.
- Commencez par les faits bruts ; mesurez le coût des requêtes ; puis consolidez quand cela rapporte.
- Définissez les événements tardifs et le comportement des corrections.
-
Opérationnaliser.
- Alerter sur le lag d’ingestion, le count de parts, la marge disque et la latence des requêtes.
- Créer des runbooks on-call qui commencent par « les données sont-elles en retard ? »
-
Tester les modes de défaillance.
- Consumer CDC down.
- ClickHouse disque presque plein.
- Requête de tableau de bord élargie accidentellement à 365 jours.
Checklist de sécurité : protéger le checkout de l’ambition des tableaux de bord
- Les tableaux de bord ne peuvent pas se connecter au primaire MySQL (ACL réseau + enforcement des credentials).
- Les utilisateurs MySQL pour l’analytics ont des privilèges stricts et des limites de ressources basses.
- L’API du tableau de bord a du cache et du rate limiting.
- La fenêtre temporelle par défaut du tableau de bord est petite ; l’élargissement nécessite une action explicite de l’utilisateur.
- ClickHouse a une politique de marge disque (objectif < 80–85% utilisé).
- Le lag d’ingestion est affiché et l’alerte est configurée.
FAQ
1) Puis-je garder les tableaux de bord dans MySQL si j’optimise les requêtes ?
Pour un petit jeu de données et un petit ensemble de tableaux de bord stables, oui. Dès que les tableaux de bord deviennent exploratoires (beaucoup de filtres, fenêtres plus longues), vous luttez contre les objectifs du moteur. Si le checkout compte, isolez l’analytics tôt.
2) Une réplique MySQL est-elle « suffisamment sûre » pour des tableaux de bord ?
C’est plus sûr que de frapper le primaire, mais pas « sûr » par défaut. Les gros scans peuvent saturer la réplique, augmenter le lag, et causer quand même de la douleur opérationnelle. Une réplique analytique dédiée avec contrôles stricts peut marcher comme étape transitoire.
3) Jusqu’à quel point ClickHouse peut-il être temps réel ?
De secondes à minutes, selon le batching d’ingestion, la pression des merges et les patterns de requêtes. Si vous essayez de forcer un ingest sub-second avec de petits inserts, vous paierez ailleurs (parts, merges, disque).
4) Quelle est la plus grosse erreur de modélisation ClickHouse pour les tableaux de bord ?
Choisir un ORDER BY sans penser aux filtres courants, en particulier le temps. Si vos tableaux de bord filtrent par temps et par merchant, votre ORDER BY devrait le refléter.
5) Ai-je besoin de vues matérialisées dans ClickHouse ?
Pas le premier jour. Commencez par des faits bruts et mesurez. Utilisez des vues matérialisées quand un petit nombre de requêtes domine le coût et que les sémantiques de la métrique sont stables.
6) Comment gérer les updates et deletes de MySQL dans ClickHouse ?
Préférez la modélisation événementielle (append des « state change » events) aux mutations fréquentes. Si vous devez refléter l’état des lignes, utilisez des patterns de versioning et des clés de déduplication, et acceptez que les mutations lourdes puissent être coûteuses.
7) Pourquoi les chiffres diffèrent entre MySQL et ClickHouse lors d’incidents ?
Le plus souvent le lag d’ingestion, des événements hors ordre, ou une ingestion dupliquée. Traitez la « fraîcheur des données » comme une métrique, pas un ressenti, et affichez-la sur le tableau de bord.
8) Quelles métriques dois-je alerter pour cette stack ?
MySQL : latence de requête (p95/p99), threads actifs, hit rate du buffer pool, util disque / iowait, lag de réplication.
ClickHouse : latence de requête, count de parts, backlog de merges, utilisation disque, débit d’inserts, lag d’ingestion.
9) Devrais-je mettre en cache les résultats des tableaux de bord ?
Oui, sauf si votre tableau de bord doit absolument être au coin de la seconde. Cachez au niveau API avec des TTL courts et des règles de cache-busting. C’est moins cher que « plus de base de données ».
10) Quelle est une définition raisonnable de « temps réel » pour des tableaux de bord business ?
Ce que vos opérateurs peuvent supporter de manière fiable. Beaucoup d’organisations prospèrent avec une fraîcheur de 30–120 secondes. Rendre cela explicite, le mesurer et l’afficher.
Conclusion : que faire lundi matin
Si vos requêtes de tableau de bord touchent le primaire MySQL, corrigez cela d’abord. Pas demain. Aujourd’hui. Ajoutez une voie dédiée : soit une réplique avec des limites strictes comme palliatif, soit (de préférence) ClickHouse comme plan analytique.
Ensuite faites le travail qui empêche les incidents répétés :
- Définir des SLO de fraîcheur et afficher le lag d’ingestion dans l’UI du tableau de bord.
- Modéliser des faits pour ClickHouse avec un
ORDER BYqui correspond aux filtres réels. - Batcher l’ingestion pour éviter l’explosion de parts et la dette de merge.
- Opérationnaliser : alerter sur le lag, les merges, la marge disque et les pires requêtes.
- Écrire le runbook et en faire la première chose à consulter sous stress.
L’objectif n’est pas « des tableaux de bord rapides. » L’objectif est « des tableaux de bord rapides sans transformer le checkout en expérience de laboratoire. » Séparez les charges, mesurez le lag, et laissez chaque base de données faire le travail pour lequel elle a été conçue.