PostgreSQL vs Redis pour les sessions, les limites de débit et les files d’attente

Cet article vous a aidé ?

Les systèmes en production ne tombent pas en panne parce que vous avez choisi « la mauvaise base de données ». Ils tombent en panne parce que vous avez choisi la bonne base pour le mauvais comportement.
Des sessions qui survivent aux déploiements, des limites de débit qui ne doivent pas se réinitialiser au redémarrage, des files qui ne peuvent pas perdre un travail mais non plus bloquer tout le système — ce sont des comportements.

Si vous stockez tout au même endroit parce que c’est pratique, vous le paierez plus tard. Généralement à 2h17 du matin, quand votre « cache simple »
devient votre système d’authentification et que le canal d’incident commence à faire du cardio.

Cadre de décision : choisissez selon le mode de défaillance, pas l’intuition

PostgreSQL et Redis se recoupent juste assez pour vous tenter de faire un choix dogmatique. Résistez. Choisissez en fonction de ce qui doit être vrai
pendant les défaillances : redémarrages, partitions, surcharge, écritures partielles, dérive d’horloge, retries clients et déploiements progressifs.

Une règle tranchante

Si le fait de perdre les données est acceptable (ou reconstructible), Redis est un excellent choix par défaut. Si la perte est inacceptable (ou douloureuse légalement/financièrement),
PostgreSQL est l’adulte dans la pièce.

À quoi sert chaque système en pratique

  • Redis : mémoire partagée rapide avec réseau, opérations atomiques, expirations et plusieurs structures de données. La durabilité est optionnelle et nuancée.
  • PostgreSQL : système transactionnel avec write-ahead logging, contraintes, interrogeabilité et sémantique durable sous conditions bien comprises.

Comment la même exigence sonne différemment en revue d’incident

« Nous stockons les sessions dans Redis » est une déclaration de conception. « Un redémarrage de Redis a déconnecté tout le monde » est une déclaration d’incident.
Votre travail est de traduire les déclarations de conception en déclarations d’incident avant que la production ne le fasse pour vous.

Une citation sur la fiabilité (idée paraphrasée)

Idée paraphrasée : l’espoir n’est pas une stratégie — attribué à de nombreux responsables ops ; le sentiment est courant en ingénierie de fiabilité.

Deux petites blagues (exactement deux)

Blague n°1 : Un cache est l’endroit où les données partent à la retraite. Sauf si vous y stockez l’authentification, alors c’est une carrière avec des gardes de nuit.

Blague n°2 : Chaque clé Redis « temporaire » vit exactement aussi longtemps que votre rotation d’on-call.

Matrice de décision (à utiliser quand les gens argumentent)

Posez ces questions dans l’ordre ; arrêtez-vous quand vous obtenez un « non » catégorique.

  1. Pouvez-vous tolérer de le perdre ? Si non : privilégiez PostgreSQL (ou une autre file/magasin durable).
  2. Avez-vous besoin d’incréments/expirations atomiques à haut débit ? Si oui : Redis gagne souvent.
  3. Avez-vous besoin de requêtes ad hoc, d’audit ou de backfills ? Si oui : PostgreSQL gagne.
  4. Avez-vous besoin de fan-out / streams / consumer groups ? Redis peut être excellent, mais engagez-vous à l’exploiter comme un vrai système, pas comme un jouet.
  5. Avez-vous besoin d’un couplage transactionnel strict avec des écritures métier ? Si oui : PostgreSQL, car « écrire dans l’app + écrire dans Redis » est l’endroit où la consistance va mourir.

Modèle de menace : les défaillances pour lesquelles vous devez concevoir

  • Redémarrage : redémarrages de processus, reboots de nœuds, reprogrammation de conteneurs.
  • Partition : l’app peut atteindre un nœud de datastore mais pas un autre ; les clients retryent.
  • Surcharge : pics de latence, accumulation de backlog, timeouts deviennent retries, les retries deviennent une tempête.
  • Temps : TTL et fenêtres de rate-limit sont basés sur le temps ; les horloges dérivent, les déploiements roulent, les utilisateurs sont impatients.
  • Éviction : Redis peut évincer des clés ; « volatile-lru » n’est pas un plan de continuité métier.
  • Vacuum/compactage : la bloat Postgres et le vacuum affectent la latence ; le fork Redis pour les snapshots RDB affecte la mémoire et la latence.

Faits intéressants et contexte historique (du type qui change les décisions)

  • La lignée de PostgreSQL remonte à POSTGRES (années 1980, UC Berkeley), conçu avec une obsession de recherche pour la correction et l’extensibilité — toujours visible dans MVCC et WAL.
  • Redis a démarré (fin des années 2000) comme serveur de structures de données en mémoire ; sa killer feature n’était pas « clé/valeur », mais les opérations atomiques sur des structures utiles (lists, sets, sorted sets).
  • La persistance Redis a commencé comme snapshots optionnels (RDB). Le fichier append-only (AOF) est arrivé pour réduire les fenêtres de perte, mais échange durabilité contre amplification des écritures et choix d’fsync.
  • Le MVCC de Postgres signifie que les lectures ne bloquent pas les écritures (en général). Cela signifie aussi que des tuples morts s’accumulent ; le vacuum n’est pas optionnel si vous aimez la latence stable.
  • L’exécution mono-thread des commandes Redis est une feature : elle rend la plupart des opérations atomiques sans verrous. C’est aussi un plafond quand vous chargez des scripts Lua lourds ou des commandes longues.
  • Les queues basées sur LIST dans Redis étaient populaires bien avant les streams ; les patterns BRPOP ont façonné une génération de systèmes « assez bons » — jusqu’à ce que les gens aient besoin de replay et de consumer groups.
  • « Exactement-once » a été une mirage récurrente dans l’industrie. La plupart des systèmes réels atteignent au moins-once avec idempotence. Postgres facilite l’application de l’idempotence via les contraintes.
  • Le rate limiting a évolué des fenêtres fixes vers des fenêtres glissantes et des token buckets parce que l’équité compte lors des rafales ; les incréments atomiques de Redis ont rendu ces patterns pratiques à grande échelle.

Sessions : collantes, sans état, et le mensonge entre les deux

Ce que « stockage de session » veut réellement dire

Les sessions sont de l’état, mais l’application veut faire semblant d’être sans état. Cette tension apparaît à trois endroits :
authentification, révocation et expiration.

L’hypothèse dangereuse est que les sessions sont « juste un cache ». Elles ne le sont pas, sauf si vous ne vous souciez vraiment pas qu’un utilisateur soit déconnecté ou re-challengé.
Pour les applications grand public cela peut être acceptable. Pour des consoles d’administration B2B pendant une démo commerciale, c’est comme ça que vous gagnez votre prochaine coupe de budget.

Trois patterns et où ils appartiennent

  1. Tokens signés (pas de session côté serveur)
    Ne rien stocker côté serveur ; mettre des claims dans un token signé (type JWT). Excellent pour les API à lecture intensive. Mauvais quand vous avez besoin de révocation instantanée
    et que de courts TTL provoquent des tempêtes de refresh.
  2. Sessions côté serveur
    Stocker un session ID dans un cookie, garder les données de session dans Redis ou Postgres. Opérationnellement ennuyeux, ce qui est un compliment.
  3. Hybride
    Token signé pour l’identité + blacklist/révocation côté serveur. C’est là que Redis est fort : petites clés, TTL, checks atomiques.

Redis pour les sessions : quand c’est approprié

Redis fonctionne bien pour les sessions quand :

  • La perte de session est acceptable (ou vous pouvez ré-authentifier facilement).
  • Vous avez besoin de lectures rapides et d’expiration TTL sans job de nettoyage.
  • Vous êtes discipliné sur la persistance (ou choisissez explicitement d’être permissif sur les pertes).
  • Vous pouvez tolérer le jour occasionnel où « tout le monde se reconnecte ».

Redis pour les sessions : quand c’est un piège

C’est un piège quand la session est en fait un enregistrement d’attribution (rôles, scopes, flags MFA) et que la perte signifie :
une accessibilité révoquée revient, ou l’accès disparaît pour des utilisateurs valides. L’un ou l’autre génère des tickets support.

Aussi : les politiques d’éviction de Redis ne se préoccupent pas de votre entonnoir commercial. Si la pression mémoire déclenche l’éviction et que vos clés de session sont éligibles,
vous venez de créer une loterie de déconnexions aléatoires.

PostgreSQL pour les sessions : ennuyeux et correct

Les sessions dans Postgres sont plus lentes par requête, mais prévisibles et interrogeables. Vous pouvez faire respecter l’unicité, suivre last_seen, exécuter des audits
et backfiller la logique de nettoyage. Et vous pouvez coupler les changements d’état de session avec les écritures métier à l’intérieur de transactions.

Le compromis : vous devez maintenant gérer le nettoyage et le contrôle de la bloat. Les tables de sessions sont des usines à churn. Si vous ne gérez pas le vacuum, vous verrez la latence augmenter.

Conception pratique : séparer « identité de session » et « payload de session »

Un bon compromis : stocker l’enregistrement minimal et autoritaire dans Postgres (session id, user id, created_at, revoked_at, expires_at),
et stocker un payload de performance optionnel dans Redis (préférences utilisateur, permissions calculées) indexé par session id avec TTL.
Si Redis le perd, vous recomputez. Si Postgres le perd, vous avez de plus gros problèmes.

Limites de débit : compteurs, horloges et équité

Ce que protège le rate limiting

Le rate limiting n’est pas juste « arrêter les abuseurs ». C’est aussi :
protéger les dépendances en aval, façonner les locataires bruyants, éviter les thundering herds au login ou reset de mot de passe,
et empêcher que des retries auto-infligés consomment tout votre budget.

Rate limiting avec Redis : le choix par défaut pour une raison

Redis est excellent pour le rate limiting parce qu’il offre :

  • Incréments atomiques (INCR/INCRBY) afin d’éviter les races.
  • TTL (EXPIRE) pour que les compteurs disparaissent sans cron jobs.
  • Scripts Lua pour combiner « increment + check + set TTL » en une seule opération atomique.
  • Faible latence pour que votre limiteur ne devienne pas le goulot.

Mais : la durabilité Redis n’est pas gratuite

Si votre limiteur se réinitialise au redémarrage, cela peut être acceptable. Ou vous pourriez inonder une API partenaire pendant 90 secondes et voir votre clé révoquée.
Décidez dans quel monde vous vivez.

Si vous devez « survivre aux redémarrages », configurez la persistance de façon réfléchie et testez-la. AOF avec politiques fsync peut aider,
mais cela change l’enveloppe de performance et le mode de défaillance (IO disque devient votre limiteur).

Rate limiting avec Postgres : quand vous devriez le faire quand même

Postgres peut faire du rate limiting, généralement sous ces formes :

  • Compteurs par fenêtre avec upserts (INSERT … ON CONFLICT DO UPDATE). Fonctionne pour des débits modérés et un fort besoin de correction.
  • Token bucket stocké par user/tenant dans une ligne avec « last_refill » et « tokens ». Nécessite une gestion prudente du temps et de la concurrence.
  • Leaky bucket via jobs où la base est autoritaire et l’application met en cache la décision brièvement.

Le rate limiting Postgres est plus lent, mais vous apporte audit et interrogeabilité. Si vous devez répondre « pourquoi le tenant X a été throttlé hier »,
Postgres est l’endroit où cette histoire se reconstitue le plus facilement.

L’équité est une décision produit déguisée en algorithme

Les limites en fenêtre fixe sont faciles et injustes aux frontières. Les logs en fenêtre glissante sont équitables et coûteux. Les token buckets sont assez équitables
et assez peu coûteux. Le point : choisissez l’équité que vous pouvez supporter opérationnellement. Un algorithme intelligent que vous ne pouvez pas déboguer pendant un incident
est une responsabilité.

Files d’attente : durabilité, visibilité et contre-pression

Une file n’est pas une liste

Une vraie file est un contrat :
les messages sont durablement enregistrés, les workers les réclament, les échecs les remettent en circulation, des doublons surviennent, et le système reste observable.
Si vous implémentez des files comme « une liste + de l’espoir », vous apprendrez les cas limites dans le pire environnement possible : la production.

Queues Redis : rapides, flexibles, et faciles à mal configurer subtilement

Redis peut implémenter des files avec des lists (LPUSH/BRPOP), des sorted sets (jobs retardés), des streams (consumer groups) et pub/sub (pas une file).
Chacun a ses compromis :

  • Lists : simples, rapides. Mais vous avez besoin de votre propre modèle de fiabilité (ack, retry, dead-letter). Les patterns BRPOPLPUSH aident.
  • Streams : plus proche d’un log réel avec consumer groups, acking et pending entries. Opérationnellement plus complexe, mais plus honnête.
  • Pub/Sub : pas durable ; les abonnés qui se déconnectent ratent les messages. Idéal pour les notifications éphémères, pas pour des jobs.

Queues Postgres : étonnamment fortes pour beaucoup de charges

Postgres peut alimenter des files en utilisant tables + index + verrouillage de lignes :

  • SELECT … FOR UPDATE SKIP LOCKED est l’outil de travail pour « réclamer un job sans que deux workers le prennent ».
  • Enqueue transactionnel permet de coupler la création de jobs avec l’état métier (pattern outbox).
  • Interrogeabilité vous permet de construire des tableaux de bord et d’investiguer des jobs bloqués sans outils personnalisés.

Le compromis est le débit et la contention. Postgres est excellent jusqu’à ce que ce ne soit plus le cas : taux d’écriture élevé, partitions chaudes ou transactions longues
peuvent transformer la table de queue en champ de bataille. Mais pour beaucoup de systèmes corporate — débits modérés, forte correction — les queues Postgres sont
le choix ennuyeux qui fonctionne vraiment.

Visibility timeout : ce qui décide si vous dormez

Les jobs ont besoin d’un concept de « en cours ». Si un worker meurt en plein job, ce job doit redevenir visible. Les listes Redis ne vous donnent pas cela
automatiquement. Les streams le font, mais vous devez toujours gérer les pending entries. Postgres le fait, si vous modélisez « locked_at/locked_by » et récupérez les jobs
après un timeout.

Idempotence : vous traiterez des doublons

Entre retries, timeouts, partitions réseau et déploiements, les doublons ne sont pas hypothétiques. « Exactly once » est du marketing.
Intégrez des clés d’idempotence dans les handlers de job et appliquez-les autant que possible (les contraintes uniques de Postgres sont vos meilleures amies).

Fiche de diagnostic rapide : trouver le goulot en 10 minutes

Vous êtes page. La latence monte. Les logins échouent. Les jobs s’accumulent. Ne débattez pas architecture. Lancez la fiche.

Première étape : déterminer si le problème est latence du datastore ou contention applicative

  1. Vérifiez la latence Redis et les clients bloqués (si Redis est dans le chemin). Si vous voyez des commandes lentes ou des clients bloqués, vous êtes probablement limité par Redis.
  2. Vérifiez les sessions actives et verrous Postgres. Si vous voyez des waits de lock ou des connexions saturées, vous êtes probablement limité par Postgres.
  3. Vérifiez les motifs d’erreur : timeouts vs « OOM » vs « too many clients » vs « READONLY » vous aide à trier rapidement.

Deuxième étape : confirmer si le problème est saturation des ressources ou bugs de correction

  1. Saturation des ressources : CPU haut, IO élevé, pertes réseau, pression mémoire, éviction, pool de connexions plein.
  2. Bugs de correction : pic soudain de retries, boucle accidentelle, script Lua parti en vrille, croissance non bornée de la queue due à un ack manquant.

Troisième étape : choisir la mitigation la moins dangereuse

  • Limiter plus strictement (oui, même si le produit râle) pour stabiliser.
  • Désactiver les fonctionnalités coûteuses (enrichissement de session, vérifications profondes de permissions) si elles frappent le datastore.
  • Mettre en pause les consommateurs si la queue fond les systèmes en aval.
  • Scaler les lectures si la charge le permet ; n’ajoutez pas bêtement des writers pour résoudre un problème de verrous.

Ce qu’il ne faut pas faire

Ne redémarrez pas Redis comme « fix » à moins d’accepter de perdre des données volatiles et de savoir pourquoi il est bloqué. Ne faites pas rebondir Postgres quand vous avez
des transactions longues ouvertes sauf si vous aimez expliquer à la finance pourquoi des factures ont été dupliquées.

Tâches pratiques : commandes, sorties, ce que ça signifie, décision

Ce sont le genre de vérifications que vous exécutez pendant les revues de conception et les incidents. Chaque point inclut une commande, un exemple de sortie,
ce que la sortie signifie et la décision qu’elle entraîne.

Redis : santé, latence, persistance, mémoire, éviction

Task 1: Check Redis basic health and role

cr0x@server:~$ redis-cli -h redis-01 INFO replication
# Replication
role:master
connected_slaves:1
master_repl_offset:987654321
repl_backlog_active:1

Signification : Vous êtes sur un master ; un replica est connecté ; le backlog de réplication est actif.

Décision : Si le rôle est « slave » de façon inattendue, vos clients peuvent écrire sur un nœud en lecture seule. Corrigez le service discovery / failover d’abord.

Task 2: Measure Redis command latency spikes

cr0x@server:~$ redis-cli -h redis-01 --latency -i 1
min: 0, max: 12, avg: 1.23 (176 samples)
min: 0, max: 97, avg: 4.87 (182 samples)

Signification : Pics occasionnels à 97ms. Dans un chemin de rate limiter, cela fait mal. Pour des lectures de session, cela peut en cascade provoquer des timeouts/retries.

Décision : Investiguer les commandes lentes, les forks de persistance ou le jitter réseau. Si les pics corrèlent avec des saves RDB, ajustez la persistance ou passez à des réglages AOF adaptés.

Task 3: Identify slow Redis commands (built-in slowlog)

cr0x@server:~$ redis-cli -h redis-01 SLOWLOG GET 3
1) 1) (integer) 12231
   2) (integer) 1735600000
   3) (integer) 24567
   4) 1) "EVAL"
      2) "..."
   5) "10.0.2.41:53422"
   6) ""

Signification : Un script Lua a pris ~24ms. Quelques-uns de ces scripts sous charge peuvent sérialiser votre serveur car Redis exécute les commandes majoritairement en mono-thread.

Décision : Réécrire les scripts pour les simplifier, réduire les scans de clés ou sortir la logique lourde de Redis. Si vous avez besoin d’une logique de queue complexe, envisagez Redis streams ou Postgres.

Task 4: Check Redis memory use and fragmentation

cr0x@server:~$ redis-cli -h redis-01 INFO memory | egrep 'used_memory_human|maxmemory_human|mem_fragmentation_ratio'
used_memory_human:18.42G
maxmemory_human:20.00G
mem_fragmentation_ratio:1.78

Signification : Vous êtes proche de maxmemory et la fragmentation est élevée. Des évictions ou erreurs OOM sont imminentes ; le fork pour la persistance peut échouer.

Décision : Augmenter la mémoire, réduire la cardinalité des clés, corriger la stratégie TTL ou choisir une politique d’éviction intentionnelle. Si les sessions ne doivent pas disparaître, ne comptez pas sur des réglages favorisant l’éviction.

Task 5: Confirm eviction behavior

cr0x@server:~$ redis-cli -h redis-01 CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

Signification : Toute clé peut être évincée sous pression mémoire.

Décision : Si vous stockez des sessions ou l’état de queue ici, c’est un risque de fiabilité. Envisagez « volatile-ttl » pour les clés TTL seules, ou déplacez l’état autoritaire vers Postgres.

Task 6: Check persistence mode and last save

cr0x@server:~$ redis-cli -h redis-01 INFO persistence | egrep 'aof_enabled|rdb_last_save_time|aof_last_write_status'
aof_enabled:1
aof_last_write_status:ok
rdb_last_save_time:1735600123

Signification : AOF est activé et écrit correctement ; RDB existe aussi. Vous avez une certaine durabilité, selon la politique fsync.

Décision : Si ce Redis est désormais autorité pour les sessions ou source de vérité de la queue, vérifiez la politique fsync et le temps de récupération. Si vous ne pouvez pas tolérer de perte, Redis peut quand même ne pas suffire.

Task 7: Check for blocked clients (often queue-related)

cr0x@server:~$ redis-cli -h redis-01 INFO clients | egrep 'blocked_clients|connected_clients'
connected_clients:1823
blocked_clients:312

Signification : Beaucoup de clients sont bloqués, probablement en attente de pops bloquants ou de scripts lents. Cela peut être normal pour les patterns BRPOP, mais 312 est élevé.

Décision : Si les clients bloqués corrèlent avec des timeouts, repensez la consommation de la queue (streams, concurrency bornée) ou augmentez l’efficacité des workers.

PostgreSQL : connexions, verrous, vacuum, bloat, IO, comportement des requêtes

Task 8: See Postgres connection saturation

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select count(*) as total, state from pg_stat_activity group by state order by total desc;"
 total | state
-------+----------------
   120 | active
    80 | idle
    35 | idle in transaction

Signification : 35 sessions sont idle in transaction. C’est souvent un bug ou une mauvaise config du pool et cela bloque le vacuum et retient des verrous.

Décision : Corriger l’application pour commit/rollback rapidement ; définir des statement timeouts ; s’assurer que votre pool ne laisse pas de transactions ouvertes.

Task 9: Identify lock waits

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where wait_event is not null group by 1,2 order by 3 desc;"
 wait_event_type |   wait_event   | count
-----------------+----------------+-------
 Lock            | transactionid   |     9
 LWLock          | BufferMapping   |     4

Signification : Des waits de lock sur transaction IDs suggèrent de la contention (updates/deletes, transactions longues) affectant les autres.

Décision : Trouver la transaction bloquante. Si c’est une migration ou un batch, arrêtez-la ou throttlez-la. Si ce sont des workers de queue qui retiennent des verrous, corrigez le comportement des workers.

Task 10: Find the blockers

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select pid, usename, state, now()-xact_start as xact_age, query from pg_stat_activity where state <> 'idle' order by xact_age desc limit 5;"
 pid  | usename | state  | xact_age |                  query
------+--------+--------+----------+------------------------------------------
 8421 | app    | active | 00:14:32 | update sessions set last_seen=now() ...
 9110 | app    | active | 00:09:11 | delete from queue_jobs where ...

Signification : Écritures longues sur les tables sessions/queue. C’est la zone chaude pour le churn de sessions et la contention de queue.

Décision : Ajouter des index, réduire la fréquence des updates (write-behind pour last_seen) et s’assurer que les deletes de queue sont batchés avec des limites raisonnables.

Task 11: Check vacuum health on a churny table

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select relname, n_dead_tup, last_autovacuum, autovacuum_count from pg_stat_user_tables where relname in ('sessions','queue_jobs');"
  relname  | n_dead_tup |     last_autovacuum     | autovacuum_count
-----------+------------+-------------------------+------------------
 sessions  |    812334  | 2025-12-30 01:10:02+00  |              421
 queue_jobs|    120998  | 2025-12-30 01:08:47+00  |              388

Signification : Nombre important de dead tuples. L’autovacuum tourne, mais vous pouvez être en retard sous charge.

Décision : Ajuster autovacuum pour ces tables (seuils plus bas), envisager le partitionnement par temps et réduire le churn d’updates.

Task 12: Inspect index usage for sessions or rate-limit tables

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select relname, idx_scan, seq_scan from pg_stat_user_tables where relname='sessions';"
 relname  | idx_scan | seq_scan
----------+----------+----------
 sessions | 98234122 |     4132

Signification : Les scans d’index dominent (bien). Si seq_scan était élevé, vous feriez probablement des scans de table complets sur une table chaude.

Décision : Si seq_scan monte, ajoutez/réparez des index ou corrigez les predicates de requête. Pour les sessions, vous voulez des recherches par session_id et expires_at.

Task 13: Measure Postgres cache vs disk pressure (rough signal)

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select datname, blks_hit, blks_read, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) as hit_pct from pg_stat_database where datname='appdb';"
 datname | blks_hit  | blks_read | hit_pct
---------+-----------+-----------+---------
 appdb   | 891234567 |  23123456 |   97.46

Signification : Hit cache ~97%. C’est correct. Si cela chute brusquement, l’IO disque peut être votre goulot.

Décision : Si hit_pct chute, vérifiez la taille du working set, les index et si un nouveau pattern de requête lit beaucoup de données froides.

Task 14: Check queue backlog and oldest job age (Postgres queue)

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select count(*) as ready, min(now()-created_at) as oldest_age from queue_jobs where state='ready';"
 ready | oldest_age
-------+------------
 48211 | 02:41:18

Signification : Le backlog est important ; le job le plus ancien a presque 3 heures. C’est un problème de débit ou de dépendance en aval.

Décision : Scalez les consumers prudemment, mais confirmez d’abord que la DB n’est pas le goulot. Envisagez de mettre en pause les producteurs ou d’appliquer de la contre-pression en amont.

Task 15: Inspect Redis keyspace and TTL behavior for sessions

cr0x@server:~$ redis-cli -h redis-01 INFO keyspace
# Keyspace
db0:keys=4123891,expires=4100022,avg_ttl=286000

Signification : Presque toutes les clés expirent ; TTL moyen ~286 secondes. Typique pour les rate limits, risqué pour des sessions sauf si c’est intentionnel.

Décision : Si ce sont des sessions et que le TTL est court, vous générez du churn et des déconnexions. Ajustez la stratégie TTL, utilisez des refresh tokens ou déplacez l’autorité vers Postgres.

Task 16: Confirm Postgres queue claim behavior (SKIP LOCKED)

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "begin; select id from queue_jobs where state='ready' order by id limit 3 for update skip locked; commit;"
BEGIN
  id
------
 9912
 9913
 9914
(3 rows)
COMMIT

Signification : Les workers peuvent réclamer des jobs sans double-traitement grâce aux verrous de lignes.

Décision : Si cela bloque ou ne retourne rien alors que le backlog existe, investiguez la contention de verrous, des index manquants ou des jobs bloqués « in progress ».

Trois mini-récits d’entreprise depuis le terrain

1) Incident causé par une mauvaise hypothèse : « Les sessions sont un cache »

Une entreprise SaaS de taille moyenne a déplacé les sessions de Postgres vers Redis pour « réduire la charge ». Sur le papier c’était propre :
session_id → JSON blob, TTL 30 jours. Les lectures sont devenues plus rapides, les graphiques DB avaient meilleure mine, tout le monde est rentré tôt.

Des mois plus tard, le trafic a augmenté et le nœud Redis a eu une montée de mémoire. Pendant la fenêtre de maintenance, le nœud a redémarré.
Pas de gros souci, pensaient-ils — la persistance était « activée ». Techniquement oui. Snapshots toutes les 15 minutes.

Le rayon d’impact a été immédiat : les utilisateurs ont été déconnectés, mais pire, certains contrôles de sécurité étaient couplés au payload de session.
Une partie des flags « MFA récemment vérifié » a été perdue et a relancé des challenges MFA. Les tickets support se sont accumulés, les démos commerciales ont été perturbées,
et l’incident a pris ce ton particulier où tout le monde est techniquement calme mais émotionnellement furieux.

La mauvaise hypothèse n’était pas « Redis est peu fiable ». Redis faisait exactement ce pour quoi il était configuré.
La mauvaise hypothèse était que les sessions étaient un « cache reconstructible ». Elles ne l’étaient pas. Les sessions étaient devenues un artefact d’attribution.

Le correctif a été ennuyeux et efficace : les enregistrements de session autoritaires sont revenus dans Postgres avec une table de révocation.
Redis est resté, mais seulement pour l’enrichissement dérivé des sessions avec TTL. Ils ont aussi changé le runbook : tout redémarrage de Redis est traité comme un événement planifié affectant l’auth, sauf preuve du contraire.

2) Optimisation qui s’est retournée contre eux : le rate limiter en Lua

Une autre entreprise opérait une API publique et faisait du rate limiting dans Redis. Initialement c’était un INCR + EXPIRE simple.
Quelqu’un l’a amélioré avec un script Lua implémentant une fenêtre glissante. L’équité s’est améliorée, les dashboards aussi, et le développeur a été félicité.

Puis l’API a lancé une fonctionnalité batch. Les clients ont martelé l’endpoint en rafales, comme le font les batchs.
Le script Lua s’est retrouvé à tourner sur des clés chaudes avec de grands sorted sets. Sous charge, le script est apparu dans SLOWLOG.

Redis, étant mono-thread pour l’exécution des commandes, est devenu le point d’étranglement. La latence a monté, les clients ont timeouté, les clients ont retryé,
et les retries ont augmenté la charge. Le rate limiter — censé protéger le système — est devenu le principal mode de défaillance du système.

Ils ont tenté de faire scaler Redis verticalement, ce qui a gagné du temps mais pas la paix. Le vrai correctif a été de simplifier :
token bucket avec ops atomiques et clés courtes ; accepter une légère injuste aux frontières ; ajouter lissage par-tenant dans l’application.
L’équité est agréable. La survie est plus agréable.

Après l’incident, ils ont ajouté une règle : tout changement de script Lua nécessite un test de charge et un plan de rollback.
Le scripting Redis est puissant. C’est aussi la manière la plus simple d’introduire un verrou global caché dans votre architecture.

3) Pratique ennuyeuse mais correcte qui a sauvé la mise : outbox Postgres + idempotence

Une entreprise traitait des paiements. Elle devait émettre des événements « payment_succeeded » pour déclencher emails, provisionnement et analytics.
Ils utilisaient Postgres pour les données cœur et avaient un système de workers séparé. Quelqu’un a proposé de pousser les événements directement dans Redis pour la vitesse.

L’équipe, qui avait déjà été brulée, a insisté pour une table outbox dans Postgres : quand une ligne payment est commitée, une ligne est insérée dans outbox
dans la même transaction. Un worker background lit les lignes outbox avec SKIP LOCKED, publie vers les systèmes downstream et les marque comme traitées.
Ce n’est pas glamour. C’est extrêmement débogable.

Un jour, un service downstream a dégradé et la publication d’événements a ralenti. Le backlog outbox a grossi, mais les paiements ont continué en toute sécurité.
Parce que l’outbox était dans Postgres, l’équipe a pu requêter les états bloqués exacts, rejouer en toute sécurité et faire du nettoyage ciblé.

Le détail qui a sauvé : l’idempotence. L’outbox avait une contrainte unique sur (event_type, aggregate_id, version).
Quand les workers retryaient sous timeout, les doublons étaient inoffensifs. La contrainte a fait respecter le contrat même sous le chaos.

L’incident s’est terminé sans perte de données, sans double-provisionnement et sans une saga médico-légale d’une semaine.
Personne n’a été applaudi pour son architecture excitante. Ils ont obtenu quelque chose de mieux : un postmortem calme.

Erreurs fréquentes (symptômes → cause racine → correctif)

1) Déconnexions aléatoires et pics « session not found »

Syndromes : Utilisateurs déconnectés de façon intermittente ; service d’auth montre des cache misses ; Redis proche du max mémoire.

Cause racine : La politique d’éviction Redis permet d’évincer des clés de session, ou la fenêtre de persistance perd des sessions récentes après un redémarrage.

Correctif : Déplacez l’état de session autoritaire vers Postgres, ou configurez maxmemory-policy de Redis pour éviter d’évincer les clés de session et assurez-vous que la persistance correspond à votre tolérance à la perte.

2) Les limites de débit se réinitialisent après deploy/redémarrage

Syndromes : Des rafales passent au travers du limiteur après redémarrage Redis ; l’API partenaire se plaint ; chute soudaine des 429 à zéro.

Cause racine : État du limiteur Redis volatile sans persistance durable, ou clés limiteur existant seulement en mémoire.

Correctif : Décidez si la réinitialisation est acceptable. Si non, utilisez AOF avec fsync adapté, ou stockez les compteurs dans Postgres pour les limites critiques, ou implémentez un hybride (chemin rapide Redis + réconciliation périodique).

3) Le backlog de la queue grandit alors que les workers semblent « healthy »

Syndromes : Workers en fonctionnement, CPU bas, mais l’âge des jobs augmente. blocked_clients Redis élevé ou verrous Postgres présents.

Cause racine : Dépendance en aval lente, visibility timeout mal dimensionné, ou workers bloqués sur un lock ou une transaction longue.

Correctif : Instrumenter la durée des jobs et la latence des dépendances ; implémenter des timeouts ; dans les queues Postgres, éviter les transactions longues et séparer claim+work des écritures métier quand possible.

4) La queue Postgres cause de la contention de verrous et ralentit toute la DB

Syndromes : Waits de lock élevés ; latence accrue pour des requêtes non liées ; autovacuum en retard sur la table de queue.

Cause racine : Table de queue chaude avec updates/deletes fréquents ; index partiels manquants ; workers mettant à jour les lignes trop souvent (heartbeats).

Correctif : Utiliser un index partiel « ready » ; mettre à jour des colonnes minimales ; batcher les deletes ; partitionner par temps ; envisager de déplacer des queues éphémères à haut débit vers Redis streams.

5) La queue Redis perd des jobs quand un consumer crash

Syndromes : Un job disparaît après la mort d’un worker ; pas de retry ; processus métier inachevé.

Cause racine : Utilisation d’un simple pop sur liste sans ack/requeue (RPOP/BLPOP) et sans suivi des in-flight.

Correctif : Utiliser BRPOPLPUSH avec une liste de processing et un reaper, ou utiliser Redis streams avec consumer groups, ou migrer vers une queue Postgres avec sémantique de visibility timeout.

6) « Too many connections » dans Postgres lors de pics de login

Syndromes : Postgres rejette des connexions ; threads app bloqués ; pgbouncer absent ou mal configuré.

Cause racine : Pattern une connexion par requête ; absence de pooling ; écritures de session à chaque requête.

Correctif : Ajouter du pooling de connexions, réduire la fréquence d’écriture (éviter d’updater last_seen à chaque requête) et pré-calculer les données dérivées de session dans Redis si c’est sûr.

7) Pics de latence Redis toutes les quelques minutes

Syndromes : Sauts de latence P99 ; le rate limiter provoque des timeouts ; les graphes montrent des pics périodiques.

Cause racine : Forks de snapshot RDB, réécriture AOF, ou saturation IO disque si fsync AOF est agressif.

Correctif : Ajuster le calendrier de persistance, utiliser fsync « everysec » si acceptable, surveiller le temps de fork, assurer un disque rapide et éviter des empreintes mémoire énormes qui rendent les forks coûteux.

Checklists / plan étape par étape

Checklist A : décider où vont les sessions

  1. Classer ce que contient une session : identité seulement, attributions, état MFA ou préférences utilisateur.
  2. Si les attributions/état MFA sont dans le payload de session, faire de Postgres l’autorité (ou séparer autorité et cache).
  3. Définir la tolérance à la perte : « les utilisateurs peuvent se reloguer » vs « la révocation doit être immédiate ».
  4. Choisir une stratégie TTL : expiration absolue + timeout d’inactivité ; décider qui rafraîchit et quand.
  5. Planifier le nettoyage : tuning vacuum/autovacuum Postgres ; TTL et politique d’éviction Redis.
  6. Tester le comportement au redémarrage en staging avec une charge réaliste et les paramètres de persistance.

Checklist B : implémenter des limites de débit sans créer un nouveau goulot

  1. Choisir l’algorithme : token bucket est généralement le meilleur compromis.
  2. Définir la portée : par-IP, par-utilisateur, par-tenant, par-endpoint — éviter une cardinalité non bornée sans plan.
  3. Utiliser Redis si vous avez besoin de haut débit et pouvez tolérer de courtes fenêtres de perte.
  4. Si les limites sont contractuelles (APIs partenaires), envisager des limites Backed par Postgres ou une config Redis durable.
  5. Instrumenter la latence du limiteur séparément ; traitez-le comme une dépendance.
  6. Avoir un « mode dégradé » : permettre de petits bursts, bloquer les endpoints coûteux et logger les décisions pour la forensique.

Checklist C : choisir une implémentation de queue qui ne vous trahira pas

  1. Écrire le contrat : at-least-once, politique de retry, dead-letter, besoins d’ordre, scheduling retardé, visibility timeout.
  2. Si les jobs doivent être couplés transactionnellement aux écritures DB, utiliser le pattern outbox Postgres.
  3. Si le débit est élevé et les jobs éphémères, Redis streams peuvent être une option solide — exploitez-les sérieusement.
  4. Concevoir des clés d’idempotence et les faire respecter (contraintes uniques Postgres ou ensembles de déduplication dans Redis avec TTL).
  5. Rendre la contre-pression explicite : les producteurs doivent ralentir quand les consumers prennent du retard.
  6. Construire des requêtes opérationnelles : « job le plus ancien », « bloqué en cours », « distribution du nombre de retries ».

Plan de migration étape par étape (de « n’importe quoi » à sain)

  1. Inventaire de l’état : sessions, limites de débit, queues, clés d’idempotence. Pour chacun, définir la tolérance à la perte et les besoins d’audit.
  2. Choisir les magasins d’autorité : Postgres pour la vérité durable ; Redis pour l’accélération dérivée/éphémère.
  3. Ajouter de l’observabilité : latence P95/P99 pour appels Redis/Postgres, lag de queue, compteurs d’éviction, retard d’autovacuum.
  4. Implémenter le double-écriture seulement si vous pouvez rapprocher ; privilégier des basculements progressifs et des stratégies de lecture de secours.
  5. Tester en charge le « chemin d’incident » : redémarrer Redis, basculer Postgres, simuler partition réseau, throttler l’IO disque.
  6. Écrire des runbooks : que faire sur éviction, lag de réplication, tempêtes de verrous, pics de backlog.

FAQ

1) Puis-je stocker des sessions dans Redis en toute sécurité ?

Oui, si « en toute sécurité » signifie que vous acceptez des fenêtres de perte, le risque d’éviction et le comportement au redémarrage — ou si vous configurez la persistance et la capacité pour correspondre à vos exigences.
Si les sessions sont des artefacts de sécurité autoritaires, gardez l’autorité dans Postgres et mettez en cache les données dérivées dans Redis.

2) La persistance Redis (AOF/RDB) suffit-elle pour le traiter comme une base de données ?

Parfois, mais ne minimisez pas. Les réglages de persistance déterminent les fenêtres de perte et la performance. Les snapshots forkés et la réécriture AOF ont des coûts opérationnels.
Si l’entreprise ne peut pas tolérer la perte, Postgres est généralement l’histoire de durabilité la plus simple.

3) Pourquoi ne pas utiliser Postgres pour tout ?

Vous pouvez, et de nombreuses équipes devraient en faire plus. Les limites sont le débit, la latence et la contention sous compteurs ou queues très chaudes.
Redis est mieux adapté quand vous avez besoin d’opérations atomiques à très haut débit avec des sémantiques TTL.

4) Pourquoi ne pas utiliser Redis pour tout ?

Parce que « en mémoire » ne veut pas dire « infaillible », et parce que l’interrogeabilité compte lors d’incidents et d’audits.
Redis est incroyable pour certains patterns ; ce n’est pas un système d’enregistrement général à moins que vous ne le conceviez ainsi et acceptiez les compromis.

5) Les Redis streams sont-ils une vraie file ?

Ils y ressemblent plus que les lists ou le pub/sub : vous avez consumer groups, acknowledgments et pending entries que vous pouvez inspecter.
Vous devez toujours concevoir les retries, le dead-lettering et les outils opérationnels. Les streams sont une boîte à outils de file, pas un produit file complet.

6) Comment implémenter une queue fiable dans Postgres ?

Utilisez une table de jobs avec un « state » et des timestamps, réclamez les jobs via FOR UPDATE SKIP LOCKED, gardez les transactions courtes et implémentez la logique de retry/dead-letter.
Si l’enqueue doit être couplé aux écritures métier, utilisez le pattern outbox dans la même transaction.

7) Quel est le plus grand risque opérationnel avec Postgres pour les sessions ?

Le churn et la bloat. Les tables de sessions créent vite des dead tuples. Si autovacuum n’est pas tuné, la latence devient imprévisible. Le partitionnement et la réduction de la fréquence d’écriture aident beaucoup.

8) Quel est le plus grand risque opérationnel avec Redis pour le rate limiting ?

Transformer le limiteur en goulot. Les scripts Lua lourds, la cardinalité élevée des clés et les pics de latence induits par la persistance peuvent créer une boucle de défaillance
où les timeouts provoquent des retries, qui augmentent le trafic vers le limiteur.

9) Les limites de débit doivent-elles être durables ?

Cela dépend de ce qu’elles protègent. Si c’est pour l’équité interne, les réinitialisations sont souvent acceptables. Si c’est contractuel (APIs partenaires, contrôles anti-fraude),
la durabilité compte — soit via une configuration Redis persistante, soit via une autorité Postgres.

10) Comment éviter le double-traitement des jobs ?

Supposez une livraison au moins-once et rendez les handlers idempotents. Dans Postgres, faites respecter l’idempotence avec des contraintes uniques.
Dans Redis, utilisez des clés de déduplication (avec TTL) ou intégrez l’idempotence dans les écritures avales.

Prochaines étapes à exécuter cette semaine

Cessez de débattre quel outil est « meilleur ». Décidez quelles défaillances vous pouvez tolérer, puis choisissez le datastore qui échoue de manière acceptable.
Redis est fantastique pour la vitesse et l’état piloté par TTL. Postgres est fantastique pour la vérité, le couplage et la clarté médico-légale.
Les systèmes les plus résilients utilisent les deux, avec une frontière stricte entre autorité et accélération.

  1. Écrivez ce qui se passe si Redis perd toutes les clés. Si la réponse inclut « incident de sécurité », déplacez l’autorité vers Postgres.
  2. Exécutez les tâches pratiques ci-dessus en staging et en production. Capturez des baselines pour latence, éviction, waits de verrous et santé du vacuum.
  3. Pour les queues, choisissez un contrat (at-least-once + idempotence est le défaut sain) et implémentez visibility timeouts et dead-lettering.
  4. Pour les sessions, séparez l’autorité du cache : Postgres pour révocation/expiry ; Redis pour le payload dérivé et recomputable.
  5. Pour le rate limiting, gardez l’algorithme simple, instrumentez le limiteur et traitez-le comme une dépendance qui peut vous faire tomber.

Stockez correctement maintenant, ou payez plus tard. La facture arrive pendant l’incident. Elle arrive toujours.

← Précédent
Proxmox « l’agent invité n’est pas en cours d’exécution » : activer QEMU Guest Agent et le rendre persistant
Suivant →
MySQL vs RDS MySQL : les limites cachées qui frappent lors des incidents

Laisser un commentaire