Applications réparties¶
Client-serveur, appels distants et serialisation — les briques elementaires de la communication entre composants.
Le modèle client-serveur¶
Le modèle client-serveur est la base de toute architecture distribuée. Un client envoie une requête, un serveur la traite et retourné une réponse. Simple en apparence, mais les implications en système distribué sont profondes.
Variantes du modèle¶
| Variante | Description | Exemple |
|---|---|---|
| 2-tiers | Client communique directement avec le serveur | Application desktop + base SQL |
| 3-tiers | Présentation, logique métier, données sur des couches séparées | Web app + API + base de données |
| N-tiers | Decomposition fine en services specialises | Microservices derriere un gateway |
| Peer-to-peer | Chaque nœud est à la fois client et serveur | BitTorrent, blockchain, gossip |
Le passage de 2-tiers a 3-tiers a été motive par la séparation des responsabilités : le client ne parle plus directement à la base de données. Le passage a N-tiers suit la même logique — chaque couche devient un service indépendant avec son propre cycle de vie.
Problèmes inhérents¶
Distribuer introduit huit erreurs classiques (Peter Deutsch, "Fallacies of Distributed Computing") :
- Le réseau est fiable
- La latence est nulle
- La bande passante est infinie
- Le réseau est sécurisé
- La topologie ne change pas
- Il y a un seul administrateur
- Le coût de transport est nul
- Le réseau est homogène
Chaque architecture distribuée doit traiter explicitement ces réalités : timeouts, retries, circuit breakers, serialisation efficace, authentification inter-services.
Remote Procédure Call (RPC)¶
L'idée du RPC : appeler une procédure distante comme si elle etait locale. Le programmeur écrit un appel de fonction normal, l'infrastructure se charge de la communication réseau.
Architecture du RPC¶
graph LR
subgraph Client
CC[Code client]
CS[Client stub]
end
subgraph Reseau
M["Marshalling<br/>(serialisation)"]
end
subgraph Serveur
SS[Server skeleton]
SC[Code serveur]
end
CC --"appel local"--> CS
CS --"serialise + envoie"--> M
M --"deserialise"--> SS
SS --"appel local"--> SC
SC --"retour"--> SS
SS --"serialise + envoie"--> M
M --"deserialise"--> CS
CS --"retour"--> CC Stub (côté client) : proxy local qui à la même signature que la méthode distante. Il serialise les parametres, envoie la requête sur le réseau, attend la réponse et la deserialise.
Skeleton (côté serveur) : reçoit la requête, deserialise les parametres, appelle la méthode réelle, serialise le résultat et le renvoie.
Marshalling : processus de conversion des objets en mémoire en une représentation transmissible sur le réseau (octets). Le demarshalling fait l'inverse. C'est la serialisation appliquee au contexte RPC.
Semantiques d'appel¶
Le réseau peut perdre des messages. Comment le client sait-il si son appel a été traite ?
| Semantique | Garantie | Risque |
|---|---|---|
| At-most-once | L'appel est exécuté 0 ou 1 fois | Le client ne sait pas si exécuté |
| At-least-once | L'appel est exécuté au moins 1 fois (retry auto) | Effet de bord duplique |
| Exactly-once | L'appel est exécuté exactement 1 fois | Très coûteux a garantir |
En pratique, at-least-once + idempotence est le choix standard. Le serveur reçoit potentiellement le même appel plusieurs fois et produit le même résultat. Exactly-once nécessité un protocole de commit en deux phases, rarement justifie.
gRPC¶
gRPC (Google, 2015) est le standard moderne du RPC. Il utilisé HTTP/2 comme transport et Protocol Buffers comme format de serialisation.
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User);
rpc CreateUser(CreateUserRequest) returns (User);
}
message GetUserRequest {
string id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
Les quatre modes de communication gRPC :
| Mode | Client | Serveur | Cas d'usage |
|---|---|---|---|
| Unary | 1 requête | 1 réponse | CRUD classique |
| Server streaming | 1 requête | N réponses | Feed d'événements, pagination |
| Client streaming | N requêtes | 1 réponse | Upload de fichier, batch |
| Bidirectional streaming | N requêtes | N réponses | Chat, collaboration temps réel |
gRPC vs REST
gRPC excelle pour la communication inter-services : typage fort, performance binaire, streaming natif, génération de code automatique. REST reste preferable pour les APIs publiques : universalite, outils de debug (curl, navigateur), documentation lisible.
RMI Java¶
Java RMI (Remote Method Invocation) est l'implémentation Java du RPC. Moins utilise aujourd'hui que gRPC, il illustre les concepts fondamentaux avec clarte.
Architecture RMI¶
graph TD
subgraph Client JVM
CL[Client]
ST[Stub<br/>genere automatiquement]
end
subgraph RMI Registry
REG[Registry<br/>port 1099]
end
subgraph Server JVM
SK[Skeleton]
SV[Service impl]
end
CL --"1. lookup(name)"--> REG
REG --"2. retourne stub"--> CL
CL --"3. appel methode"--> ST
ST --"4. serialise + envoie"--> SK
SK --"5. appel local"--> SV
SV --"6. retour"--> SK
SK --"7. serialise + envoie"--> ST Le RMI Registry est un annuaire ou les services s'enregistrent avec un nom. Le client fait un lookup pour obtenir le stub, puis appelle les méthodes comme si l'objet etait local.
Serialisation Java¶
RMI utilise la serialisation Java native (Serializable). Chaque objet est converti en flux d'octets avec ses metadonnees de classe. C'est pratique mais problematique :
- Version : le
serialVersionUIDdoit correspondre entre client et serveur - Sécurité : la deserialisation peut exécuter du code arbitraire (vulnérabilités de deserialisation)
- Performance : le format est verbeux (metadonnees de classe incluses)
- Interopérabilité : lie au langage Java
Ces limitations ont motive la migration vers des formats de serialisation indépendants du langage.
Formats de serialisation¶
Le choix du format de serialisation impacte la performance, l'interopérabilité et l'évolution du schéma.
Comparaison des formats¶
| Format | Type | Lisible | Taille | Vitesse | Schéma | Langages |
|---|---|---|---|---|---|---|
| JSON | Texte | Oui | Moyenne | Moyenne | Optionnel | Tous |
| XML | Texte | Oui | Grande | Lente | XSD | Tous |
| Protobuf | Binaire | Non | Petite | Rapide | Obligatoire | Principaux |
| Avro | Binaire | Non | Petite | Rapide | Obligatoire | JVM, Python |
| MessagePack | Binaire | Non | Petite | Très rapide | Non | Tous |
| FlatBuffers | Binaire | Non | Petite | Zero-copy | Obligatoire | Principaux |
JSON¶
Standard de facto pour les APIs REST. Lisible, universel, supporte par tous les langages sans librairie externe.
{
"id": "usr-001",
"name": "Alice Martin",
"email": "alice@example.com",
"roles": ["admin", "reviewer"],
"created_at": "2025-03-15T10:30:00Z"
}
Limites : pas de schéma natif (JSON Schéma existe mais est optionnel), types limites (pas de date, pas de binaire natif, pas de distinction int/float), verbeux pour les gros volumes.
XML¶
Predecesseur de JSON pour les APIs web (SOAP). Schéma puissant (XSD) avec validation, namespaces, et transformations (XSLT). Encore present dans les systèmes legacy, les protocoles bancaires (ISO 20022), et les configurations complexes.
<user xmlns="urn:example:users:v1">
<id>usr-001</id>
<name>Alice Martin</name>
<email>alice@example.com</email>
<roles>
<role>admin</role>
<role>reviewer</role>
</roles>
</user>
Le ratio signal/bruit est faible : les balises occupent souvent plus de place que les données. Pour les APIs modernes, JSON a gagne.
Protocol Buffers (Protobuf)¶
Format binaire avec schéma obligatoire. Les champs sont identifiés par numéro, pas par nom — ce qui permet de renommer un champ sans casser la compatibilité.
message User {
string id = 1;
string name = 2;
string email = 3;
repeated string roles = 4;
google.protobuf.Timestamp created_at = 5;
}
Règles d'évolution du schéma :
- Ajouter un champ : nouveau numéro, les anciens clients ignorent le champ inconnu
- Supprimer un champ : marquer le numéro
reserved, ne jamais le réutiliser - Renommer un champ : libre, seul le numéro compte en binaire
- Changer un type : interdit (sauf compatibles comme
int32→int64)
Avro¶
Utilisé principalement dans l'écosystème Kafka et Hadoop. Le schéma est envoye avec les données ou stocke dans un Schéma Registry. Supporte l'évolution de schéma avec résolution automatique entre le schéma d'écriture et celui de lecture.
Choisir son format
Pour les APIs REST publiques : JSON. Pour la communication inter-services haute performance : Protobuf via gRPC. Pour les pipelines de données Kafka : Avro avec Schéma Registry. Pour les systèmes legacy : XML si impose par le protocole. Le format est un choix d'infrastructure, pas de préférence personnelle.
Découverte de services¶
Dans un système distribué, les instances de services apparaissent et disparaissent dynamiquement. La découverte de services permet à un client de trouver les instances disponibles sans coder en dur les adresses.
DNS classique¶
Le DNS reste la méthode la plus simple : un nom de domaine pointe vers les adresses IP des instances. Le load balancer (nginx, HAProxy, ALB) reçoit le trafic et le distribué.
Limite : le TTL DNS introduit un délai de propagation. Une instance tombee continue de recevoir du trafic jusqu'à l'expiration du cache DNS.
Service registry¶
Un registre centralise ou chaque instance s'enregistre au démarrage et se desenregistre à l'arrêt. Les clients interrogent le registre pour obtenir la liste des instances saines.
graph TD
S1[Service A<br/>instance 1] --"register"--> R[(Service Registry<br/>Consul / etcd / Eureka)]
S2[Service A<br/>instance 2] --"register"--> R
S3[Service A<br/>instance 3] --"register"--> R
R --"health check"--> S1
R --"health check"--> S2
R --"health check"--> S3
C[Client] --"lookup: Service A"--> R
R --"[instance 1, instance 3]"--> C | Outil | Protocole | Écosystème |
|---|---|---|
| Consul | HTTP/DNS/gRPC | HashiCorp, multi-DC |
| etcd | gRPC | Kubernetes, CNCF |
| Eureka | HTTP | Netflix, Spring Cloud |
| ZooKeeper | TCP custom | Apache, Kafka ancien |
Client-side vs server-side discovery¶
Client-side : le client interroge le registre et choisit l'instance. Le client à le contrôle du load balancing (round-robin, least-connections, weighted). Utilisé par Netflix Ribbon, gRPC avec service mesh.
Server-side : le client envoie au load balancer qui interroge le registre et route. Le client ne sait pas combien d'instances existent. Utilisé par Kubernetes (kube-proxy), AWS ALB.
graph LR
subgraph Client-side
C1[Client] --"1. lookup"--> R1[Registry]
R1 --"2. instances"--> C1
C1 --"3. appel direct"--> S1[Instance]
end
subgraph Server-side
C2[Client] --"1. appel"--> LB[Load Balancer]
LB --"2. lookup"--> R2[Registry]
LB --"3. route"--> S2[Instance]
end Health checks¶
Le registre doit savoir si une instance est reellement opérationnelle. Deux types :
- Liveness : le processus tourne-t-il ? (TCP connect, HTTP 200 sur
/healthz) - Readiness : le service est-il prêt à recevoir du trafic ? (connexion DB établie, cache charge)
Une instance live mais pas ready ne doit pas recevoir de trafic. Kubernetes distingue les deux avec des probes séparées. En cas de startup lent (chargement de modèle ML, prechauffage de cache), une startup probe evite que le liveness check tue le pod avant qu'il soit pret.
Timeouts de health check
Des health checks trop agressifs (intervalle de 1s, timeout de 500ms) peuvent déclarer des instances mortes pendant un pic de charge temporaire. Des health checks trop laxistes (intervalle de 30s) laissent du trafic aller vers des instances mortes. Commencer avec un intervalle de 5s et un timeout de 3s, puis ajuster selon les observations.
Chapitre suivant : Synchrone et asynchrone — patterns de communication, brokers de messages et backpressure.