Skip to content

Latest commit

 

History

History
1629 lines (1305 loc) · 47.3 KB

File metadata and controls

1629 lines (1305 loc) · 47.3 KB

🔝 Retour au Sommaire

19.3.5. Tests de migration et validation

Introduction

La migration vers PostgreSQL 18 est un événement majeur pour votre infrastructure. Comme pour tout changement critique, tester avant de migrer n'est pas optionnel, c'est essentiel. Cette section vous guide à travers les différentes étapes de tests et de validation pour garantir une migration réussie.

Pourquoi tester est crucial

Analogie : Le voyage en avion

Imaginez prendre un avion sans que celui-ci ait été testé :

Sans tests :

✈️ Avion neuf → Embarquement direct → Vol avec passagers
   ↓
   ❌ Risque d'accident catastrophique

Avec tests :

✈️ Avion neuf → Tests au sol → Vol d'essai vide → Vol avec équipage réduit
   → Vol d'essai avec quelques passagers → Vol commercial normal
   ↓
   ✅ Sécurité maximale

Pour PostgreSQL, c'est identique :

PostgreSQL 18 → Tests en DEV → Tests en STAGING → Tests de charge
   → Migration de préproduction → Migration production
   ↓
   ✅ Migration réussie et sécurisée

Les risques sans tests

┌─────────────────────────────────────────────────────────────┐
│  Conséquences d'une migration non testée                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ Incompatibilité SQL découverte en production            │
│     → Applications qui plantent                             │
│     → Perte de revenus                                      │
│                                                             │
│  ❌ Performances dégradées inattendues                      │
│     → Requêtes 10× plus lentes                              │
│     → Timeout utilisateurs                                  │
│                                                             │
│  ❌ Perte de données ou corruption                          │
│     → Types de données incompatibles                        │
│     → Contraintes violées                                   │
│                                                             │
│  ❌ Rollback impossible ou difficile                        │
│     → Downtime prolongé                                     │
│     → Panique et stress de l'équipe                         │
│                                                             │
│  💰 Coût : Millions d'euros potentiels                      │
│  😰 Stress : Maximal                                        │
│  👥 Réputation : Endommagée                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Les bénéfices des tests

✅ Confiance : L'équipe sait que ça fonctionne
✅ Anticipation : Problèmes découverts avant production
✅ Documentation : Procédures validées et documentées
✅ Rollback préparé : Plan B testé et prêt
✅ Sérénité : Migration en production sans surprises
✅ Business continuity : Service maintenu sans interruption

Les environnements de test

Pyramide des environnements

┌─────────────────────────────────────────────────────────────┐
│  Pyramide des environnements de test                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                       🏢 PRODUCTION                         │
│                    ┌──────────────────┐                     │
│                    │   PostgreSQL 18  │                     │
│                    │   (Migration finale)                   │
│                    └──────────────────┘                     │
│                           ↑                                 │
│                  ────────────────────                       │
│                 /                    \                      │
│                                                             │
│              🔧 STAGING / PRÉPRODUCTION                     │
│           ┌──────────────────────────────┐                  │
│           │   Clone quasi-identique prod │                  │
│           │   Tests finaux réalistes     │                  │
│           │   Validation performance     │                  │
│           └──────────────────────────────┘                  │
│                        ↑                                    │
│               ────────────────────                          │
│              /                    \                         │
│                                                             │
│                   💻 DÉVELOPPEMENT                          │
│          ┌──────────────────────────────────┐               │
│          │   Environnement développeur      │               │
│          │   Tests unitaires et intégration │               │
│          │   Découverte des incompatibilités│               │
│          └──────────────────────────────────┘               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Description des environnements

1. Développement (DEV)

Caractéristiques :

Données : Sous-ensemble ou données de test (< 1 GB)  
Infrastructure : Serveur modeste (2-4 cores, 8 GB RAM)  
Objectif : Découvrir rapidement les incompatibilités  
Fréquence : Tests quotidiens, multiples itérations  
Audience : Développeurs uniquement  

Utilisation :

-- DEV : Tester les requêtes SQL sur PG 18
-- Exemple : Vérifier qu'une requête fonctionne toujours

-- Cette requête fonctionnait sur PG 17
SELECT * FROM users WHERE id = ANY('{1,2,3}');

-- Vérifier qu'elle fonctionne toujours sur PG 18
-- Tester les nouvelles fonctionnalités
SELECT * FROM users WHERE created_at > NOW() - INTERVAL '1 day';

Avantages :

  • Cycle de test rapide (minutes)
  • Permet d'itérer rapidement
  • Pas d'impact sur la production

Limitations :

  • Données limitées (ne détecte pas tous les problèmes)
  • Performances non représentatives
  • Certains bugs n'apparaissent qu'avec de vraies données

2. Staging / Préproduction (STAGING)

Caractéristiques :

Données : Clone de production ou anonymisées (taille réelle)  
Infrastructure : Identique ou proche de production  
Objectif : Validation finale avant migration prod  
Fréquence : Tests hebdomadaires ou avant chaque release  
Audience : Équipe complète (dev, ops, QA, business)  

Utilisation :

# Créer un clone de production pour STAGING
# Option 1 : Via pg_dump/restore
pg_dump -h prod-server -U postgres mydb | \
    psql -h staging-server -U postgres mydb

# Option 2 : Via snapshot disque (AWS, Azure)
aws rds create-db-snapshot --db-instance-identifier staging-pg18

# Option 3 : Via réplication puis promotion
# (Utiliser réplication physique, puis promouvoir le replica)

Tests à effectuer :

  • Migration complète avec pg_upgrade
  • Tests de charge réalistes
  • Validation avec applications réelles
  • Tests de rollback

Avantages :

  • Données réelles (ou anonymisées)
  • Performances réalistes
  • Détecte les vrais problèmes

Limitations :

  • Coût infrastructure (serveur dédié)
  • Synchronisation des données avec prod (peuvent être obsolètes)

3. Production (PROD)

Caractéristiques :

Données : Données réelles  
Infrastructure : Configuration optimale  
Objectif : Service en conditions réelles  
Migration : Après validation complète en DEV et STAGING  

Principe :

La production n'est PAS un environnement de test !  
On y applique uniquement ce qui a été validé en STAGING.  

Stratégie de données de test

Anonymisation des données

Pour STAGING, il est souvent nécessaire d'anonymiser les données sensibles :

-- Exemple : Anonymiser les données personnelles
-- Sur le clone STAGING après copie

-- Anonymiser emails
UPDATE users  
SET email = 'user_' || id || '@test.example.com';  

-- Anonymiser noms
UPDATE users  
SET  
    first_name = 'FirstName' || id,
    last_name = 'LastName' || id;

-- Anonymiser adresses
UPDATE addresses  
SET  
    street = 'Test Street ' || id,
    city = 'TestCity',
    postal_code = LPAD(id::text, 5, '0');

-- Supprimer données très sensibles
DELETE FROM payment_methods;  
UPDATE orders SET notes = 'Test order';  

Outils d'anonymisation :

  • pg_anonymize : Extension PostgreSQL
  • PostgreSQL Anonymizer : Solution complète
  • Scripts personnalisés SQL

Données synthétiques

Alternative : Générer des données de test réalistes

-- Générer 1 million d'utilisateurs de test
INSERT INTO users (first_name, last_name, email, created_at)  
SELECT  
    'User' || generate_series AS first_name,
    'Test' || generate_series AS last_name,
    'user' || generate_series || '@test.com' AS email,
    NOW() - (random() * INTERVAL '365 days') AS created_at
FROM generate_series(1, 1000000);

-- Générer des commandes aléatoires
INSERT INTO orders (user_id, total, status, created_at)  
SELECT  
    (random() * 1000000)::int + 1 AS user_id,
    (random() * 1000)::numeric(10,2) AS total,
    (ARRAY['pending', 'paid', 'shipped', 'delivered'])[floor(random() * 4 + 1)] AS status,
    NOW() - (random() * INTERVAL '90 days') AS created_at
FROM generate_series(1, 5000000);

Phase 1 : Tests pré-migration

Analyse de compatibilité

Vérifier les dépréciations PostgreSQL 18

PostgreSQL 18 peut déprécier certaines fonctionnalités ou changer des comportements :

-- 1. Identifier les fonctions dépréciées
-- Consulter les release notes PostgreSQL 18

-- 2. Rechercher l'utilisation de md5 (déprécié en faveur de scram-sha-256)
SELECT usename, passwd  
FROM pg_shadow  
WHERE passwd LIKE 'md5%';  

-- Si résultats, migrer vers scram-sha-256 avant migration

-- 3. Vérifier les types de données personnalisés
SELECT typname, typtype  
FROM pg_type  
WHERE typtype = 'c'  -- composite types  
    AND typnamespace::regnamespace::text NOT IN ('pg_catalog', 'information_schema');

Vérifier la compatibilité des extensions

-- Lister toutes les extensions installées
SELECT
    extname AS extension_name,
    extversion AS version,
    extrelocatable AS relocatable
FROM pg_extension  
ORDER BY extname;  

-- Vérifier la disponibilité dans PostgreSQL 18
-- Consulter : https://www.postgresql.org/docs/18/

Extensions populaires et compatibilité PG 18 :

✅ PostGIS : Compatible (version 3.4+)
✅ pg_stat_statements : Inclus (amélioré)
✅ pgcrypto : Compatible
✅ uuid-ossp : Compatible (mais UUIDv7 natif maintenant)
✅ hstore : Compatible
✅ pg_trgm : Compatible
✅ pgvector : Compatible (version 0.6.0+)
⚠️  Extension_X : Vérifier la documentation spécifique

Analyser les requêtes SQL

-- Identifier les requêtes qui utilisent des syntaxes potentiellement problématiques

-- 1. Activer pg_stat_statements (si pas déjà fait)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- 2. Collecter les requêtes les plus fréquentes
SELECT
    query,
    calls,
    mean_exec_time,
    total_exec_time
FROM pg_stat_statements  
WHERE query NOT LIKE 'SELECT % FROM pg_%'  -- Exclure requêtes système  
ORDER BY calls DESC  
LIMIT 100;  

-- 3. Exporter pour analyse
\copy (SELECT query FROM pg_stat_statements ORDER BY calls DESC LIMIT 1000) TO '/tmp/queries.sql'

Vérifier manuellement :

  • Utilisation de fonctionnalités dépréciées
  • Requêtes non standard
  • Utilisations de types spécifiques

Test de pg_upgrade --check

# Sur un clone de production en DEV
pg_upgrade \
    --old-datadir=/var/lib/postgresql/17/main \
    --new-datadir=/var/lib/postgresql/18/main \
    --old-bindir=/usr/lib/postgresql/17/bin \
    --new-bindir=/usr/lib/postgresql/18/bin \
    --check

# Sortie :
# Performing Consistency Checks
# -----------------------------
# Checking cluster versions                                  ok
# Checking database user is the install user                 ok
# Checking database connection settings                      ok
# Checking for prepared transactions                         ok
# ...

# Si "ok" partout → Compatible ✅
# Si erreurs → À corriger avant migration

Tests des applications

Test de connexion

# Python : Vérifier que l'application se connecte à PG 18
import psycopg2

try:
    conn = psycopg2.connect(
        host="staging-pg18",
        database="mydb",
        user="appuser",
        password="secret"
    )

    cursor = conn.cursor()
    cursor.execute("SELECT version();")
    version = cursor.fetchone()
    print(f"✅ Connexion réussie : {version[0]}")

    cursor.close()
    conn.close()
except Exception as e:
    print(f"❌ Erreur de connexion : {e}")
// Node.js : Test similaire
const { Pool } = require('pg');

const pool = new Pool({
    host: 'staging-pg18',
    database: 'mydb',
    user: 'appuser',
    password: 'secret'
});

pool.query('SELECT version()', (err, result) => {
    if (err) {
        console.error('❌ Erreur:', err);
    } else {
        console.log('✅ Version:', result.rows[0].version);
    }
    pool.end();
});

Tests unitaires et d'intégration

# Lancer les tests automatisés contre PostgreSQL 18
# Exemple avec Jest (Node.js)
DATABASE_URL=postgresql://user:pass@staging-pg18:5432/mydb npm test

# Exemple avec pytest (Python)
DATABASE_URL=postgresql://user:pass@staging-pg18:5432/mydb pytest

# Observer les résultats
# ✅ 1250 tests passed
# ❌ 3 tests failed ← À investiguer !

Analyser les échecs :

Test failed: test_user_creation  
Error: column "uuid" does not exist  

→ Investigation : UUID généré différemment en PG 18 ?
→ Solution : Utiliser UUIDv7 natif ou ajuster le code

Tests fonctionnels manuels

Créer une checklist de tests fonctionnels :

□ Authentification utilisateur
  □ Login avec email/password
  □ Login avec OAuth
  □ Logout

□ Opérations CRUD basiques
  □ Créer un utilisateur
  □ Lire les données utilisateur
  □ Mettre à jour un profil
  □ Supprimer un compte

□ Fonctionnalités métier critiques
  □ Créer une commande
  □ Payer avec carte bancaire
  □ Rechercher des produits
  □ Exporter un rapport

□ Fonctionnalités avancées
  □ Full-text search
  □ Filtres complexes
  □ Agrégations et statistiques

Tests de performance

Établir une baseline (référence)

Sur PostgreSQL 17 (production actuelle) :

-- 1. Activer le timing
\timing on

-- 2. Exécuter des requêtes représentatives et noter les temps
-- Requête 1 : Recherche utilisateur
SELECT * FROM users WHERE email = 'test@example.com';
-- Time: 2.345 ms

-- Requête 2 : Liste des commandes récentes
SELECT * FROM orders  
WHERE created_at > NOW() - INTERVAL '7 days'  
ORDER BY created_at DESC  
LIMIT 100;  
-- Time: 45.678 ms

-- Requête 3 : Agrégation complexe
SELECT
    DATE_TRUNC('day', created_at) AS day,
    COUNT(*) AS orders_count,
    SUM(total) AS revenue
FROM orders  
WHERE created_at > NOW() - INTERVAL '30 days'  
GROUP BY day  
ORDER BY day;  
-- Time: 234.567 ms

Documenter dans un fichier baseline.txt :

PostgreSQL 17 - Baseline Performance
====================================
Date: 2024-11-23  
Server: production-pg17 (16 cores, 64 GB RAM, NVMe)  

Query 1 - User search: 2.345 ms  
Query 2 - Recent orders: 45.678 ms  
Query 3 - Daily aggregation: 234.567 ms  
Query 4 - Complex join: 567.890 ms  
...

Comparer avec PostgreSQL 18

Sur STAGING PostgreSQL 18 :

# Script de benchmark
#!/bin/bash
# benchmark_pg18.sh

echo "PostgreSQL 18 - Performance Benchmark"  
echo "======================================"  
date  

psql -h staging-pg18 -U postgres -d mydb << 'EOF'
\timing on

-- Query 1
SELECT * FROM users WHERE email = 'test@example.com';

-- Query 2
SELECT * FROM orders  
WHERE created_at > NOW() - INTERVAL '7 days'  
ORDER BY created_at DESC  
LIMIT 100;  

-- Query 3
SELECT
    DATE_TRUNC('day', created_at) AS day,
    COUNT(*) AS orders_count,
    SUM(total) AS revenue
FROM orders  
WHERE created_at > NOW() - INTERVAL '30 days'  
GROUP BY day  
ORDER BY day;  

-- Autres requêtes...
EOF

Résultats :

PostgreSQL 18 - Performance Benchmark
======================================

Query 1 - User search: 2.123 ms (✅ 9% plus rapide)  
Query 2 - Recent orders: 38.456 ms (✅ 16% plus rapide)  
Query 3 - Daily aggregation: 198.765 ms (✅ 15% plus rapide)  
Query 4 - Complex join: 456.789 ms (✅ 20% plus rapide)  

Interprétation :

  • Si performances similaires ou meilleures : ✅ Bon signe
  • Si dégradation < 10% : ⚠️ Acceptable (peut être optimisé)
  • Si dégradation > 20% : ❌ Investigation requise

Tests de charge avec pgbench

# 1. Initialiser pgbench sur STAGING
pgbench -i -s 100 -h staging-pg18 -U postgres mydb
# s=100 → ~10 GB de données

# 2. Benchmark en lecture seule
pgbench -c 50 -j 4 -T 300 -S -h staging-pg18 -U postgres mydb
# -c 50 : 50 clients concurrents
# -j 4 : 4 threads
# -T 300 : Durée 5 minutes
# -S : Read-only (SELECT only)

# Résultats :
# transaction type: <builtin: select only>
# scaling factor: 100
# number of clients: 50
# number of threads: 4
# duration: 300 s
# number of transactions: 1234567
# tps = 4115.223456 (including connections establishing)
# tps = 4116.789012 (excluding connections establishing)

# 3. Benchmark en lecture/écriture
pgbench -c 50 -j 4 -T 300 -h staging-pg18 -U postgres mydb

# 4. Comparer avec PostgreSQL 17
# Exécuter les mêmes tests sur PG 17 et comparer les TPS

Benchmark personnalisé :

# Script SQL personnalisé pour pgbench
cat > custom_benchmark.sql << 'EOF'
-- Simulation de charge réaliste
BEGIN;
-- Insert
INSERT INTO orders (user_id, total, status)  
VALUES (random() * 1000000, random() * 1000, 'pending');  
-- Update
UPDATE users SET last_login = NOW() WHERE id = :id;
-- Select
SELECT * FROM products WHERE category = 'electronics' LIMIT 10;  
COMMIT;  
EOF  

# Exécuter
pgbench -c 20 -j 4 -T 60 -f custom_benchmark.sql -h staging-pg18 mydb

Tests de charge applicative

# test_load.py - Simuler une charge applicative réelle
import psycopg2  
import random  
import time  
from concurrent.futures import ThreadPoolExecutor  

def simulate_user_session(user_id):
    """Simule une session utilisateur"""
    conn = psycopg2.connect(
        host="staging-pg18",
        database="mydb",
        user="appuser",
        password="secret"
    )
    cursor = conn.cursor()

    try:
        # 1. Login (SELECT)
        cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        user = cursor.fetchone()

        # 2. Browse products (SELECT with JOIN)
        cursor.execute("""
            SELECT p.*, c.name as category_name
            FROM products p
            JOIN categories c ON p.category_id = c.id
            WHERE p.active = true
            LIMIT 20
        """)
        products = cursor.fetchall()

        # 3. Create order (INSERT)
        cursor.execute("""
            INSERT INTO orders (user_id, total, status)
            VALUES (%s, %s, 'pending')
            RETURNING id
        """, (user_id, random.uniform(10, 500)))
        order_id = cursor.fetchone()[0]

        # 4. Update profile (UPDATE)
        cursor.execute("""
            UPDATE users
            SET last_activity = NOW()
            WHERE id = %s
        """, (user_id,))

        conn.commit()
        return True

    except Exception as e:
        conn.rollback()
        print(f"Error for user {user_id}: {e}")
        return False
    finally:
        cursor.close()
        conn.close()

# Simuler 100 utilisateurs concurrents
with ThreadPoolExecutor(max_workers=100) as executor:
    start = time.time()
    results = list(executor.map(simulate_user_session, range(1, 1001)))
    duration = time.time() - start

    success_rate = sum(results) / len(results) * 100
    print(f"Duration: {duration:.2f}s")
    print(f"Success rate: {success_rate:.1f}%")
    print(f"Throughput: {len(results)/duration:.2f} sessions/sec")

Phase 2 : Tests pendant la migration

Simulation de migration complète

Sur STAGING : Répétition générale

#!/bin/bash
# full_migration_rehearsal.sh
# Simulation complète de la migration

echo "🎬 Répétition générale de la migration"  
echo "======================================"  

# 1. État initial
echo "📊 État initial"  
psql -h staging-pg17 -c "\l+"  
psql -h staging-pg17 -d mydb -c "SELECT COUNT(*) FROM users;"  

# 2. Arrêt de PostgreSQL 17
echo "⏸️  Arrêt PostgreSQL 17"  
sudo systemctl stop postgresql@17-main  

# 3. Migration pg_upgrade
echo "🔄 Migration pg_upgrade"  
time sudo -u postgres pg_upgrade \  
    --old-datadir=/var/lib/postgresql/17/main \
    --new-datadir=/var/lib/postgresql/18/main \
    --old-bindir=/usr/lib/postgresql/17/bin \
    --new-bindir=/usr/lib/postgresql/18/bin \
    --jobs=8 \
    --swap

# 4. Démarrage PostgreSQL 18
echo "▶️  Démarrage PostgreSQL 18"  
sudo systemctl start postgresql@18-main  

# 5. Validation immédiate
echo "✅ Validation"  
psql -h localhost -p 5432 -c "SELECT version();"  
psql -h localhost -d mydb -c "SELECT COUNT(*) FROM users;"  

# 6. Tests de fumée
echo "💨 Smoke tests"
./smoke_tests.sh

# 7. Mesure du downtime
DOWNTIME=$(grep "Upgrade Complete" pg_upgrade_server.log | \
    awk '{print $NF}')
echo "⏱️  Downtime simulé: $DOWNTIME"

echo "✅ Répétition terminée"

Chronométrer chaque étape

Résultats de la répétition générale :
=====================================

Arrêt PG 17 :           15 secondes  
pg_upgrade --check :    8 minutes  
pg_upgrade migration :  45 minutes  
Démarrage PG 18 :       30 secondes  
Smoke tests :           2 minutes  

DOWNTIME TOTAL ESTIMÉ : 47 minutes

Tests de rollback

Il est crucial de tester le rollback avant la migration en production :

#!/bin/bash
# test_rollback.sh

echo "🔙 Test de rollback"  
echo "==================="  

# Scénario : Migration PG 18 effectuée, mais problème détecté

# 1. État actuel : PG 18 en production simulée
echo "État actuel : PostgreSQL 18"  
psql -h staging-pg18 -c "SELECT version();"  

# 2. Simulation d'un problème
echo "⚠️  Problème détecté : Performance dégradée"

# 3. Arrêt PG 18
echo "⏸️  Arrêt PostgreSQL 18"  
sudo systemctl stop postgresql@18-main  

# 4. Restauration PG 17 depuis .old (si --swap utilisé)
echo "🔄 Restauration PostgreSQL 17"  
sudo -u postgres mv /var/lib/postgresql/18/main /var/lib/postgresql/18/main.failed  
sudo -u postgres mv /var/lib/postgresql/.old /var/lib/postgresql/17/main  

# 5. Redémarrage PG 17
echo "▶️  Redémarrage PostgreSQL 17"  
sudo systemctl start postgresql@17-main  

# 6. Validation
echo "✅ Validation rollback"  
psql -h localhost -c "SELECT version();"  
psql -h localhost -d mydb -c "SELECT COUNT(*) FROM users;"  

# 7. Mesurer le temps
echo "⏱️  Temps de rollback: < 2 minutes"

echo "✅ Rollback réussi"

Important : Le rollback n'est possible que si aucune donnée n'a été modifiée sur PG 18 après la migration.

Phase 3 : Tests post-migration

Validation immédiate (T+0 à T+1h)

Checklist de validation post-migration

#!/bin/bash
# post_migration_validation.sh

echo "✅ Validation post-migration PostgreSQL 18"  
echo "=========================================="  

# 1. Version
echo "1️⃣  Vérification version"  
VERSION=$(psql -t -c "SELECT version();" | grep "PostgreSQL 18")  
if [ -n "$VERSION" ]; then  
    echo "✅ Version PostgreSQL 18 confirmée"
else
    echo "❌ Version incorrecte !"
    exit 1
fi

# 2. Connexions
echo "2️⃣  Vérification connexions"  
CONNECTIONS=$(psql -t -c "SELECT count(*) FROM pg_stat_activity;")  
echo "   Connexions actives : $CONNECTIONS"  

# 3. Réplicas (si applicable)
echo "3️⃣  Vérification réplication"  
psql -c "SELECT * FROM pg_stat_replication;"  

# 4. Intégrité données
echo "4️⃣  Vérification intégrité données"  
psql -d mydb << 'EOF'  
-- Compter les enregistrements de chaque table
SELECT
    schemaname,
    tablename,
    n_live_tup
FROM pg_stat_user_tables  
ORDER BY n_live_tup DESC  
LIMIT 20;  
EOF  

# 5. Extensions
echo "5️⃣  Vérification extensions"  
psql -d mydb -c "SELECT extname, extversion FROM pg_extension;"  

# 6. Test de lecture
echo "6️⃣  Test de lecture"  
psql -d mydb -c "SELECT COUNT(*) FROM users;"  

# 7. Test d'écriture
echo "7️⃣  Test d'écriture"  
psql -d mydb << 'EOF'  
BEGIN;  
INSERT INTO test_migration (message, created_at)  
VALUES ('Post-migration test', NOW());  
SELECT * FROM test_migration WHERE message = 'Post-migration test';  
ROLLBACK;  
EOF  

# 8. Performance simple
echo "8️⃣  Test performance"  
psql -d mydb -c "\timing on" -c "SELECT COUNT(*) FROM orders;"  

echo ""  
echo "✅ Validation post-migration terminée"  

Tests de fumée (Smoke tests)

# smoke_tests.py - Tests rapides post-migration

import psycopg2  
import sys  

def run_smoke_tests():
    """Exécute les tests de fumée essentiels"""

    tests_passed = 0
    tests_failed = 0

    try:
        conn = psycopg2.connect(
            host="localhost",
            database="mydb",
            user="postgres",
            password="secret"
        )
        cursor = conn.cursor()

        # Test 1: Version
        print("🧪 Test 1: Version PostgreSQL")
        cursor.execute("SELECT version();")
        version = cursor.fetchone()[0]
        if "PostgreSQL 18" in version:
            print("   ✅ PASS")
            tests_passed += 1
        else:
            print(f"   ❌ FAIL - Version: {version}")
            tests_failed += 1

        # Test 2: Lecture table users
        print("🧪 Test 2: Lecture table users")
        cursor.execute("SELECT COUNT(*) FROM users;")
        count = cursor.fetchone()[0]
        if count > 0:
            print(f"   ✅ PASS - {count} users")
            tests_passed += 1
        else:
            print("   ❌ FAIL - Aucun utilisateur")
            tests_failed += 1

        # Test 3: Écriture (transaction rollback)
        print("🧪 Test 3: Écriture")
        conn.autocommit = False
        cursor.execute("""
            INSERT INTO users (email, first_name, last_name)
            VALUES ('test@migration.com', 'Test', 'Migration')
            RETURNING id
        """)
        user_id = cursor.fetchone()[0]
        conn.rollback()
        print(f"   ✅ PASS - INSERT réussi (rollback)")
        tests_passed += 1

        # Test 4: Index fonctionnent
        print("🧪 Test 4: Index")
        cursor.execute("SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname = 'public' LIMIT 5;")
        indexes = cursor.fetchall()
        if len(indexes) > 0:
            print(f"   ✅ PASS - {len(indexes)} index trouvés")
            tests_passed += 1
        else:
            print("   ❌ FAIL - Aucun index")
            tests_failed += 1

        # Test 5: Extensions
        print("🧪 Test 5: Extensions")
        cursor.execute("SELECT extname FROM pg_extension WHERE extname = 'pg_stat_statements';")
        if cursor.fetchone():
            print("   ✅ PASS - Extensions chargées")
            tests_passed += 1
        else:
            print("   ⚠️  WARNING - pg_stat_statements non trouvé")

        cursor.close()
        conn.close()

        # Résumé
        print("\n" + "="*50)
        print(f"Tests réussis : {tests_passed}")
        print(f"Tests échoués : {tests_failed}")

        if tests_failed > 0:
            print("❌ ÉCHEC - Certains tests ont échoué")
            sys.exit(1)
        else:
            print("✅ SUCCÈS - Tous les tests sont passés")
            sys.exit(0)

    except Exception as e:
        print(f"❌ ERREUR CRITIQUE : {e}")
        sys.exit(1)

if __name__ == "__main__":
    run_smoke_tests()

Validation continue (T+1h à T+24h)

Monitoring des performances

-- Surveiller les requêtes lentes dans les premières heures
-- (pg_stat_statements doit être activé)

-- Top 20 des requêtes les plus lentes
SELECT
    substring(query, 1, 100) AS query_preview,
    calls,
    mean_exec_time,
    max_exec_time,
    total_exec_time,
    stddev_exec_time
FROM pg_stat_statements  
WHERE mean_exec_time > 100  -- > 100ms  
ORDER BY mean_exec_time DESC  
LIMIT 20;  

-- Comparer avec la baseline PG 17
-- Si dégradation importante, investiguer

Monitoring des ressources

#!/bin/bash
# monitor_resources.sh
# À lancer toutes les 5 minutes via cron

echo "📊 Monitoring PostgreSQL 18 - $(date)"

# CPU
echo "CPU:"  
top -b -n 1 | grep postgres | head -5  

# RAM
echo "RAM:"  
free -h | grep Mem  

# Disk I/O
echo "Disk I/O:"  
iostat -x 1 2 | tail -n +4  

# Connexions actives
echo "Connexions:"  
psql -t -c "SELECT count(*) FROM pg_stat_activity WHERE state = 'active';"  

# Cache hit ratio
echo "Cache Hit Ratio:"  
psql -t -c "  
SELECT  
    ROUND(
        (sum(blks_hit) * 100.0 / NULLIF(sum(blks_hit + blks_read), 0)), 2
    ) AS cache_hit_ratio
FROM pg_stat_database;
"

# Locks en attente
LOCKS=$(psql -t -c "SELECT count(*) FROM pg_locks WHERE NOT granted;")  
if [ "$LOCKS" -gt "0" ]; then  
    echo "⚠️  $LOCKS locks en attente"
fi

echo "---"

Alertes automatiques

# monitoring_alerts.py - Alertes automatiques

import psycopg2  
import smtplib  
from email.mime.text import MIMEText  

def check_and_alert():
    """Vérifie l'état et envoie des alertes si nécessaire"""

    conn = psycopg2.connect(
        host="localhost",
        database="mydb",
        user="postgres"
    )
    cursor = conn.cursor()

    alerts = []

    # Alert 1: Connexions > 90% de max_connections
    cursor.execute("""
        SELECT
            count(*) AS current,
            (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max
        FROM pg_stat_activity
    """)
    current, max_conn = cursor.fetchone()
    if current > max_conn * 0.9:
        alerts.append(f"⚠️  Connexions élevées: {current}/{max_conn}")

    # Alert 2: Cache hit ratio < 90%
    cursor.execute("""
        SELECT
            ROUND((sum(blks_hit) * 100.0 / NULLIF(sum(blks_hit + blks_read), 0)), 2)
        FROM pg_stat_database
    """)
    cache_ratio = cursor.fetchone()[0]
    if cache_ratio < 90:
        alerts.append(f"⚠️  Cache hit ratio faible: {cache_ratio}%")

    # Alert 3: Requêtes lentes
    cursor.execute("""
        SELECT count(*)
        FROM pg_stat_activity
        WHERE state = 'active'
            AND now() - query_start > interval '5 minutes'
    """)
    long_queries = cursor.fetchone()[0]
    if long_queries > 0:
        alerts.append(f"⚠️  {long_queries} requêtes longues (> 5 min)")

    # Alert 4: Locks en attente
    cursor.execute("SELECT count(*) FROM pg_locks WHERE NOT granted")
    waiting_locks = cursor.fetchone()[0]
    if waiting_locks > 10:
        alerts.append(f"⚠️  {waiting_locks} locks en attente")

    # Envoyer les alertes
    if alerts:
        message = "\n".join(alerts)
        send_alert(message)
        print(message)
    else:
        print("✅ Tout va bien")

    cursor.close()
    conn.close()

def send_alert(message):
    """Envoie une alerte par email"""
    # Configuration email
    msg = MIMEText(message)
    msg['Subject'] = '⚠️  Alerte PostgreSQL 18 Migration'
    msg['From'] = 'monitoring@example.com'
    msg['To'] = 'dba@example.com'

    # Envoyer (exemple avec SMTP)
    # server = smtplib.SMTP('smtp.example.com')
    # server.send_message(msg)
    # server.quit()

if __name__ == "__main__":
    check_and_alert()

Validation approfondie (T+24h à T+7 jours)

Comparaison détaillée avec PG 17

-- Comparer les statistiques d'exécution avec PG 17
-- (nécessite d'avoir sauvegardé les stats de PG 17 avant migration)

-- Créer une table pour archiver les stats PG 17
CREATE TABLE pg17_baseline AS  
SELECT  
    query,
    calls,
    mean_exec_time,
    total_exec_time
FROM pg_stat_statements;

-- Après migration vers PG 18, comparer
SELECT
    pg17.query,
    pg17.mean_exec_time AS pg17_mean_time,
    pg18.mean_exec_time AS pg18_mean_time,
    ROUND(
        ((pg18.mean_exec_time - pg17.mean_exec_time) / pg17.mean_exec_time * 100)::numeric,
        2
    ) AS percent_change
FROM pg17_baseline pg17  
JOIN pg_stat_statements pg18 ON pg17.query = pg18.query  
WHERE pg17.calls > 100  -- Seulement les requêtes fréquentes  
ORDER BY ABS(pg18.mean_exec_time - pg17.mean_exec_time) DESC  
LIMIT 50;  

-- Identifier les régressions de performance
SELECT *  
FROM (  
    -- Même requête que ci-dessus
) AS comparison
WHERE percent_change > 20  -- > 20% plus lent  
ORDER BY percent_change DESC;  

Tests de régression

# regression_tests.py - Tests de régression automatisés

import psycopg2  
import json  
import time  

def load_baseline(filename='baseline_pg17.json'):
    """Charge les performances baseline de PG 17"""
    with open(filename, 'r') as f:
        return json.load(f)

def run_performance_tests(baseline):
    """Exécute les tests de performance et compare avec baseline"""

    conn = psycopg2.connect(
        host="localhost",
        database="mydb",
        user="postgres"
    )
    cursor = conn.cursor()

    results = []
    regressions = []

    for test in baseline['tests']:
        query = test['query']
        expected_time = test['mean_time_ms']

        # Exécuter la requête 10 fois et moyenner
        times = []
        for _ in range(10):
            start = time.time()
            cursor.execute(query)
            cursor.fetchall()
            duration = (time.time() - start) * 1000  # En ms
            times.append(duration)

        actual_time = sum(times) / len(times)
        difference = ((actual_time - expected_time) / expected_time) * 100

        result = {
            'query': query[:100],
            'expected': expected_time,
            'actual': actual_time,
            'difference_pct': difference
        }
        results.append(result)

        # Régression si > 20% plus lent
        if difference > 20:
            regressions.append(result)
            print(f"❌ RÉGRESSION: {query[:50]}... ({difference:.1f}% plus lent)")
        elif difference < -10:
            print(f"✅ AMÉLIORATION: {query[:50]}... ({abs(difference):.1f}% plus rapide)")
        else:
            print(f"✅ OK: {query[:50]}... ({difference:.1f}%)")

    # Rapport final
    print("\n" + "="*70)
    print(f"Tests total: {len(results)}")
    print(f"Régressions: {len(regressions)}")

    if regressions:
        print("\n⚠️  RÉGRESSIONS DÉTECTÉES:")
        for reg in regressions:
            print(f"  - {reg['query']}")
            print(f"    Attendu: {reg['expected']:.2f}ms")
            print(f"    Actuel: {reg['actual']:.2f}ms")
            print(f"    Différence: +{reg['difference_pct']:.1f}%")
    else:
        print("\n✅ Aucune régression détectée")

    cursor.close()
    conn.close()

    return len(regressions) == 0

if __name__ == "__main__":
    baseline = load_baseline()
    success = run_performance_tests(baseline)
    exit(0 if success else 1)

Outils de test

pgTAP : Tests unitaires pour PostgreSQL

-- Installation
CREATE EXTENSION pgtap;

-- Exemple de tests
BEGIN;  
SELECT plan(5);  -- Nombre de tests prévus  

-- Test 1: La table existe
SELECT has_table('public', 'users', 'Table users should exist');

-- Test 2: La colonne existe
SELECT has_column('public', 'users', 'email', 'Column email should exist');

-- Test 3: L'index existe
SELECT has_index('public', 'users', 'idx_users_email', 'Index on email should exist');

-- Test 4: Contrainte PK
SELECT has_pk('public', 'users', 'users should have a primary key');

-- Test 5: Nombre d'enregistrements
SELECT ok(
    (SELECT count(*) FROM users) > 0,
    'users table should not be empty'
);

SELECT * FROM finish();  
ROLLBACK;  

pgBadger : Analyse des logs

# Analyser les logs PostgreSQL 18
pgbadger /var/log/postgresql/postgresql-18-main.log \
    -o /var/www/html/pgbadger_report.html

# Générer un rapport HTML avec :
# - Requêtes les plus lentes
# - Requêtes les plus fréquentes
# - Distribution des temps de réponse
# - Erreurs et warnings

check-pgactivity : Monitoring Nagios-like

# Installer
apt-get install check-pgactivity

# Vérifier les connexions
check_pgactivity -s connections --critical=90% --warning=80%

# Vérifier les backends bloqués
check_pgactivity -s backends_status --critical=5 --warning=2

# Vérifier le temps des transactions
check_pgactivity -s oldest_xact --critical=1h --warning=30m

# Intégrer dans un système de monitoring (Nagios, Icinga, etc.)

Scripts personnalisés

#!/bin/bash
# comprehensive_test_suite.sh
# Suite de tests complète pour validation migration

echo "🧪 Suite de tests PostgreSQL 18 Migration"  
echo "========================================="  

FAILED=0

# Test 1: Version
echo "Test 1: Version PostgreSQL"  
if psql -t -c "SELECT version();" | grep -q "PostgreSQL 18"; then  
    echo "✅ PASS"
else
    echo "❌ FAIL"
    FAILED=$((FAILED + 1))
fi

# Test 2: Extensions
echo "Test 2: Extensions critiques"  
for ext in pg_stat_statements uuid-ossp pgcrypto; do  
    if psql -t -c "SELECT 1 FROM pg_extension WHERE extname = '$ext';" | grep -q 1; then
        echo "$ext présent"
    else
        echo "$ext manquant"
        FAILED=$((FAILED + 1))
    fi
done

# Test 3: Tables critiques
echo "Test 3: Tables critiques"  
for table in users orders products; do  
    COUNT=$(psql -t -d mydb -c "SELECT count(*) FROM $table;")
    if [ "$COUNT" -gt 0 ]; then
        echo "$table: $COUNT rows"
    else
        echo "$table: vide"
        FAILED=$((FAILED + 1))
    fi
done

# Test 4: Index
echo "Test 4: Index critiques"  
MISSING_INDEXES=$(psql -t -d mydb -c "  
    SELECT count(*)
    FROM pg_indexes
    WHERE schemaname = 'public'
    AND indexname IN ('idx_users_email', 'idx_orders_user_id', 'idx_products_category')
")
if [ "$MISSING_INDEXES" -eq 3 ]; then
    echo "✅ Tous les index présents"
else
    echo "❌ Index manquants"
    FAILED=$((FAILED + 1))
fi

# Test 5: Performance baseline
echo "Test 5: Performance baseline"  
TIME=$(psql -t -d mydb -c "\timing on" -c "SELECT count(*) FROM orders;" 2>&1 | grep "Time" | awk '{print $2}')  
if [ $(echo "$TIME < 1000" | bc) -eq 1 ]; then  
    echo "✅ Performance OK ($TIME ms)"
else
    echo "⚠️  Performance degradée ($TIME ms)"
fi

# Résumé
echo ""  
echo "========================================="  
if [ "$FAILED" -eq 0 ]; then  
    echo "✅ SUCCÈS - Tous les tests sont passés"
    exit 0
else
    echo "❌ ÉCHEC - $FAILED test(s) échoué(s)"
    exit 1
fi

Documentation des tests

Template de rapport de test

# Rapport de Test - Migration PostgreSQL 18

## Informations générales

- **Date du test** : 2024-11-23
- **Environnement** : STAGING
- **Testeur** : Jean Dupont
- **Version source** : PostgreSQL 17.2
- **Version cible** : PostgreSQL 18.0
- **Taille base** : 1.5 TB
- **Méthode migration** : pg_upgrade --swap --jobs=8

## Tests pré-migration

### Analyse de compatibilité
- ✅ pg_upgrade --check : OK
- ✅ Extensions compatibles : OK
- ✅ Types de données : OK
- ⚠️  3 tables sans PK (corrigé)

### Tests applications
- ✅ Connexion : OK
- ✅ Tests unitaires : 1247/1250 passés
- ❌ 3 tests échoués :
  - test_uuid_generation (résolu: utiliser gen_random_uuid())
  - test_json_aggregation (résolu: syntaxe JSON ajustée)
  - test_full_text_search (en cours d'investigation)

### Tests performance
- ✅ Baseline établie
- ✅ Benchmark pgbench : +15% TPS vs PG 17

## Tests migration

### Répétition générale
- ✅ Migration complète : OK
- ✅ Durée : 47 minutes
- ✅ Downtime estimé : 50 minutes (avec marge)
- ✅ Rollback testé : OK (< 2 minutes)

## Tests post-migration

### Validation immédiate
- ✅ Version : PostgreSQL 18.0
- ✅ Connexions : OK
- ✅ Intégrité données : OK
- ✅ Extensions : Toutes présentes
- ✅ Smoke tests : OK

### Validation continue (24h)
- ✅ Monitoring : Aucune alerte
- ✅ Cache hit ratio : 97.5% (excellent)
- ✅ Requêtes lentes : Aucune régression
- ⚠️  1 requête +12% plus lente (acceptable)

## Problèmes rencontrés

### Problème 1 : Tests unitaires UUID
**Description** : 3 tests échouaient car UUID généré différemment  
**Solution** : Utiliser gen_random_uuid() natif PG 18  
**Statut** : ✅ Résolu  

### Problème 2 : Full-text search
**Description** : 1 test FTS échoue  
**Solution** : En cours d'investigation  
**Statut** : ⚠️  En cours  

## Recommandations

1. ✅ GO pour migration production
2. ⚠️  Résoudre le problème FTS avant (non bloquant)
3. 📊 Prévoir monitoring renforcé 48h post-migration
4. 🔄 Conserver Blue 2 semaines minimum

## Conclusion

**Migration PostgreSQL 18 validée pour PRODUCTION**

Signature : _________________  
Date : _________________  

Bonnes pratiques et recommandations

Avant les tests

□ Planifier suffisamment de temps (2-4 semaines minimum)
□ Impliquer toutes les parties prenantes (dev, ops, QA, business)
□ Provisionner environnements dédiés (DEV, STAGING)
□ Documenter l'état actuel (baseline)
□ Préparer les scripts de test automatisés
□ Former l'équipe sur PostgreSQL 18 (nouveautés)

Pendant les tests

□ Commencer par DEV (itérations rapides)
□ Progresser vers STAGING (tests réalistes)
□ Documenter chaque problème rencontré
□ Mesurer systématiquement les performances
□ Tester les cas limites et edge cases
□ Valider le rollback fonctionne

Après les tests

□ Compiler un rapport de test complet
□ Obtenir l'approbation des stakeholders
□ Planifier la migration production (date, fenêtre)
□ Communiquer largement (équipes, utilisateurs)
□ Préparer le plan de communication de crise
□ Documenter les procédures (runbook)

Checklist complète de validation

┌─────────────────────────────────────────────────────────────┐
│  Checklist Complète - Validation Migration PostgreSQL 18    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  PHASE 1 : PRÉ-MIGRATION                                    │
│  □ pg_upgrade --check : OK                                  │
│  □ Compatibilité extensions vérifiée                        │
│  □ Compatibilité applications testée                        │
│  □ Performance baseline documentée                          │
│  □ Tests de charge effectués                                │
│  □ Rollback testé et validé                                 │
│                                                             │
│  PHASE 2 : MIGRATION                                        │
│  □ Répétition générale réussie                              │
│  □ Downtime mesuré et acceptable                            │
│  □ Procédures documentées                                   │
│  □ Équipe formée et prête                                   │
│                                                             │
│  PHASE 3 : POST-MIGRATION                                   │
│  □ Validation immédiate (T+0) : OK                          │
│  □ Smoke tests passés                                       │
│  □ Monitoring actif                                         │
│  □ Performance validée (T+24h)                              │
│  □ Aucune régression détectée (T+7j)                        │
│  □ Rapport final rédigé                                     │
│                                                             │
│  ✅ Migration validée pour PRODUCTION                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Conclusion

Les tests et la validation sont la clé du succès d'une migration PostgreSQL 18. Investir le temps nécessaire dans les tests permet d'éviter les catastrophes en production.

Points clés à retenir

  1. Tester, tester, tester : Jamais trop de tests
  2. Environnements multiples : DEV → STAGING → PROD
  3. Tests automatisés : Scripts réutilisables et fiables
  4. Documentation : Tracer chaque étape et problème
  5. Performance : Comparer systématiquement avec baseline
  6. Rollback : Toujours testé avant migration prod

Temps recommandé

Projet petit (< 100 GB) :
- Tests DEV : 1 semaine
- Tests STAGING : 1 semaine
- Total : 2 semaines minimum

Projet moyen (100 GB - 1 TB) :
- Tests DEV : 2 semaines
- Tests STAGING : 2 semaines
- Total : 4 semaines (1 mois)

Projet large (> 1 TB) :
- Tests DEV : 3 semaines
- Tests STAGING : 3 semaines
- Total : 6-8 semaines (2 mois)

Citations d'experts

"Weeks of coding can save hours of planning."

"If you don't have time to do it right, when will you have time to do it over?" - John Wooden

"Test early, test often, test in production... wait, no, not that last one." - DevOps Wisdom

Ressources complémentaires

Pour approfondir :

  • Section 19.3.1 à 19.3.4 : Techniques de migration avancées
  • Section 19.4 : Troubleshooting et gestion de crises
  • Section 14 : Observabilité et monitoring
  • Section 13 : Optimisation et performance

Note : Tester une migration peut sembler long et fastidieux, mais c'est comme apprendre à conduire : vous passez du temps sur le simulateur et avec un moniteur avant de conduire seul sur l'autoroute. Les tests vous donnent la confiance et l'expérience nécessaires pour réussir la migration en production sans surprises désagréables. Ne sautez jamais cette étape !

⏭️ Troubleshooting et Crises