MariaDB vs PostgreSQL pour l’hébergement multi‑locataire : empêcher un site client de tout tuer

Cet article vous a aidé ?

L’hébergement multi‑locataire, c’est l’optimisme qui finit par être audité. Vous vendez des sites « illimités », un client installe un plugin qui transforme chaque page vue en un scan de table complète, et soudain votre file d’assistance ressemble à un rapport de déni de service.

Le problème n’est pas que MariaDB ou PostgreSQL soit « lent ». Le problème est que les bases partagées sont un bien commun, et qu’un locataire peut absolument tout dégrader — CPU, E/S, verrous, connexions, espace temporaire, autovacuum/undo history, vous nommez —. Votre travail est de faire en sorte que le rayon d’impact d’un mauvais comportement soit petit, diagnostiquable et ennuyeux.

Ce qui fait vraiment tomber tout le monde dans les bases de données multi‑locataires

Une « panne » multi‑locataire est rarement un seul bug. C’est généralement un locataire qui déclenche une limite partagée et le système s’effondre ensuite poliment pour tout le monde.

Les suspects habituels (par ordre de production)

  • Épuisement des connexions : l’application évolue en « ouvrant une autre connexion », et votre base évolue en « tombant en panne ».
  • CPU incontrôlé : prédicats sans index, jointures pathologiques, filtrage JSON sans index GIN/fonctionnel, et crimes générés par l’ORM.
  • Saturation des E/S : gros tris sur disque, tables temporaires, tempêtes de checkpoints, et « exportons tout le locataire toutes les heures ».
  • Contention sur les verrous : un locataire effectue de grosses migrations à midi ; tous les autres attendent, réessaient, puis s’empilent.
  • Dette de maintenance : autovacuum PostgreSQL à la traîne, ou longueur de l’historique InnoDB qui croît jusqu’à ce que tout commence à payer des intérêts.
  • Événements disque plein : fichiers temporaires, journaux binaires/WAL, croissance incontrôlée des tables — rien ne rend les bases de données plus honnêtes qu’un système de fichiers à 100 %.

Dans l’hébergement multi‑locataire, le schéma « un site tue tout le monde » n’est pas mystérieux. Il est prévisible. Ce qui est bien, car vous pouvez en concevoir la défense.

Blague n°1 : Le cloud n’est que l’ordinateur de quelqu’un d’autre, et l’hébergement multi‑locataire, c’est cet ordinateur avec votre alerte attachée.

MariaDB vs PostgreSQL : la décision qui compte

Les gens comparent MariaDB et PostgreSQL comme s’il s’agissait d’une checklist de fonctionnalités. En hébergement multi‑locataire, la vraie question est : quel système vous donne le plus de leviers efficaces pour isoler les locataires et diagnostiquer rapidement le voisin bruyant ?

Résumé d’opinion

  • Si vous pouvez standardiser un pooler et appliquer des timeouts, PostgreSQL est généralement la plateforme multi‑locataire la plus propre. Il offre de meilleurs primitives pour la gouvernance des requêtes et le contrôle par rôle, et son histoire d’introspection est excellente.
  • Si vous opérez au niveau « hébergement LAMP classique » avec beaucoup de petits locataires et une forte expérience opérationnelle MySQL/MariaDB, MariaDB peut être parfaitement solide — si vous prenez les limites de ressources au sérieux et cessez de prétendre que « max_connections=5000 » est un plan.
  • Si vous n’êtes pas prêt à opérationnaliser des garde‑fous, aucune des deux bases ne vous sauvera. La base n’est pas une stratégie parentale.

Où PostgreSQL a tendance à gagner pour la contention des locataires

  • Modèle de rôles de première classe pour limiter les privilèges ; facile de mapper « rôle locataire » à des politiques.
  • Row Level Security (RLS) permet le modèle ligne‑par‑locataire d’une manière exécutable au niveau base de données, pas « on jure que l’app filtre toujours tenant_id ».
  • Observabilité puissante : pg_stat_statements, pg_locks, pg_stat_activity, auto_explain, et moins de moments « que fait‑il ? ».
  • Timeouts de requête et timeouts de verrou sont simples et peuvent être appliqués par rôle ou par base.

Où MariaDB a tendance à gagner (ou à faire moins mal)

  • Familiarité opérationnelle dans les environnements d’hébergement : de nombreuses équipes ont déjà des outils autour de MariaDB.
  • Limites par utilisateur existent et sont simples à appliquer (MAX_QUERIES_PER_HOUR, MAX_USER_CONNECTIONS, etc.). Ce sont des outils rugueux, mais les outils rugueux sont parfois les seuls utilisés.
  • Écosystème de réplication peut être simple pour le scaling en lecture dans les patterns d’hébergement courants (avec réserves concernant la cohérence).

Ce qu’il ne faut pas faire

Ne choisissez pas une base parce qu’on vous a dit qu’elle est « plus rapide ». En hébergement multi‑locataire, vous ne faites pas de tuning pour un benchmark. Vous faites du tuning pour le pire client au pire moment.

Modèles d’isolation : base-par-locataire, schéma-par-locataire, ligne-par-locataire

Avant de débattre MariaDB vs PostgreSQL, décidez ce que vous isolez : les données, la performance, le rayon opérationnel d’impact, ou les limites de conformité. Vous n’obtenez pas les quatre gratuitement.

Modèle A : base‑par‑locataire (isolation forte, opérations lourdes)

C’est le classique « chaque client a sa propre base ». C’est excellent pour la containment : le bloat, les verrous et les migrations d’un locataire impactent moins les autres.

  • Avantages : Rayon d’impact clair, sauvegardes/restauration plus simples par locataire, facilité pour déplacer un locataire sur un autre hôte, suppression/retention GDPR plus simple.
  • Inconvénients : Plus d’objets, plus de connexions, plus de travail en arrière‑plan, plus de surface de monitoring. Vous avez besoin d’automatisation sinon vous coulez.
  • PostgreSQL : Fonctionne bien, mais surveillez le nombre global de connexions et le coût autovacuum sur de nombreuses bases.
  • MariaDB : Fonctionne aussi ; schéma/base sont étroitement liés. Surveillez le cache de tables et les limites des fichiers ouverts.

Modèle B : schéma‑par‑locataire (compromis, toujours délicat)

Une base, plusieurs schémas (ou bases MariaDB comme namespaces). Vous obtenez une séparation organisationnelle, mais l’isolation de performance est faible sauf si vous ajoutez des garde‑fous.

  • Avantages : Moins d’objets côté serveur que DB‑per‑tenant, reporting cross‑tenant parfois plus simple.
  • Inconvénients : Pool de buffers partagé, WAL/binlog partagé, espace temporaire partagé. Les DDL peuvent toujours causer des douleurs.
  • Meilleure utilisation : Comptes modérés de locataires avec charges similaires et forte ownership SRE.

Modèle C : ligne‑par‑locataire (densité maximale, indulgence minimale)

Un seul schéma, tables partagées, tenant_id dans chaque ligne. C’est efficace. C’est aussi là où un seul index manquant devient un cauchemar d’hébergement partagé.

  • Avantages : Simplifie le déploiement de changements de schéma ; moins d’objets ; excellent pour le SaaS avec modèle cohérent.
  • Inconvénients : Le risque de voisin bruyant est le plus élevé. Sauvegarde/restauration par locataire est plus difficile. Il faut une discipline stricte des requêtes.
  • PostgreSQL : RLS peut rendre cela « relativement sûr » du point de vue de l’accès aux données.
  • MariaDB : Vous dépendez de la correction de l’application pour les filtres par locataire à moins d’implémenter des vues/procédures élaborées (que la plupart des équipes ne maintiennent pas correctement dans le temps).

Vérification réaliste de l’isolation

« Isolation des locataires » n’est pas seulement des permissions. C’est contrôler les ressources partagées : temps CPU, E/S, mémoire, verrous et connexions. Si votre modèle ne vous permet pas de limiter ces éléments, vous n’avez pas d’isolation — vous avez de l’espoir.

Limites strictes et garde‑fous (la couche « vous ne pouvez pas faire ça »)

Les garde‑fous sont ce que vous mettez en place avant d’en avoir besoin. Après qu’un locataire ait fondu le cluster, vous les ajouterez de toute façon — juste sous stress, avec des clients qui hurlent. Faites‑le maintenant.

Garde‑fous PostgreSQL qui fonctionnent réellement

  • statement_timeout par rôle ou par base : tue les requêtes incontrôlées.
  • lock_timeout : évite les files d’attente « attendre pour toujours ».
  • idle_in_transaction_session_timeout : stoppe les connexions qui gardent des verrous sans rien faire.
  • work_mem par rôle : empêche le tri d’un locataire de manger toute la RAM (ou de provoquer de gros déversements temporaires).
  • paramètres par rôle via ALTER ROLE … SET : rend la politique applicable.

Garde‑fous MariaDB sous‑estimés

  • Limites par utilisateur (MAX_USER_CONNECTIONS, MAX_QUERIES_PER_HOUR) : grossières, efficaces.
  • max_execution_time (pour SELECT) : peut tuer les lectures longues si utilisé prudemment.
  • Paramètres InnoDB qui réduisent les dégâts des rafales (ex. taille saine des fichiers de log, dimensionnement du buffer pool, comportement de flush).

Ce que ni l’une ni l’autre ne vous donne nativement

Aucune des deux ne fera parfaitement respecter « un locataire obtient 10 % du CPU et 50 IOPS » au niveau SQL. Si vous avez besoin d’un tel niveau de multi‑tenancy strict, vous finirez par utiliser :

  • Instances séparées par locataire (ou par niveau de locataire).
  • Contrôles au niveau OS (cgroups) pour plafonner les ressources du processus de base — utile, mais grossier.
  • Mise en file d’attente côté application ou pooler : vous ne pouvez pas exécuter ce que vous n’acceptez pas.

Tempêtes de connexions : pooling, plafonds et mise en file d’attente

La façon la plus rapide pour qu’un seul locataire tue tout le monde est le churn de connexions. PHP‑FPM qui scale, workers Node qui forment des forks, cron qui s’empilent, et soudain la base passe sa vie à établir des connexions et à faire du context switching.

PostgreSQL : vous voulez presque toujours un pooler

PostgreSQL gère bien la concurrence, mais des milliers de processus backend actifs ont un coût. En hébergement multi‑locataire avec trafic en rafales, vous voulez typiquement PgBouncer (pooling transactionnel pour la plupart des apps web), avec des plafonds par locataire si vous pouvez mapper proprement utilisateurs/rôles.

MariaDB : le pooling existe, mais soyez honnête sur son emplacement

MariaDB peut accepter beaucoup de connexions, mais ne confondez pas « accepter des connexions TCP » et « bien fonctionner ». La stratégie pratique est souvent :

  • Plafonner les connexions par utilisateur.
  • Utiliser le pooling côté application (selon la stack).
  • Garder max_connections réaliste et concevoir pour la rétropression.

Blague n°2 : Si votre stratégie de montée en charge est « augmenter max_connections », vous n’avez pas une base de données ; vous avez une file d’attente très chère.

La vérité gênante sur l’équité

Sans contrôles d’équité explicites, le locataire le plus bruyant gagne. La base ne sait pas ce qu’est « un client ». Elle connaît les sessions. Vous avez besoin d’un mapping : locataire → rôle/utilisateur → limites. Si vous ne pouvez pas le mapper, vous ne pouvez pas l’appliquer.

E/S et stockage : empêcher le disque d’être la tragédie partagée

Dans les systèmes multi‑locataires, les disques échouent d’une manière très spécifique : pas en tombant en panne, mais en étant occupés. Vos CPUs vont bien, votre RAM va bien, et votre base est « lente » parce qu’elle attend le stockage. Alors tous les locataires expirent et réessaient, et vous obtenez une tempête de réessais qui rend le problème initial presque anecdotique.

Schémas de containment qui fonctionnent

  • Séparer WAL/binlog des données quand c’est possible. Ce n’est pas magique, mais cela réduit la contention et rend les événements disque‑plein moins catastrophiques.
  • Jeux de données/volumes par locataire (lorsque faisable) : si vous faites DB‑per‑tenant, placer chaque base de locataire sur son dataset permet d’appliquer des quotas et de détecter la croissance.
  • Surveillance de l’espace temporaire : tris et tables temporaires sont les mangeurs discrets de disque.
  • Tuning des checkpoints et flush : PostgreSQL et InnoDB peuvent générer des rafales d’écritures ; en multi‑tenant, ces rafales se corrèlent aux pics de chargement des pages, ce qui provoque « tous les sites sont lents ».

Prise d’ingénierie du stockage : ne partagez pas sans télémétrie

Si vous ne pouvez pas attribuer les E/S à un locataire (ou au moins à une base/utilisateur), vous combattez des fantômes. Au minimum, attribuez aux requêtes (pg_stat_statements / Performance Schema) et aux bases.

Observabilité : prouver qui est bruyant, rapidement

En hébergement, « quel locataire l’a fait ? » n’est pas une question philosophique. C’est une compétence vitale pour les astreintes. Vous avez besoin de :

  • Requêtes principales par temps total, temps moyen, nombre d’appels et lignes traitées.
  • Graphes de verrous : qui bloque qui.
  • Comptes de connexions par mapping locataire (utilisateur/rôle/base).
  • Croissance disque/WAL/binlog en tendance.
  • Retard de maintenance : délais autovacuum, longueur de la liste d’historique InnoDB, etc.

Une citation à garder sur un post‑it : de John Allspaw : « You can’t improve what you don’t measure. »

Faits intéressants et contexte historique (parce que ça façonne les compromis d’aujourd’hui)

  1. PostgreSQL descend du projet POSTGRES à l’UC Berkeley, conçu avec extensibilité bien avant que les « extensions » deviennent à la mode.
  2. MySQL est devenu le choix par défaut des premiers hébergements web parce qu’il était facile à déployer et largement suffisant pour les charges de l’époque — beaucoup de lectures, schémas simples, petites données.
  3. MariaDB a été forké depuis MySQL après l’acquisition de Sun par Oracle ; le fork concernait autant la gouvernance que la technologie.
  4. Le modèle MVCC de PostgreSQL rend les lectures non bloquantes par conception, mais il impose une exigence de maintenance : vacuumer les tuples morts n’est pas optionnel.
  5. InnoDB (moteur dominant pour MySQL/MariaDB) a apporté les transactions et le verrouillage au niveau ligne dans l’écosystème MySQL et a changé la fiabilité d’hébergement dans les années 2000.
  6. PgBouncer est devenu un standard de fait dans les environnements Postgres à haute concurrence parce que le modèle process‑par‑connexion a un coût réel à l’échelle.
  7. Row Level Security dans PostgreSQL est devenue une fonctionnalité multi‑tenant grand public seulement après que les équipes en eurent assez de « on promet que l’app ajoute toujours tenant_id ».
  8. Les limites par utilisateur de MySQL/MariaDB existent depuis longtemps, mais de nombreuses stacks d’hébergement les ignoraient parce que « ça pourrait casser un client », jusqu’à ce que des clients cassent tout le monde.

Tâches pratiques : commandes, sorties, décisions (12+)

Voici le type de vérifications à exécuter pour garder l’hébergement multi‑locataire stable. Chaque tâche inclut : commande, sortie réaliste, ce que ça signifie, et la décision à prendre.

Tâche 1 : Trouver les requêtes PostgreSQL principales par temps total (pg_stat_statements)

cr0x@server:~$ sudo -u postgres psql -d postgres -c "SELECT queryid, calls, round(total_exec_time)::int AS total_ms, round(mean_exec_time,2) AS mean_ms, left(query,120) AS sample FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;"
 queryid  | calls | total_ms | mean_ms |                         sample
----------+-------+----------+---------+----------------------------------------------------------
 99120311 |  2140 |   987654 |  461.52 | SELECT * FROM orders WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2
 77112002 |   120 |   501223 | 4176.86 | SELECT count(*) FROM events WHERE tenant_id = $1 AND payload::text ILIKE $2
 55110019 | 98000 |   210112 |    2.14 | SELECT id FROM sessions WHERE expires_at < now()
 44110070 |    42 |   180990 | 4309.29 | SELECT * FROM invoices WHERE status IN (...) AND tenant_id=$1 ORDER BY id
 99120355 |   300 |   150333 |  501.11 | UPDATE products SET stock = stock - 1 WHERE tenant_id=$1 AND id=$2
(5 rows)

Sens : La deuxième requête est un exemple classique de tueur de locataire (ILIKE sur payload text). Temps moyen élevé, moins d’appels, gros impact.

Décision : Identifier le locataire via les logs/app ou paramètres de requête, ajouter un index ou redesigner (ex. GIN/trigram, colonne extraite), et définir statement_timeout par rôle pour limiter les dégâts.

Tâche 2 : Voir les sessions PostgreSQL et qui attend

cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, usename, datname, state, wait_event_type, wait_event, now()-query_start AS age, left(query,80) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC LIMIT 10;"
 pid  |  usename   |  datname  | state  | wait_event_type | wait_event |   age    |                                         q
------+------------+-----------+--------+-----------------+------------+----------+--------------------------------------------------------------------------------
 8123 | tenant_442 | appdb     | active | Lock            | relation   | 00:03:14 | ALTER TABLE posts ADD COLUMN foo text
 8201 | tenant_101 | appdb     | active |                 |            | 00:02:59 | SELECT * FROM orders WHERE tenant_id=$1 ORDER BY created_at DESC LIMIT $2
 8210 | tenant_333 | appdb     | active | Lock            | tuple      | 00:02:10 | UPDATE posts SET status='x' WHERE tenant_id=$1 AND id=$2
(3 rows)

Sens : Un locataire exécute du DDL et retient des verrous ; d’autres locataires attendent sur des verrous relation/tuple.

Décision : Tuer ou reporter le DDL, appliquer lock_timeout pour les rôles locataires, et mettre en place une politique de « fenêtre de maintenance » pour les migrations.

Tâche 3 : Détecter les bloqueurs de verrous PostgreSQL

cr0x@server:~$ sudo -u postgres psql -c "SELECT blocked.pid AS blocked_pid, blocked.usename AS blocked_user, blocker.pid AS blocker_pid, blocker.usename AS blocker_user, left(blocker.query,80) AS blocker_query FROM pg_locks bl JOIN pg_stat_activity blocked ON blocked.pid=bl.pid JOIN pg_locks kl ON bl.locktype=kl.locktype AND bl.database IS NOT DISTINCT FROM kl.database AND bl.relation IS NOT DISTINCT FROM kl.relation AND bl.page IS NOT DISTINCT FROM kl.page AND bl.tuple IS NOT DISTINCT FROM kl.tuple AND bl.virtualxid IS NOT DISTINCT FROM kl.virtualxid AND bl.transactionid IS NOT DISTINCT FROM kl.transactionid AND bl.classid IS NOT DISTINCT FROM kl.classid AND bl.objid IS NOT DISTINCT FROM kl.objid AND bl.objsubid IS NOT DISTINCT FROM kl.objsubid AND kl.granted JOIN pg_stat_activity blocker ON blocker.pid=kl.pid WHERE NOT bl.granted LIMIT 5;"
 blocked_pid | blocked_user | blocker_pid | blocker_user |                         blocker_query
-------------+--------------+-------------+--------------+---------------------------------------------------------------
        8210 | tenant_333   |        8123 | tenant_442   | ALTER TABLE posts ADD COLUMN foo text
(1 row)

Sens : Vous avez un bloqueur clair. Pas de conjectures, pas d’ambiance.

Décision : Terminer la session bloquante si elle viole la politique ; ajouter des contrôles DDL dans le pipeline de déploiement.

Tâche 4 : Vérifier les timeouts PostgreSQL pour les rôles locataires

cr0x@server:~$ sudo -u postgres psql -c "SELECT rolname, rolconfig FROM pg_roles WHERE rolname IN ('tenant_333','tenant_442');"
  rolname   |                        rolconfig
------------+----------------------------------------------------------
 tenant_333 | {statement_timeout=30000,lock_timeout=2000,work_mem=16MB}
 tenant_442 | {statement_timeout=30000,lock_timeout=2000,work_mem=16MB}
(2 rows)

Sens : La politique est encodée au niveau du rôle de base de données, pas dans un wiki interne que personne ne lit.

Décision : Si absent, ajouter des defaults par rôle ; si trop strict, créer des paliers (petit/moyen/entreprise).

Tâche 5 : Vérifier la santé de l’autovacuum PostgreSQL

cr0x@server:~$ sudo -u postgres psql -c "SELECT relname, n_dead_tup, last_autovacuum, last_vacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
  relname  | n_dead_tup |        last_autovacuum        |          last_vacuum
-----------+------------+-------------------------------+-------------------------------
 events    |    5021132 |                               |
 sessions  |     901223 | 2025-12-29 08:11:02.12345+00  |
 orders    |     200111 | 2025-12-29 08:09:10.99431+00  |
 invoices  |     150991 | 2025-12-29 07:59:34.33121+00  |
 posts     |      90112 | 2025-12-29 08:00:20.11200+00  |
(5 rows)

Sens : La table events a des millions de tuples morts et aucun autovacuum récent. C’est une mine de performance.

Décision : Investiguer pourquoi autovacuum ne tourne pas (seuils, risque de wraparound, transactions longues). Envisager un tuning autovacuum par table ou du partitionnement par locataire/temps.

Tâche 6 : MariaDB : identifier les requêtes principales (digest Performance Schema)

cr0x@server:~$ sudo mariadb -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1000000000000,2) AS total_s FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT COUNT ( * ) FROM events WHERE tenant_id = ? AND payload LIKE ?
COUNT_STAR: 118
total_s: 632.11
*************************** 2. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 9801
total_s: 201.44
*************************** 3. row ***************************
DIGEST_TEXT: UPDATE products SET stock = stock - ? WHERE tenant_id = ? AND id = ?
COUNT_STAR: 3002
total_s: 95.12

Sens : Vous pouvez voir les « formes de requête » même si les valeurs littérales diffèrent. Le COUNT avec LIKE est l’agresseur.

Décision : Ajouter des index appropriés, limiter le temps d’exécution, ou réécrire la fonctionnalité. Si le locataire insiste, le déplacer sur sa propre instance.

Tâche 7 : MariaDB : trouver les threads actifs et qui bouffe tout

cr0x@server:~$ sudo mariadb -e "SHOW FULL PROCESSLIST;"
Id	User	Host	db	Command	Time	State	Info
9112	tenant442	app1:44210	appdb	Query	210	Waiting for table metadata lock	ALTER TABLE posts ADD COLUMN foo TEXT
9201	tenant101	app2:51012	appdb	Query	45	Sending data	SELECT * FROM orders WHERE tenant_id=101 ORDER BY created_at DESC LIMIT 50
9303	tenant333	app3:39910	appdb	Query	39	Locked	UPDATE posts SET status='x' WHERE tenant_id=333 AND id=9

Sens : Même histoire que Postgres : DDL provoquant des attentes de verrouillage de métadonnées et des empilements.

Décision : Tuer le DDL, appliquer des outils/politiques de changement de schéma en ligne, et planifier les migrations.

Tâche 8 : MariaDB : appliquer des limites de connexions par utilisateur

cr0x@server:~$ sudo mariadb -e "ALTER USER 'tenant442'@'%' WITH MAX_USER_CONNECTIONS 20;"
cr0x@server:~$ sudo mariadb -e "SHOW GRANTS FOR 'tenant442'@'%';"
Grants for tenant442@%
GRANT USAGE ON *.* TO `tenant442`@`%` WITH MAX_USER_CONNECTIONS 20

Sens : Le locataire 442 ne peut plus ouvrir 500 connexions. Ils feront la file au niveau de l’app plutôt que dans votre base.

Décision : Définir des limites par paliers ; surveiller les erreurs « trop de connexions » et fournir des recommandations de pooling.

Tâche 9 : PostgreSQL : voir la consommation de connexions par base

cr0x@server:~$ sudo -u postgres psql -c "SELECT datname, count(*) AS conns FROM pg_stat_activity GROUP BY datname ORDER BY conns DESC;"
 datname | conns
---------+-------
 appdb   |   312
 postgres|     3
(2 rows)

Sens : La plus grande pression est sur appdb. Pas choquant, mais maintenant vous savez.

Décision : Si appdb est multi‑locataire, envisagez de séparer les gros locataires vers des bases ou clusters séparés.

Tâche 10 : Vérifier l’attente I/O système et les principaux coupables

cr0x@server:~$ iostat -xz 1 3
Linux 6.8.0 (db01) 	12/29/2025 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.10    0.00    5.20   32.40    0.00   50.30

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         220.0  18432.0     2.0   0.90    8.20    83.78   600.0  51200.0   120.0  16.67   25.10    85.33   17.3  99.20

Sens : 32 % d’iowait et le NVMe est proche de 100 % d’utilisation avec un fort w_await. C’est un goulet de stockage maintenant.

Décision : Identifier pourquoi les écritures sont en pic (checkpoint, vacuum, binlog/WAL, déversements temporaires). Envisager d’ajuster le tuning des checkpoints/flush et de garantir une marge IOPS suffisante.

Tâche 11 : PostgreSQL : vérifier le taux de génération WAL pour symptômes

cr0x@server:~$ sudo -u postgres psql -c "SELECT now(), pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_bytes_since_start;"
              now              | wal_bytes_since_start
------------------------------+------------------------
 2025-12-29 08:22:10.1122+00  | 842 GB
(1 row)

Sens : Ce n’est pas « mauvais » en soi, mais si le WAL croît vite et que les réplicas accusent du retard, vous allez subir une pression disque.

Décision : Vérifier le retard de réplication et l’archivage ; si un locataire écrit en masse, le limiter ou l’isoler.

Tâche 12 : MariaDB : vérifier l’état du moteur InnoDB pour la contention et l’historique

cr0x@server:~$ sudo mariadb -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,70p'
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-29 08:23:01 0x7f3c6c1fe700 INNODB MONITOR OUTPUT
=====================================
...
History list length 412398
...
Trx id counter 928331120
Purge done for trx's n:o < 928300000 undo n:o < 0 state: running
...

Sens : Une longueur importante de la history list peut indiquer un retard de purge, souvent dû à des transactions longues. Cela peut dégrader les performances pour tous les locataires.

Décision : Trouver et tuer les transactions longues ; ajuster le comportement de l’application ; s’assurer que la purge peut suivre.

Tâche 13 : PostgreSQL : trouver les transactions longues (bloqueurs de vacuum)

cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, usename, now()-xact_start AS xact_age, state, left(query,80) AS q FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 5;"
 pid  |  usename   |  xact_age  |        state        |                                         q
------+------------+------------+---------------------+--------------------------------------------------------------------------------
 9001 | tenant_777 | 02:14:09   | idle in transaction | SELECT * FROM reports WHERE tenant_id=$1
(1 row)

Sens : « idle in transaction » tue le vacuum et est un risque de verrou. En multi‑tenant, c’est une taxe partagée.

Décision : Appliquer idle_in_transaction_session_timeout pour les rôles locataires ; corriger le chemin applicatif qui oublie de commit/rollback.

Tâche 14 : Vérifier l’utilisation disque et localiser les zones en forte croissance

cr0x@server:~$ df -h /var/lib/postgresql /var/lib/mysql
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  900G  812G   43G  95% /var/lib/postgresql
/dev/nvme1n1p1  900G  620G  235G  73% /var/lib/mysql

Sens : Le volume Postgres est à 95 %. Vous êtes à un pic de WAL ou de fichiers temporaires d’un incident très pédagogique.

Décision : Nettoyage d’urgence (vérifier la rétention/archivage WAL, chasser les fichiers temporaires), puis quotas/alertes permanents. Aussi : ne laissez pas un cluster tourner à 95 % en permanence.

Feuille de route de diagnostic rapide

Voici la séquence « quelqu’un vient d’alerter et tous les locataires hurlent ». L’objectif n’est pas l’élégance. L’objectif est de trouver le goulot d’étranglement en quelques minutes.

Première étape : décider si c’est CPU, E/S, verrous ou connexions

  1. Saturation CPU ? Vérifier la charge système et le CPU user/system.
    • Si le CPU est élevé et iowait faible : suspecter des requêtes lentes, index manquants, tris/jointures coûteux.
  2. Attente I/O élevée ? Si iowait élevé et %util disque proche de 100 % : suspecter tempêtes de checkpoint/flush, déversements temporaires, churn WAL/binlog, retard de vacuum/purge.
  3. Accumulation de verrous ? Chercher des sessions en attente sur des verrous (pg_stat_activity wait_event_type=Lock ; MariaDB processlist « Locked » / metadata locks).
  4. Épuisement des connexions ? Vérifier les connexions actives vs max configuré ; chercher des erreurs « too many connections » et une explosion de threads/processus.

Deuxième étape : identifier le principal coupable et le rayon d’impact

  1. Requêtes principales (pg_stat_statements / Performance Schema digests) : trouver la forme de requête consommant le plus de temps total.
  2. Mapper au locataire : via rôle/utilisateur, base, tags applicatifs, ou corrélation de logs.
  3. Confirmer l’impact : cause‑t‑il des verrous, du churn E/S ou une charge CPU ?

Troisième étape : appliquer la moindre mauvaise atténuation

  • Tuer la requête/session si elle nuit activement aux autres et viole la politique.
  • Limiter à la périphérie : plafonner les connexions par locataire ; réduire la concurrence dans le pooler ; limiter le débit des endpoints lourds.
  • Abaisser temporairement l’impact : réduire les statement_timeout pour le rôle bruyant, pas globalement.
  • Stabiliser le stockage : si le disque est le goulot, réduire la pression d’écriture (reporter vacuum full/reindex, tuner les checkpoints prudemment, arrêter les jobs en masse).

Si vous exécutez ces étapes de manière cohérente, le « meltdown multi‑tenant » devient un diagnostic répétable plutôt qu’une séance d’exorcisme collective.

Erreurs courantes : symptôme → cause racine → correctif

1) Symptom : « Tous les sites sont lents » et le CPU de la base est correct

Cause racine : Saturation du stockage (déversements temporaires, rafales de checkpoints, backlog WAL/binlog).

Correctif : Vérifier iowait et %util disque ; plafonner work_mem/tailles de tri ; tuner les checkpoints ; séparer WAL/binlog ; ajouter de la marge IOPS.

2) Symptom : Timeouts aléatoires, beaucoup de « waiting for lock »

Cause racine : DDL locataire ou transactions longues provoquant des files d’attente de verrous.

Correctif : Appliquer lock_timeout et idle‑in‑transaction timeout ; exiger des outils de changement de schéma en ligne ; planifier les migrations ; tuer vite les bloqueurs.

3) Symptom : Périodiquement « trop de connexions » puis récupération

Cause racine : Tempêtes de connexions dues à la montée en charge de l’app ou à des crons ; absence de pooling/retropression.

Correctif : Déployer PgBouncer (Postgres) ou pooling applicatif ; plafonner les connexions par locataire (limites MariaDB, limites basées rôle Postgres) ; réduire le nombre de workers.

4) Symptom : Les requêtes Postgres ralentissent sur plusieurs jours, puis « vacuum ne suit pas »

Cause racine : Autovacuum à la traîne à cause de transactions longues, seuils inadaptés, ou tables à fort churn (souvent partagées multi‑tenant).

Correctif : Trouver les transactions longues ; tuner autovacuum par table ; partitionner les tables à fort churn ; donner des ressources au vacuum ; réduire les causes de bloat.

5) Symptom : Performance MariaDB qui se dégrade avec beaucoup d’écritures ; problèmes d’undo/purge

Cause racine : Transactions longues et retard de purge (history list length qui augmente).

Correctif : Identifier les transactions longues ; raccourcir les scopes transactionnels ; tuer les coupables ; s’assurer que la purge progresse ; revoir niveaux d’isolation et tailles de batch.

6) Symptom : « C’est seulement le rapport d’un locataire » mais il détruit tout

Cause racine : Requête analytique lourde sur le primaire, pas de séparation des ressources, pas de réplique de lecture, pas de timeout.

Correctif : Router le reporting vers des réplicas/entrepôt ; appliquer statement_timeout ; créer un palier locataire avec ressources dédiées.

7) Symptom : Disque plein, base plante ou passe en lecture seule, panique

Cause racine : Croissance WAL/binlog, fichiers temporaires incontrôlés, ou données locataires sans limite.

Correctif : Appliquer des quotas (si possible), surveiller la croissance, maintenir une politique d’espace libre (pas optionnelle), et limiter la rétention. Considérez disque‑plein comme une erreur de conception, pas une surprise.

Trois mini‑histoires d’entreprise issues du terrain

Mini‑histoire 1 : L’incident causé par une mauvaise hypothèse

Ils exploitaient un cluster PostgreSQL partagé pour une flotte de petits sites clients. Chaque locataire avait son propre utilisateur de base, mais tous partageaient la même base et le même schéma. L’équipe produit jurait que l’application filtrerait toujours par tenant_id. L’équipe SRE les croyait, parce que les logs semblaient propres et que tout le monde voulait en finir avec le sprint.

Un nouveau client a intégré un « plugin d’analytics » qui ajoutait une recherche sur les événements historiques. Le plugin utilisait un générateur de requêtes dynamique, et dans un chemin de code il a oublié d’inclure tenant_id dans le WHERE. Pas toujours. Juste sur certaines combinaisons de filtres. Ce qui rendait difficile la détection en staging ; staging n’avait jamais assez de données pour que cela nuise.

En production, la requête est passée de « scanner quelques milliers de lignes pour un locataire » à « scanner quelques centaines de millions de lignes pour tous les locataires ». PostgreSQL a fait ce qu’on lui disait. Le CPU a grimpé, puis les E/S ont explosé parce que le plan a commencé à déverser des tris sur le disque. Pendant ce temps, les requêtes des autres locataires s’empilaient et expiraient, et leur logique de retry augmentait la charge. Le plugin n’a pas seulement créé un risque de fuite de données — il a provoqué un incident de performance à l’échelle de l’hôte.

La correction n’a pas été héroïque. Ils ont implémenté Row Level Security, forcé toutes les requêtes locataires via une fonction SECURITY DEFINER avec contexte locataire, et ajouté statement_timeout pour les rôles locataires. Le changement culturel a été plus grand : « l’app filtre toujours » a cessé d’être une décision d’architecture fiable. C’est devenu une entrée non fiable.

Mini‑histoire 2 : L’optimisation qui a mal tourné

Une plateforme d’hébergement avait standardisé sur MariaDB. Un trimestre, quelqu’un a décidé « d’optimiser » en augmentant significativement max_connections parce que les clients voyaient parfois des erreurs de connexion lors de pics. L’idée était simple : moins d’erreurs = clients plus heureux.

Pendant deux jours ça a paru fonctionner. Puis une campagne marketing a frappé le site d’un locataire. Leurs workers PHP ont monté en charge. Chaque worker a ouvert sa propre connexion, a exécuté une requête lente, et est resté à attendre les E/S. Plus de connexions a signifié plus de travail lent concurrent. Le churn du buffer pool a augmenté. InnoDB a commencé à flusher plus fort. La latence a augmenté partout. Les autres locataires ont réessayé, ce qui a créé plus de connexions, ce qui a généré plus de flush. Tempête auto‑infligée classique.

Quand ils ont finalement réduit max_connections, le symptôme immédiat s’est amélioré mais l’équipe a tiré la mauvaise leçon : « max_connections doit être réglé ». La véritable leçon était « vous avez besoin de rétropression et de pooling ». Ils ont ajouté des plafonds de connexions par utilisateur, corrigé la requête fautive avec un index, et implémenté un petit pool de connexions côté application pour leur stack managée.

La conclusion du postmortem était sèche mais exacte : permettre plus de concurrence ne réduit pas la demande ; cela augmente la capacité du système à subir plus de demande simultanément.

Mini‑histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une autre entreprise exploitait PostgreSQL pour un SaaS multi‑tenant. Ils n’étaient pas extravagants. Ils avaient des politiques : timeouts par rôle, PgBouncer, et fenêtres de migration strictes. Chaque rôle locataire avait un statement_timeout et un lock_timeout par défaut, et les migrations de production étaient exécutées par un rôle « deploy » séparé avec des droits élevés et une fenêtre de changement.

Un vendredi, un locataire a poussé une nouvelle version de son intégration qui a accidentellement effectué un gros UPDATE dans une seule transaction, touchant une table chaude. La requête était valide. Elle était aussi désastreuse à ce moment‑là de la journée. Le statement_timeout l’a tuée automatiquement. L’application a réessayé quelques fois puis a rétrogradé parce que l’intégration avait une logique de retry raisonnable. La base est restée en vie. Les autres locataires ont à peine remarqué.

L’ingénieur d’astreinte a vu les erreurs, identifié rapidement le locataire via les logs basés sur les rôles, et a coordonné une correction avec le support. Personne n’a été tiré d’un sommeil pour un all‑hands. Personne n’a « heroicament » scalé le cluster. Le système s’est comporté comme un système doté de garde‑fous.

Ce n’était pas glamour. C’est le but. Le meilleur travail SRE ressemble à rien ne s’est passé.

Listes de contrôle / plan étape par étape

Étape par étape : durcir une base partagée contre les locataires bruyants

  1. Choisir le modèle d’isolation
    • Si les locataires peuvent exécuter des plugins/requêtes imprévisibles : préférer base‑par‑locataire ou instance‑par‑palier.
    • Si vous contrôlez toutes les requêtes et voulez de la densité : ligne‑par‑locataire avec RLS PostgreSQL est viable.
  2. Appliquer le mapping d’identité
    • Créer un rôle/utilisateur DB distinct par locataire (ou par palier) pour appliquer des limites et attribuer le comportement.
  3. Définir des timeouts de requête par défaut
    • PostgreSQL : statement_timeout, lock_timeout, idle_in_transaction_session_timeout.
    • MariaDB : utiliser des limites par utilisateur ; envisager max_execution_time pour les SELECT si pertinent.
  4. Mettre en place le contrôle des connexions
    • PostgreSQL : PgBouncer, plus dimensionnement des pools ; éviter des milliers de backends.
    • MariaDB : plafonner MAX_USER_CONNECTIONS et garder max_connections réaliste.
  5. Établir une règle « pas de DDL à midi »
    • Exiger les migrations dans une fenêtre ; préférer des patterns de changement de schéma en ligne.
  6. Rendre le stockage ennuyeux
    • Alerter tôt sur l’utilisation disque (ex. 70/80/90 %).
    • Séparer WAL/binlog si possible ; surveiller l’usage temporaire.
  7. Barre minimale d’observabilité
    • Formes de requêtes principales, attentes de verrous, compte de sessions par locataire, et santé de maintenance (vacuum/purge).
  8. Créer des voies d’escalade
    • Quand un locataire cause à répétition des incidents, migrez‑le vers un palier supérieur avec ressources dédiées — ou imposez des caps plus stricts. Ne négociez pas avec la physique.

Checklist : lors de l’ajout d’un nouveau locataire

  • Créer un rôle/utilisateur locataire avec des defaults appropriés (timeouts, work_mem, etc.).
  • Définir un cap de connexions et confirmer le comportement de pooling.
  • Confirmer le processus de sauvegarde/restauration pour le modèle choisi.
  • Activer le logging/attribution des requêtes pour identifier rapidement le locataire en cas d’incident.
  • Valider que les endpoints dangereux (exports, reporting, mises à jour massives) sont limités ou déportés.

Checklist : quand un locataire veut « un gros rapport »

  • Peut‑on l’exécuter sur une réplique ou un store analytique ?
  • Y a‑t‑il un override de statement_timeout pour le rôle reporting seulement ?
  • La requête est‑elle indexée et testée sur un volume de données proche de la production ?
  • La concurrence est‑elle limitée (un rapport à la fois par locataire) ?

FAQ

1) Dois‑je utiliser une base unique pour tous les locataires ou une base par locataire ?

Si les locataires peuvent exécuter des charges imprévisibles (plugins, reporting personnalisé, migrations), base‑par‑locataire réduit le rayon d’impact. Si vous contrôlez strictement les requêtes et avez besoin de densité, les tables partagées peuvent fonctionner — mais uniquement avec des garde‑fous stricts.

2) PostgreSQL est‑il « meilleur » pour le multi‑tenant ?

PostgreSQL offre généralement une gouvernance au niveau base plus forte (RLS, paramètres par rôle, introspection). Ça ne remplace pas un bon comportement applicatif, mais ça réduit la fréquence à laquelle vous devez lui faire confiance.

3) MariaDB peut‑il faire respecter l’équité entre locataires ?

MariaDB peut plafonner les connexions par utilisateur et certaines activités par utilisateur. Ce n’est pas un ordonnanceur d’équité complet, mais c’est suffisant pour arrêter les pires comportements « ouvrir 500 connexions » et vous acheter de la stabilité.

4) Quelle est la protection la plus efficace contre les voisins bruyants ?

Contrôle des connexions plus timeouts. Plafonner la concurrence par locataire et tuer le travail incontrôlé. Tout le reste (indexation, tuning, partitionnement) est important, mais ces deux mesures arrêtent l’hémorragie.

5) La Row Level Security dans PostgreSQL résout‑elle le multi‑tenancy ?

Elle résout l’application des accès aux données. Elle ne résout pas l’isolation de performance en soi. Un locataire peut toujours exécuter une requête coûteuse qui est « autorisée » mais destructive.

6) Dois‑je mettre les gros locataires sur des réplicas ?

Rendre les analyses et le reporting en lecture sur des réplicas si les exigences de cohérence le permettent. Mais surveillez le lag de réplication et assurez‑vous que l’application ne bascule pas silencieusement sur le primaire quand le lag augmente.

7) Comment empêcher les locataires d’exécuter des changements de schéma pendant les heures de pointe ?

Ne comptez pas sur la politesse. Faites‑le respecter : restreignez les privilèges DDL pour les rôles locataires, routez les migrations via un rôle de déploiement contrôlé, et utilisez des lock_timeouts pour prévenir les empilements.

8) Quel est le moyen le plus rapide pour trouver le locataire responsable ?

Utilisez des utilisateurs/rôles DB par locataire et l’attribution des requêtes. Sans ce mapping, vous devinez à partir d’IP et de logs applicatifs pendant que le cluster brûle.

9) Comment décider quand répartir les locataires sur des instances séparées ?

Séparez lorsque le pattern de charge d’un locataire est fondamentalement différent (écritures lourdes, données massives, reporting constant) ou lorsqu’il déclenche des incidents à répétition. L’isolation coûte moins cher que des pannes répétées.

10) Les contrôles au niveau OS (cgroups) sont‑ils utiles pour les bases de données ?

Ils sont utiles comme filet de sécurité grossier, surtout en déploiements containerisés. Ils ne remplacent pas la gouvernance au niveau requête et connexion, car ils ne comprennent pas les locataires ni le SQL.

Prochaines étapes réalisables cette semaine

  • Mapper l’identité du locataire à l’identité de la base : un rôle/utilisateur par locataire ou par palier. Si vous ne pouvez pas faire ça, votre réponse aux incidents restera coûteuse.
  • Mettre des timeouts en place :
    • PostgreSQL : statement_timeout, lock_timeout, idle_in_transaction_session_timeout par rôle locataire.
    • MariaDB : limites de connexion par utilisateur et contrôles d’exécution quand c’est sûr.
  • Implémenter pooling/rétropression : PgBouncer pour Postgres ; limites réalistes de workers et stratégie de pooling pour les stacks MariaDB.
  • Activer la bonne observabilité : pg_stat_statements ou Performance Schema digest, plus dashboards verrous/sessions.
  • Rédiger votre politique « quand évincer un locataire » : non pas comme punition, mais comme ingénierie. Si un locataire a besoin de ressources dédiées, donnez‑lui un palier adapté à la réalité.

Si vous voulez un principe directeur : concevez pour que le pire locataire ne puisse pas tenir votre entreprise en otage. Les bases de données sont puissantes. Ce ne sont pas des négociateurs.

← Précédent
Planification de la largeur des vdev ZFS : pourquoi plus de disques par vdev a un coût
Suivant →
Réseau hôte Docker : risques, quand l’utiliser et comment limiter les dégâts

Laisser un commentaire