Mise à l'échelle à zéro des bases de données PostgreSQL

created: mercredi, oct. 1, 2025

Nous cherchons toujours à concevoir des services aussi économes en ressources que possible. Cela vaut pour les services que nous proposons à l’extérieur et, tout aussi important, pour notre propre infrastructure interne. Beaucoup de nos outils internes, comme les systèmes de facturation et de supervision, reposent sur des bases de données PostgreSQL. Bien qu’essentielles, ces bases restent souvent inactives pendant de longues périodes, consommant de la RAM et des cycles CPU inutilement.

Alors nous nous sommes posé la question : pouvons-nous appliquer notre philosophie de mise à l’échelle à zéro à nos propres bases de données ? La réponse est oui. Nous avons développé un système pour provisionner des instances PostgreSQL qui ne fonctionnent que lorsqu’elles sont activement utilisées. Ce design est incroyablement économe en ressources mais comporte certains compromis, que nous explorerons.

Voici un aperçu schématique de ce que nous avons construit et de la façon dont nous avons réalisé cette mise à l’échelle dynamique.

flowchart LR
  subgraph Machine
    A[systemd.socket]
    B[systemd-socket-proxyd]
    D[(disque local)]
    A -- port 15432 --> B

    subgraph Docker-Compose
      C[conteneur postgres]
    end
    B -- port 5432 --> C
    C -- data-dir --> D
  end

  X[Internet] -- port 25432 --> A

La magie de l’activation par socket

Le cœur de cette configuration est l’activation de socket systemd. Au lieu d’avoir un conteneur PostgreSQL en fonctionnement 24/7, nous laissons le système d’init systemd écouter sur le port de la base de données. Lorsqu’une application tente de se connecter, systemd intercepte la requête, démarre le conteneur de base de données à la demande, puis transfère la connexion. Une fois que la base de données n’est plus utilisée, elle est arrêtée automatiquement.

Cette approche combine la puissance d’outils Linux standards et éprouvés : systemd pour la gestion des services et l’activation de socket, et Docker Compose pour définir notre environnement de base de données conteneurisé. C’est simple, robuste et ne nécessite aucun logiciel personnalisé.

Nos choix technologiques : pourquoi des conteneurs et Docker Compose ?

Nous avons fait deux choix technologiques précis pour cette configuration : exécuter PostgreSQL dans un conteneur et le gérer avec Docker Compose.

Décomposons maintenant les fichiers de configuration qui rendent cela possible.

Les composants

Nous utilisons une combinaison d’un fichier docker-compose.yml pour définir la base de données et de trois fichiers d’unités systemd pour gérer le cycle de vie mise-à-zéro.

1. La définition de la base : Docker Compose

Ceci est un fichier standard docker-compose.yml. Il définit un conteneur PostgreSQL 18, mappe un port interne vers l’hôte et monte un volume pour persister les données de la base sur le disque local. Cela garantit que même si le conteneur s’arrête, les données restent en sécurité. Tous les paramètres documentés dans l’image PostgreSQL officielle sur Docker Hub peuvent être utilisés ici, permettant une personnalisation supplémentaire comme la création d’utilisateurs ou de bases de données spécifiques au démarrage.

/root/pg/pg1/docker-compose.yml

 1version: "3"
 2services:
 3  database:
 4    image: 'postgres:18'
 5    ports:
 6      - 127.0.0.1:14532:5432
 7    volumes:
 8      - /root/pg/pg1/data:/var/lib/postgresql
 9    environment:
10      POSTGRES_PASSWORD: SuperSecretAdminPassword

2. L’écouteur : socket systemd

Cette unité .socket indique à systemd d’écouter sur le port 24532 sur toutes les interfaces réseau. Lorsqu’une connexion TCP arrive, systemd activera pg1-proxy.service. C’est le point d’entrée pour toutes les connexions à la base de données.

/etc/systemd/system/pg1-proxy.socket

 1[Unit]
 2Description=Socket for pg1 pg proxy (24532->127.0.0.1:14532)
 3
 4[Socket]
 5ListenStream=0.0.0.0:24532
 6ReusePort=true
 7NoDelay=true
 8Backlog=128
 9
10[Install]
11WantedBy=sockets.target

3. Le proxy et le timer d’inactivité : service systemd

C’est ici que vit la logique à la demande. Lorsqu’elle est activée par le socket, ce service démarre d’abord le service réel de base de données (Requires=pg1-postgres.service). La commande ExecStartPre est une boucle shell petite mais critique qui vérifie de manière répétée si le port PostgreSQL interne est ouvert. Sans cette vérification, une condition de course pourrait survenir où le proxy démarre et redirige la connexion du client avant que le conteneur PostgreSQL n’ait fini son initialisation. Cela se traduirait par une erreur immédiate “Connection Refused” pour le client. Ce script exécuté avant le démarrage assure une transition fluide et que le client ne se connecte qu’une fois la base prête.

Le processus principal est systemd-socket-proxyd, un outil intégré qui transfert la connexion entrante vers le port interne où le conteneur PostgreSQL écoute (127.0.0.1:14532). La partie cruciale est --exit-idle-time=3min. Cela indique au proxy de quitter automatiquement s’il est inactif pendant trois minutes.

/etc/systemd/system/pg1-proxy.service

 1[Unit]
 2Description=Socket-activated TCP proxy to local Postgres on 14532
 3
 4Requires=pg1-postgres.service
 5After=pg1-postgres.service
 6
 7[Service]
 8Type=simple
 9Sockets=pg1-proxy.socket
10ExecStartPre=/bin/bash -c 'for i in {1..10}; do nc -z 127.0.0.1 14532 && exit 0; sleep 1; done; exit 0'
11ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=3min 127.0.0.1:14532

4. Le gestionnaire de conteneur : service systemd

Ce service gère le cycle de vie Docker Compose. Il est démarré par le service proxy. La directive clé est StopWhenUnneeded=true. Cela lie son cycle de vie à celui du service proxy. Lorsque pg1-proxy.service s’arrête (parce que son temporisateur d’inactivité a expiré), systemd voit que ce service n’est plus nécessaire et l’arrête automatiquement en exécutant docker-compose down. Le conteneur est arrêté, libérant toutes ses ressources.

/etc/systemd/system/pg1-postgres.service

 1[Unit]
 2Description=postgres container
 3PartOf=pg1-proxy.service
 4StopWhenUnneeded=true
 5
 6[Service]
 7WorkingDirectory=/root/pg/pg1
 8
 9Type=simple
10ExecStart=/usr/bin/docker-compose up
11ExecStop=/usr/bin/docker-compose down
12
13Restart=on-failure
14RestartSec=2s
15TimeoutStopSec=30s

Le compromis : démarrages à froid

Cette configuration est incroyablement efficace, mais elle implique une considération majeure : la latence des “démarrages à froid”. La toute première connexion à la base après une période d’inactivité sera retardée. Le client doit attendre que systemd exécute docker-compose up et que le conteneur PostgreSQL s’initialise. D’après notre expérience, cela prend environ une seconde pour une petite base, mais augmente avec la taille du stockage.

Pour beaucoup de systèmes internes — CI/CD, tâches par lots, ou tableaux de bord d’administration peu utilisés — ce délai est un compromis parfaitement acceptable au regard des économies de ressources significatives. Pour des applications de production à fort trafic et sensibles à la latence, une base de données traditionnelle toujours active reste le bon choix.

Activation du service

Pour mettre une nouvelle base en ligne, il suffit d’activer les unités systemd.

1systemctl daemon-reload
2systemctl enable pg1-proxy.service
3systemctl enable pg1-postgres.service
4systemctl enable --now pg1-proxy.socket

Une fois activée, la base est prête à accepter des connexions, mais elle ne consommera aucune ressource jusqu’à la première connexion. C’est une autre petite étape dans notre mission d’éliminer le gaspillage, démontrant que même une infrastructure essentielle comme une base de données relationnelle peut fonctionner de manière maigre et à la demande.