Haute Disponibilité : Principes, Patterns et Architectures
La haute disponibilité (HA) est la capacité d'un système à rester opérationnel malgré les pannes de ses composants. Dans un monde où une minute d'indisponibilité peut coûter des milliers d'euros, maîtriser ces concepts est essentiel.
Les fondamentaux
Définitions clés
| Terme | Définition | Exemple |
|---|---|---|
| Disponibilité | % de temps où le service est opérationnel | 99.9% = ~8.76h d'arrêt/an |
| SLA | Service Level Agreement - engagement contractuel | "99.95% de disponibilité mensuelle" |
| SLO | Service Level Objective - objectif interne | "99.99% visé" |
| SLI | Service Level Indicator - métrique mesurée | "Temps de réponse < 200ms" |
| RTO | Recovery Time Objective - temps max de restauration | "Retour en ligne en 15 min" |
| RPO | Recovery Point Objective - perte de données acceptable | "Max 5 min de transactions perdues" |
| MTBF | Mean Time Between Failures | "30 jours entre pannes" |
| MTTR | Mean Time To Recovery | "15 min pour réparer" |
Les "9" de disponibilité
┌─────────────────────────────────────────────────────────────────┐
│ NIVEAUX DE DISPONIBILITÉ (SLA) │
├──────────────┬──────────────────┬───────────────────────────────┤
│ SLA │ Downtime/an │ Cas d'usage │
├──────────────┼──────────────────┼───────────────────────────────┤
│ 99% │ 3.65 jours │ Apps internes non critiques │
│ 99.9% │ 8.76 heures │ Apps métier standard │
│ 99.95% │ 4.38 heures │ E-commerce, SaaS │
│ 99.99% │ 52.56 minutes │ Paiement, santé │
│ 99.999% │ 5.26 minutes │ Télécoms, finance critique │
│ 99.9999% │ 31.54 secondes │ Systèmes de défense │
└──────────────┴──────────────────┴───────────────────────────────┘
Formule de disponibilité
Disponibilité = MTBF / (MTBF + MTTR)
Exemple:
- MTBF = 720 heures (30 jours)
- MTTR = 0.5 heures (30 min)
- Disponibilité = 720 / (720 + 0.5) = 99.93%
:::tip Améliorer la disponibilité :
- Augmenter le MTBF → meilleure qualité, redondance
- Réduire le MTTR → automatisation, monitoring, runbooks :::
Patterns de haute disponibilité
1. Redondance active-passive (Failover)
┌─────────────────────────────────────────────────────────────────┐
│ ACTIVE-PASSIVE (FAILOVER) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Load Balancer / VIP │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ ACTIVE │ │ STANDBY │ │
│ │ Server │◄──────►│ Server │ ← Heartbeat │
│ │ (Write) │ │ (Ready) │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Primary │───────►│ Replica │ ← Réplication sync │
│ │ DB │ │ DB │ │
│ └──────────┘ └──────────┘ │
│ │
│ En cas de panne du serveur actif : │
│ 1. Heartbeat détecte la panne │
│ 2. Standby promu en Active │
│ 3. VIP/DNS bascule vers nouveau Active │
│ 4. RTO typique : 30s - 5min │
│ │
└─────────────────────────────────────────────────────────────────┘
Avantages :
- Simple à implémenter
- Coût modéré (1 serveur en standby)
- Pas de split-brain possible
Inconvénients :
- Ressources standby sous-utilisées
- Temps de failover non nul
- Single point of failure potentiel (load balancer)
2. Redondance active-active
┌─────────────────────────────────────────────────────────────────┐
│ ACTIVE-ACTIVE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Global Load Balancer │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ACTIVE-1 │ │ ACTIVE-2 │ │ ACTIVE-3 │ │
│ │ (R/W) │ │ (R/W) │ │ (R/W) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Distributed DB │ │
│ │ (Multi-Master) │ │
│ │ ou Event Store │ │
│ └─────────────────────┘ │
│ │
│ Tous les nœuds traitent le trafic simultanément │
│ Si un nœud tombe, les autres absorbent la charge │
│ RTO : ~0 (pas de failover explicite) │
│ │
└─────────────────────────────────────────────────────────────────┘
Avantages :
- Utilisation optimale des ressources
- Scalabilité horizontale
- Pas de temps de failover
Inconvénients :
- Complexité accrue (cohérence des données)
- Coût plus élevé
- Risque de split-brain si mal configuré
3. Architecture multi-région
┌─────────────────────────────────────────────────────────────────┐
│ MULTI-RÉGION (GEO-HA) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Global DNS (GeoDNS) │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ EUROPE │ │ US-EAST │ │ ASIA │ │
│ │ (Paris) │ │ (Virginia)│ │ (Tokyo) │ │
│ ├────────────┤ ├────────────┤ ├────────────┤ │
│ │ App Tier │ │ App Tier │ │ App Tier │ │
│ │ ┌──┐ ┌──┐ │ │ ┌──┐ ┌──┐ │ │ ┌──┐ ┌──┐ │ │
│ │ │P1│ │P2│ │ │ │P1│ │P2│ │ │ │P1│ │P2│ │ │
│ │ └──┘ └──┘ │ │ └──┘ └──┘ │ │ └──┘ └──┘ │ │
│ ├────────────┤ ├────────────┤ ├────────────┤ │
│ │ DB Tier │ │ DB Tier │ │ DB Tier │ │
│ │ (Primary) │◄─┤ (Replica) │◄─┤ (Replica) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ └───────────────┴───────────────┘ │
│ Cross-Region │
│ Replication │
│ │
└─────────────────────────────────────────────────────────────────┘
Implémentation pratique
Kubernetes : Configuration HA native
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-backend
namespace: production
spec:
# Haute disponibilité : minimum 3 replicas
replicas: 3
# Stratégie de déploiement sans interruption
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 1 pod supplémentaire pendant update
maxUnavailable: 0 # Jamais en dessous de 3 replicas
selector:
matchLabels:
app: api-backend
template:
metadata:
labels:
app: api-backend
spec:
# Répartition sur plusieurs nodes
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: api-backend
topologyKey: kubernetes.io/hostname
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: api-backend
topologyKey: topology.kubernetes.io/zone
# Répartition sur plusieurs zones
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-backend
containers:
- name: api
image: registry.company.com/api:v2.1.0
ports:
- containerPort: 8080
# Health checks critiques pour la HA
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
# Ressources garanties
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
---
# PodDisruptionBudget : garantir un minimum de pods disponibles
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-backend-pdb
namespace: production
spec:
minAvailable: 2 # Au moins 2 pods toujours up
selector:
matchLabels:
app: api-backend
PostgreSQL : Configuration HA avec Patroni
# Configuration Patroni pour PostgreSQL HA
scope: postgres-cluster
namespace: /postgresql/
restapi:
listen: 0.0.0.0:8008
connect_address: ${POD_IP}:8008
etcd3:
hosts:
- etcd-0.etcd:2379
- etcd-1.etcd:2379
- etcd-2.etcd:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1MB max lag pour failover
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
max_connections: 200
shared_buffers: 2GB
effective_cache_size: 6GB
work_mem: 64MB
maintenance_work_mem: 512MB
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
max_replication_slots: 10
hot_standby_feedback: "on"
synchronous_commit: "on" # Crucial pour RPO=0
initdb:
- encoding: UTF8
- data-checksums
postgresql:
listen: 0.0.0.0:5432
connect_address: ${POD_IP}:5432
data_dir: /var/lib/postgresql/data
authentication:
replication:
username: replicator
password: ${REPLICATION_PASSWORD}
superuser:
username: postgres
password: ${POSTGRES_PASSWORD}
pg_hba:
- host replication replicator 0.0.0.0/0 md5
- host all all 0.0.0.0/0 md5
watchdog:
mode: automatic
device: /dev/watchdog
safety_margin: 5
Kafka : Réplication et ISR
# Configuration Kafka pour haute disponibilité
# Identité du broker
broker.id=1
listeners=PLAINTEXT://0.0.0.0:9092
advertised.listeners=PLAINTEXT://kafka-1.kafka:9092
# Réplication - CRITIQUE pour la HA
default.replication.factor=3
min.insync.replicas=2
# Paramètres de leadership
auto.leader.rebalance.enable=true
leader.imbalance.check.interval.seconds=300
leader.imbalance.per.broker.percentage=10
# Durabilité des données
log.flush.interval.messages=10000
log.flush.interval.ms=1000
# Tolérance aux pannes
unclean.leader.election.enable=false
replica.lag.time.max.ms=30000
replica.lag.max.messages=10000
# Configuration ZooKeeper (ou KRaft)
zookeeper.connect=zk-0.zk:2181,zk-1.zk:2181,zk-2.zk:2181
zookeeper.session.timeout.ms=18000
Properties props = new Properties();
// Connexion au cluster
props.put("bootstrap.servers", "kafka-1:9092,kafka-2:9092,kafka-3:9092");
// Durabilité maximale
props.put("acks", "all"); // Attendre confirmation de tous les ISR
props.put("retries", Integer.MAX_VALUE);
props.put("retry.backoff.ms", 100);
props.put("max.in.flight.requests.per.connection", 5);
// Idempotence pour éviter les duplicatas
props.put("enable.idempotence", true);
// Timeouts adaptés
props.put("delivery.timeout.ms", 120000); // 2 minutes
props.put("request.timeout.ms", 30000);
props.put("linger.ms", 5);
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
Patterns de résilience applicative
Circuit Breaker
import time
from enum import Enum
from dataclasses import dataclass, field
from typing import Callable, TypeVar, Optional
from functools import wraps
T = TypeVar('T')
class CircuitState(Enum):
CLOSED = "closed" # Fonctionnement normal
OPEN = "open" # Court-circuit activé
HALF_OPEN = "half_open" # Test de récupération
@dataclass
class CircuitBreaker:
"""
Implémentation du pattern Circuit Breaker.
Protège les appels vers des services externes instables.
"""
failure_threshold: int = 5
recovery_timeout: int = 30
half_open_max_calls: int = 3
_state: CircuitState = field(default=CircuitState.CLOSED, init=False)
_failure_count: int = field(default=0, init=False)
_last_failure_time: float = field(default=0, init=False)
_half_open_calls: int = field(default=0, init=False)
@property
def state(self) -> CircuitState:
if self._state == CircuitState.OPEN:
if time.time() - self._last_failure_time >= self.recovery_timeout:
self._state = CircuitState.HALF_OPEN
self._half_open_calls = 0
return self._state
def call(self, func: Callable[..., T], *args, **kwargs) -> T:
"""Exécute une fonction protégée par le circuit breaker."""
if self.state == CircuitState.OPEN:
raise CircuitBreakerOpenError(
f"Circuit ouvert. Réessayer dans "
f"{self.recovery_timeout - (time.time() - self._last_failure_time):.0f}s"
)
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise
def _on_success(self):
if self._state == CircuitState.HALF_OPEN:
self._half_open_calls += 1
if self._half_open_calls >= self.half_open_max_calls:
self._state = CircuitState.CLOSED
self._failure_count = 0
else:
self._failure_count = 0
def _on_failure(self):
self._failure_count += 1
self._last_failure_time = time.time()
if self._failure_count >= self.failure_threshold:
self._state = CircuitState.OPEN
elif self._state == CircuitState.HALF_OPEN:
self._state = CircuitState.OPEN
class CircuitBreakerOpenError(Exception):
pass
# Décorateur pour simplifier l'usage
def circuit_breaker(
failure_threshold: int = 5,
recovery_timeout: int = 30
):
cb = CircuitBreaker(
failure_threshold=failure_threshold,
recovery_timeout=recovery_timeout
)
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args, **kwargs) -> T:
return cb.call(func, *args, **kwargs)
wrapper.circuit_breaker = cb
return wrapper
return decorator
# Utilisation
@circuit_breaker(failure_threshold=3, recovery_timeout=60)
def call_external_api(endpoint: str) -> dict:
"""Appel à une API externe protégé par circuit breaker."""
import requests
response = requests.get(endpoint, timeout=5)
response.raise_for_status()
return response.json()
Retry avec backoff exponentiel
import time
import random
from functools import wraps
from typing import Callable, Type, Tuple
def retry_with_backoff(
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 60.0,
exponential_base: float = 2,
jitter: bool = True,
retryable_exceptions: Tuple[Type[Exception], ...] = (Exception,)
):
"""
Décorateur de retry avec backoff exponentiel et jitter.
Args:
max_retries: Nombre maximum de tentatives
base_delay: Délai initial en secondes
max_delay: Délai maximum en secondes
exponential_base: Base de l'exponentielle (2 = doubling)
jitter: Ajouter du bruit aléatoire pour éviter thundering herd
retryable_exceptions: Exceptions qui déclenchent un retry
"""
def decorator(func: Callable):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except retryable_exceptions as e:
last_exception = e
if attempt == max_retries:
break
# Calcul du délai avec backoff exponentiel
delay = min(
base_delay * (exponential_base ** attempt),
max_delay
)
# Jitter : ajouter 0-50% de variation aléatoire
if jitter:
delay = delay * (0.5 + random.random())
print(f"Tentative {attempt + 1}/{max_retries} échouée. "
f"Retry dans {delay:.2f}s. Erreur: {e}")
time.sleep(delay)
raise last_exception
return wrapper
return decorator
# Utilisation
@retry_with_backoff(
max_retries=5,
base_delay=1.0,
retryable_exceptions=(ConnectionError, TimeoutError)
)
def fetch_data_from_service(url: str) -> dict:
"""Récupère des données avec retry automatique."""
import requests
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
Bulkhead (isolation)
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from functools import wraps
from typing import Callable, Dict
class Bulkhead:
"""
Pattern Bulkhead : isole les ressources pour éviter
qu'une défaillance ne se propage.
"""
_instances: Dict[str, 'Bulkhead'] = {}
def __init__(self, name: str, max_concurrent: int = 10, timeout: float = 30.0):
self.name = name
self.max_concurrent = max_concurrent
self.timeout = timeout
self._executor = ThreadPoolExecutor(
max_workers=max_concurrent,
thread_name_prefix=f"bulkhead-{name}"
)
@classmethod
def get_or_create(cls, name: str, **kwargs) -> 'Bulkhead':
if name not in cls._instances:
cls._instances[name] = cls(name, **kwargs)
return cls._instances[name]
def execute(self, func: Callable, *args, **kwargs):
"""Exécute une fonction dans le bulkhead."""
future = self._executor.submit(func, *args, **kwargs)
try:
return future.result(timeout=self.timeout)
except TimeoutError:
future.cancel()
raise BulkheadTimeoutError(
f"Bulkhead '{self.name}' timeout après {self.timeout}s"
)
class BulkheadTimeoutError(Exception):
pass
def bulkhead(name: str, max_concurrent: int = 10, timeout: float = 30.0):
"""Décorateur bulkhead pour isoler les appels."""
def decorator(func: Callable):
bh = Bulkhead.get_or_create(name, max_concurrent=max_concurrent, timeout=timeout)
@wraps(func)
def wrapper(*args, **kwargs):
return bh.execute(func, *args, **kwargs)
return wrapper
return decorator
# Utilisation : isolation des appels par service
@bulkhead(name="payment-service", max_concurrent=5, timeout=10.0)
def process_payment(order_id: str, amount: float) -> bool:
"""Traitement paiement isolé des autres services."""
# ... appel au service de paiement
pass
@bulkhead(name="inventory-service", max_concurrent=20, timeout=5.0)
def check_inventory(product_id: str) -> int:
"""Vérification stock isolée."""
# ... appel au service d'inventaire
pass
Monitoring et alerting HA
Métriques clés à surveiller
groups:
- name: high-availability
rules:
# Alerte sur disponibilité des pods
- alert: HighPodUnavailability
expr: |
(
kube_deployment_status_replicas_available
/ kube_deployment_spec_replicas
) < 0.5
for: 2m
labels:
severity: critical
annotations:
summary: "Moins de 50% des pods disponibles"
description: "{{ $labels.deployment }} a {{ $value | humanizePercentage }} de disponibilité"
# Alerte sur latence élevée
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "Latence P99 > 2s"
description: "{{ $labels.service }} P99 latency: {{ $value | humanizeDuration }}"
# Alerte sur taux d'erreur
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/ sum(rate(http_requests_total[5m])) by (service)
> 0.01
for: 2m
labels:
severity: critical
annotations:
summary: "Taux d'erreur > 1%"
description: "{{ $labels.service }} error rate: {{ $value | humanizePercentage }}"
# Alerte sur réplication Kafka
- alert: KafkaUnderReplicatedPartitions
expr: kafka_server_replicamanager_underreplicatedpartitions > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Partitions Kafka sous-répliquées"
description: "{{ $value }} partitions ne sont pas correctement répliquées"
# Alerte sur lag de réplication PostgreSQL
- alert: PostgresReplicationLag
expr: pg_replication_lag_seconds > 30
for: 2m
labels:
severity: warning
annotations:
summary: "Lag de réplication PostgreSQL élevé"
description: "Lag de {{ $value | humanizeDuration }} sur {{ $labels.instance }}"
Dashboard de disponibilité
# Extraits de configuration Grafana
panels:
- title: "SLA Mensuel"
type: "stat"
targets:
- expr: |
(1 - (
sum(increase(http_requests_total{status=~"5.."}[30d]))
/ sum(increase(http_requests_total[30d]))
)) * 100
fieldConfig:
defaults:
unit: "percent"
thresholds:
- value: 99.9
color: "green"
- value: 99.5
color: "yellow"
- value: 99
color: "red"
- title: "Temps de réponse P50/P95/P99"
type: "timeseries"
targets:
- expr: histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
legendFormat: "P50"
- expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
legendFormat: "P95"
- expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
legendFormat: "P99"
- title: "Disponibilité par service"
type: "table"
targets:
- expr: |
sum by (service) (
rate(http_requests_total{status!~"5.."}[24h])
) / sum by (service) (
rate(http_requests_total[24h])
) * 100
Checklist haute disponibilité
## Infrastructure
- [ ] Minimum 3 replicas pour les services critiques
- [ ] Distribution sur plusieurs zones/régions
- [ ] Load balancer redondant
- [ ] DNS avec failover automatique
- [ ] Stockage répliqué (3 copies minimum)
## Application
- [ ] Health checks (liveness + readiness)
- [ ] Graceful shutdown implémenté
- [ ] Circuit breaker sur appels externes
- [ ] Retry avec backoff exponentiel
- [ ] Timeouts configurés partout
- [ ] Idempotence des opérations critiques
## Base de données
- [ ] Réplication synchrone ou async selon RPO
- [ ] Failover automatique testé
- [ ] Backups réguliers et testés
- [ ] Point-in-time recovery configuré
## Monitoring
- [ ] Métriques SLI collectées
- [ ] Alertes sur SLO configurées
- [ ] Dashboard de disponibilité
- [ ] Logs centralisés
- [ ] Tracing distribué
## Processus
- [ ] Runbooks documentés
- [ ] Tests de chaos réguliers
- [ ] Post-mortems après incidents
- [ ] Capacity planning trimestriel
Conclusion
La haute disponibilité n'est pas un état mais un processus continu qui combine :
- Architecture résiliente : Redondance, distribution, isolation
- Code défensif : Circuit breakers, retries, timeouts
- Monitoring proactif : SLI/SLO/SLA bien définis
- Culture d'amélioration : Post-mortems, tests de chaos
:::tip Règle d'or : Chaque niveau de "9" supplémentaire coûte environ 10x plus cher. Définissez vos SLO en fonction de l'impact business réel, pas de l'ego technique. :::