CDN, load balancers et proxies#
Lorsqu’une application doit servir des milliers, des millions ou des milliards d’utilisateurs, une seule machine ne suffit plus. Les load balancers répartissent le trafic entre plusieurs serveurs ; les CDN rapprochent le contenu des utilisateurs ; les proxies inverses centralisent le TLS, le cache et l’authentification. Ce chapitre explore ces composants fondamentaux de l’infrastructure web moderne.
Reverse proxy#
Un proxy inverse (reverse proxy) se place devant les serveurs d’application et intercepte toutes les requêtes entrantes. Il est transparent pour le client : celui-ci croit communiquer directement avec le serveur final.
Différence forward proxy / reverse proxy#
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
for ax, (titre, gauche, milieu, droite, desc) in zip(axes, [
("Forward proxy",
"Client\n(interne)", "Proxy\nforward", "Serveurs\n(Internet)",
"Le client configure explicitement le proxy.\nUsage : contournement de filtrages,\nCache entreprise, anonymisation."),
("Reverse proxy",
"Clients\n(Internet)", "Proxy\ninverse", "Serveurs\n(backend)",
"Le client ne sait pas qu'un proxy existe.\nUsage : TLS termination, load balancing,\ncache, WAF, authentification."),
]):
ax.set_xlim(0, 10)
ax.set_ylim(0, 5)
ax.axis('off')
ax.set_title(titre, fontsize=12, fontweight='bold')
for x, label, col in [(1.5, gauche, '#4575b4'), (5, milieu, '#d73027'), (8.5, droite, '#1a9850')]:
ax.add_patch(mpatches.FancyBboxPatch((x-1, 1.8), 2, 1.2,
boxstyle="round,pad=0.1", facecolor=col, alpha=0.2,
edgecolor=col, linewidth=2))
ax.text(x, 2.4, label, ha='center', va='center', fontsize=9, color=col, fontweight='bold')
for x1, x2 in [(2.5, 4.0), (6.0, 7.5)]:
ax.annotate("", xy=(x2, 2.4), xytext=(x1, 2.4),
arrowprops=dict(arrowstyle='<->', color='#555555', lw=2))
ax.text(5, 0.9, desc, ha='center', va='center', fontsize=8.5, color='#333333', style='italic')
plt.tight_layout()
plt.savefig('_static/proxy_types.png', dpi=100, bbox_inches='tight')
plt.show()
Nginx comme reverse proxy#
# /etc/nginx/sites-available/app.conf
upstream backend_app {
server 10.0.1.10:8000 weight=3;
server 10.0.1.11:8000 weight=3;
server 10.0.1.12:8000 weight=1; # serveur moins puissant
keepalive 32; # connexions persistantes vers les backends
}
server {
listen 443 ssl http2;
server_name app.exemple.fr;
ssl_certificate /etc/ssl/certs/app.pem;
ssl_certificate_key /etc/ssl/private/app-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# TLS termination : le backend reçoit du HTTP en clair
location / {
proxy_pass http://backend_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Cache de réponses statiques
proxy_cache STATIC;
proxy_cache_valid 200 10m;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
}
}
Algorithmes de load balancing#
Panorama des algorithmes#
fig, ax = plt.subplots(figsize=(11, 5))
ax.axis('off')
données = [
['Round-Robin', 'Distribution cyclique', 'Simple, équitable', 'Sessions sans état'],
['Weighted R-R', 'Cyclique avec poids', 'Respecte la capacité', 'Capacités hétérogènes'],
['Least Connections', 'Vers le moins chargé', 'Optimal pour requêtes longues', 'Connexions persistantes'],
['Least Time', 'Moins de conx + RTT min', 'Optimal globalement', 'Nginx Plus, HAProxy EE'],
['IP Hash', 'Hash de l\'IP src', 'Persistance session', 'Sessions avec état (sans sticky cookie)'],
['Random', 'Aléatoire uniforme', 'Simple', 'Tests de charge'],
['Resource-Based', 'Selon CPU/RAM backend', 'Adaptatif', 'Avec health-check actif'],
]
cols = ['Algorithme', 'Principe', 'Avantage', 'Cas d\'usage']
table = ax.table(cellText=données, colLabels=cols,
cellLoc='center', loc='center',
colWidths=[0.18, 0.25, 0.27, 0.28])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 2.0)
for (row, col), cell in table.get_celld().items():
if row == 0:
cell.set_facecolor('#2c7bb6')
cell.set_text_props(color='white', fontweight='bold')
elif row % 2 == 0:
cell.set_facecolor('#f5f8fc')
cell.set_edgecolor('#dddddd')
ax.set_title("Algorithmes de load balancing", fontsize=13, fontweight='bold', pad=20)
plt.tight_layout()
plt.savefig('_static/lb_algorithms.png', dpi=100, bbox_inches='tight')
plt.show()
Simulation Python — Round-Robin avec weights#
import itertools
from dataclasses import dataclass, field
@dataclass
class Serveur:
adresse: str
poids: int = 1
connexions_actives: int = 0
requêtes_totales: int = 0
disponible: bool = True
def traiter(self):
self.connexions_actives += 1
self.requêtes_totales += 1
def terminer(self):
self.connexions_actives = max(0, self.connexions_actives - 1)
class LoadBalancer:
def __init__(self, algorithme: str = 'round_robin'):
self.serveurs: list[Serveur] = []
self.algorithme = algorithme
self._index = 0
def ajouter(self, serveur: Serveur):
self.serveurs.append(serveur)
def _serveurs_disponibles(self) -> list[Serveur]:
return [s for s in self.serveurs if s.disponible]
def choisir(self) -> Serveur | None:
dispos = self._serveurs_disponibles()
if not dispos:
return None
if self.algorithme == 'round_robin':
s = dispos[self._index % len(dispos)]
self._index += 1
return s
elif self.algorithme == 'weighted_rr':
# Développe la liste selon les poids
pool = [s for s in dispos for _ in range(s.poids)]
s = pool[self._index % len(pool)]
self._index += 1
return s
elif self.algorithme == 'least_connections':
return min(dispos, key=lambda s: s.connexions_actives)
elif self.algorithme == 'ip_hash':
raise NotImplementedError("ip_hash nécessite l'IP source")
return dispos[0]
def statistiques(self) -> pd.DataFrame:
return pd.DataFrame([{
'adresse': s.adresse,
'poids': s.poids,
'requêtes': s.requêtes_totales,
'conx_active': s.connexions_actives,
'disponible': s.disponible,
} for s in self.serveurs])
# Comparaison round-robin vs weighted vs least-connections
serveurs_config = [
('10.0.1.10', 3), # serveur puissant
('10.0.1.11', 3), # serveur puissant
('10.0.1.12', 1), # serveur moins puissant
]
fig, axes = plt.subplots(1, 3, figsize=(13, 5))
algos = ['round_robin', 'weighted_rr', 'least_connections']
titres = ['Round-Robin\n(sans pondération)', 'Weighted\nRound-Robin', 'Least\nConnections']
for ax, algo, titre in zip(axes, algos, titres):
lb = LoadBalancer(algorithme=algo)
for addr, poids in serveurs_config:
lb.ajouter(Serveur(adresse=addr, poids=poids))
# Simulation de 300 requêtes ; least_connections : durées variables
durées = np.random.exponential(1.0, 300) if algo == 'least_connections' else [0]*300
for dur in durées:
s = lb.choisir()
if s:
s.traiter()
if algo == 'least_connections':
# Simuler des fins de requêtes aléatoires
for srv in lb.serveurs:
if srv.connexions_actives > 0 and random.random() < 0.3:
srv.terminer()
# Pour least_conn — on ne compte que les totaux
df = lb.statistiques()
cols_bar = ['#4575b4', '#1a9850', '#d73027']
bars = ax.bar(df['adresse'].str.split('.').str[-1].apply(lambda x: f"Srv {x}"),
df['requêtes'], color=cols_bar, edgecolor='white')
ax.set_title(titre, fontsize=11, fontweight='bold')
ax.set_ylabel("Requêtes reçues")
for bar, val in zip(bars, df['requêtes']):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height()+2,
str(val), ha='center', fontsize=10, fontweight='bold')
# Afficher les poids
for i, (bar, poids) in enumerate(zip(bars, df['poids'])):
ax.text(bar.get_x() + bar.get_width()/2, 5,
f"w={poids}", ha='center', fontsize=8.5, color='white', fontweight='bold')
plt.suptitle("Distribution des requêtes selon l'algorithme (300 requêtes)", fontsize=13, fontweight='bold')
plt.tight_layout()
plt.savefig('_static/lb_comparison.png', dpi=100, bbox_inches='tight')
plt.show()
Health checks et circuit breaker#
Health checks actifs et passifs#
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 5)
ax1.axis('off')
ax1.set_title("Health check actif", fontsize=11, fontweight='bold')
for x, label, col in [(1.5, "Load\nBalancer", '#4575b4'), (5, "Serveur\nbackend", '#1a9850')]:
ax1.add_patch(mpatches.FancyBboxPatch((x-0.9, 1.8), 1.8, 1.0,
boxstyle="round,pad=0.1", facecolor=col, alpha=0.2,
edgecolor=col, linewidth=2))
ax1.text(x, 2.3, label, ha='center', va='center', fontsize=9, color=col, fontweight='bold')
for y, texte, col, dir_ in [
(3.5, "GET /healthz → 200 OK", '#4575b4', (1.5, 5)),
(2.9, "Réponse < 200ms", '#1a9850', (5, 1.5)),
]:
ax1.annotate("", xy=(dir_[1]-0.9, y), xytext=(dir_[0]+0.9, y),
arrowprops=dict(arrowstyle='->', color=col, lw=1.8))
ax1.text(3.5, y+0.15, texte, ha='center', fontsize=8.5, color=col)
ax1.text(5, 0.8, "Toutes les 5s — si 3 échecs → serveur retiré\nSi 2 succès → serveur réintégré",
ha='center', fontsize=8.5, color='#555555', style='italic')
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 5)
ax2.axis('off')
ax2.set_title("Circuit Breaker — états", fontsize=11, fontweight='bold')
états_cb = [
(2, 3.5, "CLOSED\n(normal)", '#1a9850'),
(5.5, 3.5, "OPEN\n(erreurs)", '#d62728'),
(8, 1.5, "HALF-OPEN\n(test)", '#f46d43'),
]
for x, y, label, col in états_cb:
ax2.add_patch(plt.Circle((x, y), 0.7, facecolor=col, alpha=0.25, edgecolor=col, linewidth=2))
ax2.text(x, y, label, ha='center', va='center', fontsize=8, color=col, fontweight='bold')
transitions = [
((2.7, 3.5), (4.8, 3.5), "Taux d'erreurs > seuil"),
((8, 2.2), (5.5, 2.8), "Sonde OK → réintégration"),
((5.5, 2.8), (8, 2.2), "Sonde KO → reste OPEN"),
]
for (x1,y1),(x2,y2),label in transitions:
ax2.annotate("", xy=(x2,y2), xytext=(x1,y1),
arrowprops=dict(arrowstyle='->', color='#555555', lw=1.5))
ax2.text((x1+x2)/2+0.1, (y1+y2)/2+0.1, label, ha='center', fontsize=7.5, color='#444444')
plt.tight_layout()
plt.savefig('_static/healthcheck_cb.png', dpi=100, bbox_inches='tight')
plt.show()
Configuration HAProxy#
# /etc/haproxy/haproxy.cfg
global
maxconn 50000
log /dev/log local0
user haproxy
group haproxy
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
option httplog
option dontlognull
option forwardfor
option http-server-close
frontend web_in
bind *:80
bind *:443 ssl crt /etc/ssl/haproxy.pem
http-request redirect scheme https unless { ssl_fc }
default_backend app_servers
# ACL — routing applicatif
acl is_api path_beg /api/
acl is_ws hdr(Upgrade) -i WebSocket
use_backend api_servers if is_api
use_backend ws_servers if is_ws
backend app_servers
balance roundrobin
option httpchk GET /healthz HTTP/1.1\r\nHost:\ localhost
http-check expect status 200
server app1 10.0.1.10:8000 check inter 5s rise 2 fall 3
server app2 10.0.1.11:8000 check inter 5s rise 2 fall 3
server app3 10.0.1.12:8000 check inter 5s rise 2 fall 3 weight 50
backend api_servers
balance leastconn
option httpchk GET /api/health
server api1 10.0.2.10:9000 check
server api2 10.0.2.11:9000 check
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:password
CDN — Content Delivery Networks#
Un CDN (Content Delivery Network) est un réseau de serveurs distribués géographiquement (Points of Presence — PoP) qui mettent en cache le contenu au plus près des utilisateurs.
Anycast#
fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xlim(-180, 180)
ax.set_ylim(-70, 85)
ax.axis('off')
ax.set_facecolor('#f0f8ff')
ax.set_title("Réseau CDN mondial — PoP et routage Anycast", fontsize=13, fontweight='bold', pad=12)
# Continents schématiques (rectangles simplifiés)
continents = [
(-130, 25, 60, 40, "Amérique du N.", '#e8e8e8'),
(-82, -55, 50, 40, "Amérique du S.", '#e8e8e8'),
(-10, 35, 40, 35, "Europe", '#e8e8e8'),
(10, -35, 50, 50, "Afrique", '#e8e8e8'),
(60, 10, 80, 50, "Asie", '#e8e8e8'),
(110, -45, 40, 35, "Océanie", '#e8e8e8'),
]
for x, y, w, h, label, col in continents:
ax.add_patch(mpatches.FancyBboxPatch((x, y), w, h,
boxstyle="round,pad=0.5", facecolor=col, edgecolor='#cccccc', linewidth=1))
ax.text(x+w/2, y+h/2, label, ha='center', va='center', fontsize=8, color='#666666')
# PoP CDN
pops = [
(-95, 40, "NYC"),
(-115, 35, "LAX"),
(-60, -20, "SAO"),
(0, 48, "PAR"),
(10, 52, "FRA"),
(28, 55, "AMS"),
(55, 24, "DXB"),
(72, 19, "BOM"),
(103, 1, "SIN"),
(116, 39, "PEK"),
(139, 35, "TYO"),
(151, -33, "SYD"),
(37, 55, "MOW"),
(-80, 43, "TOR"),
]
for lon, lat, code in pops:
ax.plot(lon, lat, 'o', markersize=9, color='#d73027', zorder=5,
markeredgecolor='white', markeredgewidth=1.5)
ax.text(lon+2, lat+2, code, fontsize=7.5, color='#333333', fontweight='bold')
# Utilisateur en Europe → PoP PAR (le plus proche)
user_lon, user_lat = 2, 46 # France
ax.plot(user_lon, user_lat, 's', markersize=12, color='#4575b4', zorder=6,
markeredgecolor='white', markeredgewidth=2)
ax.text(user_lon+2, user_lat-4, "Utilisateur\n(Paris)", fontsize=8.5, color='#4575b4', fontweight='bold')
# Flèche vers le PoP le plus proche
for (lon, lat, code), (col, label) in zip(
[(0, 48, "PAR")],
[('#d73027', '← PoP le plus proche (12 ms)')]):
ax.annotate("", xy=(lon, lat-1), xytext=(user_lon, user_lat+0.5),
arrowprops=dict(arrowstyle='->', color=col, lw=2.5))
ax.text(1, 47.5, label, fontsize=8.5, color=col, style='italic')
# Légende
ax.plot([], [], 'o', color='#d73027', markersize=9, markeredgecolor='white',
markeredgewidth=1.5, label='PoP CDN')
ax.plot([], [], 's', color='#4575b4', markersize=10, markeredgecolor='white',
markeredgewidth=2, label='Utilisateur')
ax.legend(loc='lower left', fontsize=9)
plt.tight_layout()
plt.savefig('_static/cdn_map.png', dpi=100, bbox_inches='tight')
plt.show()
Cache hit / miss et invalidation#
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# --- Flux cache hit vs miss ---
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 8)
ax1.axis('off')
ax1.set_title("Cache hit vs Cache miss", fontsize=11, fontweight='bold')
for x, y, label, col in [
(1.2, 5, "Client", '#4575b4'),
(4.5, 5, "PoP CDN\n(cache)", '#d73027'),
(8.5, 5, "Origine\n(serveur)", '#1a9850'),
]:
ax1.add_patch(mpatches.FancyBboxPatch((x-0.9, 4.4), 1.8, 1.0,
boxstyle="round,pad=0.1", facecolor=col, alpha=0.2,
edgecolor=col, linewidth=2))
ax1.text(x, 4.9, label, ha='center', va='center', fontsize=9, color=col, fontweight='bold')
# Cache HIT
for y, texte, dir_, col in [
(7.0, "GET /image.jpg", (1.2, 4.5), '#4575b4'),
(6.4, "200 OK (X-Cache: HIT) ← données du cache", (4.5, 1.2), '#d73027'),
]:
x_src, x_dst = dir_
ax1.annotate("", xy=(x_dst+0.9, y), xytext=(x_src+0.9, y),
arrowprops=dict(arrowstyle='->', color=col, lw=1.8))
ax1.text((x_src+x_dst)/2+0.9, y+0.15, texte, ha='center', fontsize=7.5, color=col)
ax1.text(5, 5.8, "Cache HIT ✓", ha='center', fontsize=10, color='#1a9850', fontweight='bold')
# Cache MISS
for y, texte, dir_, col in [
(3.8, "GET /video.mp4", (1.2, 4.5), '#4575b4'),
(3.2, "MISS → forward vers origine", (4.5, 8.5), '#888888'),
(2.6, "200 OK + cache update", (8.5, 4.5), '#1a9850'),
(2.0, "200 OK (X-Cache: MISS)", (4.5, 1.2), '#d73027'),
]:
x_src, x_dst = dir_
ax1.annotate("", xy=(x_dst+0.9, y), xytext=(x_src+0.9, y),
arrowprops=dict(arrowstyle='->', color=col, lw=1.5))
ax1.text((x_src+x_dst)/2+0.9, y+0.15, texte, ha='center', fontsize=7.5, color=col)
ax1.text(5, 1.2, "Cache MISS ✗", ha='center', fontsize=10, color='#d73027', fontweight='bold')
# --- Taux de hit simulé ---
ax2 = axes[1]
ressources = ['Images', 'CSS/JS', 'HTML\n(pages)', 'API\ndynamique', 'Vidéo\nstreaming']
taux_hit = [0.95, 0.90, 0.60, 0.05, 0.85]
cols_hit = ['#1a9850' if t > 0.7 else '#fdae61' if t > 0.3 else '#d73027' for t in taux_hit]
bars = ax2.bar(ressources, taux_hit, color=cols_hit, edgecolor='white')
ax2.axhline(0.7, color='#888888', linestyle='--', linewidth=1.5, label='Seuil 70%')
ax2.set_ylim(0, 1.1)
ax2.set_ylabel("Taux de cache hit", fontsize=11)
ax2.set_title("Taux de cache hit typiques\npar type de ressource", fontsize=11, fontweight='bold')
ax2.legend(fontsize=9)
for bar, val in zip(bars, taux_hit):
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height()+0.02,
f"{val:.0%}", ha='center', fontsize=10, fontweight='bold')
plt.tight_layout()
plt.savefig('_static/cdn_cache.png', dpi=100, bbox_inches='tight')
plt.show()
Cache HTTP#
Le cache HTTP est contrôlé par des en-têtes spécifiques. Une bonne stratégie de cache peut réduire la charge serveur de 80 à 95 %.
En-têtes de contrôle du cache#
en_têtes_cache = {
'Cache-Control: no-store': "Ne jamais cacher (données sensibles, RGPD)",
'Cache-Control: no-cache': "Cacher mais revalider systématiquement",
'Cache-Control: private, max-age=3600': "Cacher 1h côté client seulement (HTML personnalisé)",
'Cache-Control: public, max-age=86400': "Cacher 24h partout (images, CSS, JS statiques)",
'Cache-Control: public, s-maxage=3600, stale-while-revalidate=60':
"CDN : cacher 1h, puis servir stale 60s pendant la revalidation",
'Cache-Control: immutable, max-age=31536000':
"Assets versionnés (hashes dans le nom) — cacher 1 an, jamais revalider",
'ETag: "abc123"': "Empreinte du contenu — revalidation conditionnelle (If-None-Match)",
'Last-Modified: Tue, 01 Jan 2026 00:00:00 GMT':
"Date de modification — revalidation (If-Modified-Since)",
'Vary: Accept-Encoding, Accept-Language':
"Cache distinct selon l'encodage et la langue",
}
print("En-têtes HTTP de contrôle du cache")
print("=" * 70)
for entête, desc in en_têtes_cache.items():
print(f"\n {entête}")
print(f" → {desc}")
En-têtes HTTP de contrôle du cache
======================================================================
Cache-Control: no-store
→ Ne jamais cacher (données sensibles, RGPD)
Cache-Control: no-cache
→ Cacher mais revalider systématiquement
Cache-Control: private, max-age=3600
→ Cacher 1h côté client seulement (HTML personnalisé)
Cache-Control: public, max-age=86400
→ Cacher 24h partout (images, CSS, JS statiques)
Cache-Control: public, s-maxage=3600, stale-while-revalidate=60
→ CDN : cacher 1h, puis servir stale 60s pendant la revalidation
Cache-Control: immutable, max-age=31536000
→ Assets versionnés (hashes dans le nom) — cacher 1 an, jamais revalider
ETag: "abc123"
→ Empreinte du contenu — revalidation conditionnelle (If-None-Match)
Last-Modified: Tue, 01 Jan 2026 00:00:00 GMT
→ Date de modification — revalidation (If-Modified-Since)
Vary: Accept-Encoding, Accept-Language
→ Cache distinct selon l'encodage et la langue
LRU Cache HTTP en Python#
class CacheHTTPLRU:
"""
Cache HTTP LRU (Least Recently Used) simplifié.
Simule le comportement d'un proxy de cache.
"""
def __init__(self, capacité: int = 5):
self.capacité = capacité
self._cache: OrderedDict[str, dict] = OrderedDict()
self.hits = 0
self.misses = 0
def _est_expiré(self, entrée: dict) -> bool:
return time.time() > entrée['expires_at']
def get(self, url: str) -> dict | None:
if url in self._cache:
entrée = self._cache[url]
if not self._est_expiré(entrée):
# Déplacer en fin (recently used)
self._cache.move_to_end(url)
self.hits += 1
return entrée['data']
else:
# Expiré — invalider
del self._cache[url]
self.misses += 1
return None
def set(self, url: str, données: dict, max_age: int = 60):
if url in self._cache:
self._cache.move_to_end(url)
elif len(self._cache) >= self.capacité:
# Éviction LRU : retirer l'entrée la moins récemment utilisée
victime, _ = self._cache.popitem(last=False)
print(f" [LRU] Éviction : {victime}")
self._cache[url] = {
'data': données,
'expires_at': time.time() + max_age,
'max_age': max_age,
}
@property
def taux_hit(self) -> float:
total = self.hits + self.misses
return self.hits / total if total > 0 else 0.0
def afficher(self):
print(f"\nCache ({len(self._cache)}/{self.capacité} entrées) :")
for url, entrée in self._cache.items():
ttl_restant = max(0, entrée['expires_at'] - time.time())
print(f" {url:<35} TTL={ttl_restant:.0f}s")
print(f" Hit rate : {self.taux_hit:.1%} ({self.hits} hits / {self.misses} misses)")
cache = CacheHTTPLRU(capacité=4)
# Simulation de requêtes
requêtes = [
'/static/logo.png',
'/static/app.css',
'/api/users', # dynamique — ne sera pas caché longtemps
'/static/logo.png', # HIT
'/static/bundle.js',
'/static/app.css', # HIT
'/static/fonts/roboto.woff2', # → éviction de la plus ancienne
'/static/logo.png', # HIT
]
print("Simulation de requêtes vers le cache LRU :")
print("-" * 50)
for url in requêtes:
résultat = cache.get(url)
if résultat is None:
# Simuler la récupération depuis l'origine
données_origine = {'status': 200, 'body': f'Contenu de {url}', 'size': len(url) * 100}
max_age = 10 if '/api/' in url else 3600
cache.set(url, données_origine, max_age=max_age)
print(f" MISS : {url}")
else:
print(f" HIT : {url}")
cache.afficher()
Simulation de requêtes vers le cache LRU :
--------------------------------------------------
MISS : /static/logo.png
MISS : /static/app.css
MISS : /api/users
HIT : /static/logo.png
MISS : /static/bundle.js
HIT : /static/app.css
[LRU] Éviction : /api/users
MISS : /static/fonts/roboto.woff2
HIT : /static/logo.png
Cache (4/4 entrées) :
/static/bundle.js TTL=3600s
/static/app.css TTL=3600s
/static/fonts/roboto.woff2 TTL=3600s
/static/logo.png TTL=3600s
Hit rate : 37.5% (3 hits / 5 misses)
Service mesh : Envoy, Istio#
Un service mesh ajoute une couche de proxy transparent entre tous les microservices d’une architecture, gérant automatiquement :
mTLS (mutual TLS) entre tous les services.
Load balancing L7 avancé.
Observabilité : traces distribuées, métriques, logs.
Politiques de trafic : retries, timeouts, circuit breaking.
fig, ax = plt.subplots(figsize=(11, 6))
ax.set_xlim(0, 11)
ax.set_ylim(0, 7)
ax.axis('off')
ax.set_title("Architecture Service Mesh (Istio / Envoy)", fontsize=13, fontweight='bold', pad=12)
# Plan de données
services = [
(2, 5.5, "Service A\n(app)"),
(5.5, 5.5, "Service B\n(app)"),
(9, 5.5, "Service C\n(app)"),
(2, 2.5, "Service D\n(app)"),
(5.5, 2.5, "Service E\n(app)"),
(9, 2.5, "Service F\n(app)"),
]
proxies = [
(2, 4.5, "Envoy\nsidecar"),
(5.5, 4.5, "Envoy\nsidecar"),
(9, 4.5, "Envoy\nsidecar"),
(2, 3.5, "Envoy\nsidecar"),
(5.5, 3.5, "Envoy\nsidecar"),
(9, 3.5, "Envoy\nsidecar"),
]
for (sx, sy, slabel), (px, py, plabel) in zip(services, proxies):
ax.add_patch(mpatches.FancyBboxPatch((sx-0.7, sy-0.3), 1.4, 0.7,
boxstyle="round,pad=0.05", facecolor='#4575b4', alpha=0.2,
edgecolor='#4575b4', linewidth=2))
ax.text(sx, sy, slabel, ha='center', va='center', fontsize=8, color='#4575b4', fontweight='bold')
ax.add_patch(mpatches.FancyBboxPatch((px-0.5, py-0.25), 1.0, 0.6,
boxstyle="round,pad=0.05", facecolor='#d73027', alpha=0.2,
edgecolor='#d73027', linewidth=1.5))
ax.text(px, py, plabel, ha='center', va='center', fontsize=7.5, color='#d73027', fontweight='bold')
# Connexions mTLS entre proxies
connexions = [(2, 4.5, 5.5, 4.5), (5.5, 4.5, 9, 4.5), (2, 3.5, 5.5, 3.5),
(5.5, 3.5, 9, 3.5), (5.5, 4.5, 5.5, 3.5)]
for x1, y1, x2, y2 in connexions:
ax.plot([x1+0.5, x2-0.5], [y1, y2], '-', color='#d62728', linewidth=1.5, alpha=0.7)
mx, my = (x1+x2)/2, (y1+y2)/2
ax.text(mx, my+0.12, "mTLS", ha='center', fontsize=6.5, color='#d62728', style='italic')
# Control plane Istio
ax.add_patch(mpatches.FancyBboxPatch((3.5, 0.3), 4, 1.2,
boxstyle="round,pad=0.1", facecolor='#1a9850', alpha=0.15,
edgecolor='#1a9850', linewidth=2))
ax.text(5.5, 0.9, "Istiod (control plane) — xDS API", ha='center', va='center',
fontsize=10, color='#1a9850', fontweight='bold')
ax.text(5.5, 0.5, "Pilot (routage) · Citadel (certificats) · Galley (config)",
ha='center', va='center', fontsize=8, color='#1a9850')
# Flèches control plane → sidecars
for x in [2, 5.5, 9]:
ax.annotate("", xy=(x, 3.25), xytext=(x, 1.5),
arrowprops=dict(arrowstyle='<->', color='#1a9850', lw=1.2, linestyle='dashed'))
plt.tight_layout()
plt.savefig('_static/service_mesh.png', dpi=100, bbox_inches='tight')
plt.show()
Simulation complète — topologie CDN et décision de routage#
# Simulation de la décision de routage Anycast / CDN
class PointOfPresence:
def __init__(self, code: str, ville: str, continent: str,
lat: float, lon: float, capacité: float = 100.0):
self.code = code
self.ville = ville
self.continent = continent
self.lat = lat
self.lon = lon
self.capacité = capacité
self.charge_actuelle = 0.0
@property
def charge_pct(self) -> float:
return self.charge_actuelle / self.capacité * 100
def distance_vers(self, lat: float, lon: float) -> float:
"""Distance approchée (formule sphérique simplifiée)."""
d_lat = abs(self.lat - lat)
d_lon = abs(self.lon - lon)
return (d_lat**2 + d_lon**2)**0.5
def latence_estimée(self, lat: float, lon: float) -> float:
"""Latence estimée en ms = distance * 0.5 + charge * 0.2."""
dist = self.distance_vers(lat, lon)
return dist * 0.5 + self.charge_pct * 0.2
pops = [
PointOfPresence("NYC", "New York", "Amérique", 40.7, -74.0, 100),
PointOfPresence("LAX", "Los Angeles","Amérique", 34.0, -118.2, 80),
PointOfPresence("PAR", "Paris", "Europe", 48.9, 2.3, 90),
PointOfPresence("FRA", "Francfort", "Europe", 50.1, 8.7, 85),
PointOfPresence("SIN", "Singapour", "Asie", 1.3, 103.8, 75),
PointOfPresence("TYO", "Tokyo", "Asie", 35.7, 139.7, 70),
PointOfPresence("SYD", "Sydney", "Océanie", -33.9, 151.2, 50),
PointOfPresence("DXB", "Dubaï", "Asie", 25.2, 55.3, 60),
]
# Simuler des charges
charges = [45, 72, 38, 55, 80, 25, 15, 50]
for pop, charge in zip(pops, charges):
pop.charge_actuelle = charge
def routage_cdn(lat_client: float, lon_client: float,
pops: list[PointOfPresence]) -> PointOfPresence:
"""Choisit le PoP avec la latence estimée la plus basse."""
return min(pops, key=lambda p: p.latence_estimée(lat_client, lon_client))
# Test avec plusieurs clients
clients = [
("Paris, France", 48.9, 2.3),
("New York, USA", 40.7, -74.0),
("Mumbai, Inde", 19.1, 72.9),
("Melbourne, Australie", -37.8, 144.9),
("Moscou, Russie", 55.8, 37.6),
("São Paulo, Brésil", -23.5, -46.6),
]
print(f"{'Client':<25} {'PoP choisi':<8} {'Ville':<15} {'Latence est. (ms)':<20} {'Charge PoP'}")
print("-" * 80)
for nom, lat, lon in clients:
pop = routage_cdn(lat, lon, pops)
lat_est = pop.latence_estimée(lat, lon)
print(f"{nom:<25} {pop.code:<8} {pop.ville:<15} {lat_est:<20.1f} {pop.charge_pct:.0f}%")
Client PoP choisi Ville Latence est. (ms) Charge PoP
--------------------------------------------------------------------------------
Paris, France PAR Paris 8.4 42%
New York, USA NYC New York 9.0 45%
Mumbai, Inde DXB Dubaï 26.0 83%
Melbourne, Australie SYD Sydney 9.7 30%
Moscou, Russie PAR Paris 26.4 42%
São Paulo, Brésil NYC New York 43.9 45%
# Visualisation de la charge des PoP et de la décision de routage
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# Charge par PoP
ax1 = axes[0]
noms_pops = [p.code for p in pops]
charges_pct = [p.charge_pct for p in pops]
couleurs_charge = ['#d73027' if c > 75 else '#fdae61' if c > 50 else '#1a9850' for c in charges_pct]
bars = ax1.bar(noms_pops, charges_pct, color=couleurs_charge, edgecolor='white')
ax1.axhline(75, color='#d73027', linestyle='--', linewidth=1.5, label='Seuil alerte (75%)')
ax1.axhline(50, color='#fdae61', linestyle='--', linewidth=1.2, label='Seuil attention (50%)')
ax1.set_ylabel("Charge (%)", fontsize=11)
ax1.set_title("Charge actuelle des PoP CDN", fontsize=12, fontweight='bold')
ax1.set_ylim(0, 100)
ax1.legend(fontsize=9)
for bar, val in zip(bars, charges_pct):
ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height()+1,
f"{val:.0f}%", ha='center', fontsize=9)
# Latence estimée vers chaque PoP depuis Paris
ax2 = axes[1]
lat_paris, lon_paris = 48.9, 2.3
latences = [p.latence_estimée(lat_paris, lon_paris) for p in pops]
cols_latence = sns.color_palette('coolwarm_r', len(pops))
bars2 = ax2.barh(noms_pops, latences, color=cols_latence, edgecolor='white')
ax2.set_xlabel("Latence estimée depuis Paris (ms)", fontsize=11)
ax2.set_title("Classement des PoP\n(depuis Paris)", fontsize=12, fontweight='bold')
pop_choisi = routage_cdn(lat_paris, lon_paris, pops)
for bar, pop, lat in zip(bars2, pops, latences):
suffixe = " ← CHOISI" if pop.code == pop_choisi.code else ""
col = '#d73027' if pop.code == pop_choisi.code else '#333333'
ax2.text(lat + 0.3, bar.get_y() + bar.get_height()/2,
f"{lat:.1f} ms{suffixe}", va='center', fontsize=9, color=col,
fontweight='bold' if suffixe else 'normal')
plt.tight_layout()
plt.savefig('_static/cdn_routing.png', dpi=100, bbox_inches='tight')
plt.show()
Résumé#
Points clés du chapitre
Un proxy inverse centralise TLS, cache, authentification et load balancing ; le client ne connaît que l’adresse du proxy.
Nginx avec
upstreamsupporte nativement le round-robin pondéré, le least_connections et le keepalive HTTP vers les backends.HAProxy est spécialisé dans le load balancing haute performance, avec des ACL puissantes pour le routage applicatif L7.
Les CDN réduisent la latence en servant le contenu depuis le PoP le plus proche de l’utilisateur grâce à Anycast : une même adresse IP est annoncée depuis des dizaines de villes, et BGP route vers la plus proche.
Le cache HTTP repose sur
Cache-Control,ETagetLast-Modified; les assets statiques versionnés peuvent être mis en cache pour un an (immutable, max-age=31536000).Un service mesh (Istio/Envoy) ajoute mTLS, observabilité et politiques de trafic à tous les services sans modifier leur code, grâce aux sidecars.
Le circuit breaker protège les backends surchargés en interrompant temporairement les appels vers un service défaillant.