Le défi
Une plateforme d’analytique ingérait les événements produit d’une base de clients en croissance dans une seule table « raw_events ». Le schéma d’origine était correct mais lent : chaque événement touchait une table journalisée avec sept index, le WAL était le goulot d’étranglement et le débit d’ingestion plafonnait autour de 8 000 événements par seconde. Les IOPS RDS constituaient le principal poste de coût, et une expansion client prévue d’un facteur 3 allait faire casser le pipeline avant la signature du prochain contrat.
L’équipe avait lu les conseils habituels — « utilisez une file », « shardez la table », « passez à une TSDB dédiée » — mais toutes ces réponses impliquaient une migration de plusieurs trimestres. La vraie question était : PostgreSQL peut-il suivre si nous cessons de lutter contre lui ? Le piège était une exigence stricte : tout ce qui atteignait les tables de reporting auditées devait être durable, cohérent et indexé. Nous ne pouvions pas sacrifier l’intégrité côté analytique pour sauver l’ingestion.
Notre solution
Nous avons scindé le pipeline en deux étapes clairement séparées, avec deux contrats de durabilité différents.
Étape 1 (ingestion) : une table de staging UNLOGGED qui absorbe le flux. Les tables UNLOGGED de PostgreSQL n’écrivent pas dans le WAL lors des insertions, ce qui est exactement le bon compromis pour des données de staging éphémères — nous obtenons un débit d’insertion brut 5 à 10 fois supérieur en échange de la perte des lignes en staging si le serveur plante. Combiné à un COPY par lots (et non INSERT) depuis un service d’ingestion Python et à une règle délibérée « aucun index sur la table de staging », l’ingestion brute est passée de 8 000 à plus de 80 000 événements/s sur la même instance RDS.
Étape 2 (durable) : un worker Celery vide la table de staging par micro-lots vers la vraie table `events`, entièrement journalisée et indexée, au sein d’une seule transaction avec des upserts idempotents. Le chemin de reporting audité ne lit que la table durable. Si le serveur plante en pleine ingestion, nous perdons au plus quelques secondes d’événements en transit de la table de staging — et les producteurs en amont réessaient, de sorte que la table durable converge vers l’état correct.
Nous y avons associé une couche opérationnelle réduite mais soignée : un tableau de bord Grafana pour le taux de remplissage de l’étape 1, une alerte stricte si le drainer prend du retard, une rotation des partitions sur la table durable et un runbook écrit pour les trois modes de défaillance qui comptent.
- Table de staging UNLOGGED sans index — conçue spécifiquement pour le débit d’insertion brut
- Ingestion COPY par lots depuis un service Python/FastAPI, et non des INSERT ligne par ligne
- Drainer Celery déplaçant des micro-lots vers la table events durable et entièrement indexée
- Upserts idempotents (ON CONFLICT DO NOTHING) pour que les nouvelles tentatives des producteurs soient sans risque
- Partitionnement mensuel de la table durable pour borner le travail de vacuum et d’index
- Tableaux de bord et alertes de latence de bout en bout dans Grafana / Datadog
- Runbook écrit couvrant le basculement RDS, le plantage du drainer et la contre-pression des producteurs
- Validation par l’audit du profil de durabilité avant le lancement — sans mauvaise surprise