Systèmes distribués · L3 · Section 2/12
Horloges logiques et ordre
Progression
#Horloges logiques et ordre des événements
Dans un système distribué, chaque machine suit son propre quartz tandis que pauses et latence brouillent la perception du temps. S’appuyer sur l’horloge physique pour ordonner deux opérations introduit des bugs subtils ; on privilégie donc la causalité observable plutôt qu’un temps absolu.
La relation « happens-before » () formalisée par Lamport capture l’influence potentielle entre deux événements :
- si deux événements appartiennent au même processus et que l’un arrive avant l’autre, alors ;
- si un message est envoyé à et reçu à , alors ;
- la relation est transitive : si et , alors .
Deux événements sont concurrents lorsqu’aucun n’influence l’autre. C’est cette distinction (causalité vs concurrence) qui permet aux systèmes distribués d’accepter des mises à jour parallèles tout en résolvant les conflits de manière contrôlée.
#Visualiser la causalité
Ordre local
Chaque processus impose sa propre séquence « se produit avant » (A1 → A2 → A3). Cet ordre est fiable même sans horloge globale.
L’animation met l’accent sur ce que l’on sait du graphe des dépendances. Aucune horloge ne nous indique l’ordre entre B1 et C1 : ils sont indépendants. En revanche, la réception de m₂ à C3 dépend d’une chaîne causale complète A2 → B2 → B3. En pratique, on construit un ordre total compatible avec cette relation partielle pour simplifier l’implémentation (logs ordonnés, séquenceurs). C’est précisément ce que fournit l’horloge logique de Lamport.
#Pas-à-pas : calculer une horloge de Lamport
Chaque processus maintient un compteur local initialisé à 0.
1# Lamport clock minimaliste2clock = {'A': 0, 'B': 0, 'C': 0}3 4def tick(pid: str) -> int:5 clock[pid] += 16 return clock[pid]7 8def send(pid: str, payload: str):9 ts = tick(pid)10 return payload, ts11 12def receive(pid: str, message):13 payload, ts = message14 clock[pid] = max(clock[pid], ts)#Horloges vectorielles
Les horloges de Lamport ne différencient pas les événements concurrents. Les horloges vectorielles gardent cette nuance en maintenant, pour chaque processus p, un vecteur dont l’entrée i reflète la meilleure estimation du compteur du processus i.
- Avant un événement local, on incrémente l’entrée propre .
- Lors d’un envoi, on joint le vecteur complet au message.
- À la réception, on effectue un maximum composant par composant, puis on incrémente son entrée propre.
On compare deux vecteurs composant par composant : si celui de est inférieur ou égal à celui de et strictement inférieur pour au moins une composante, alors précède ; sinon les événements sont concurrents.
1from typing import List2 3def merge_vectors(local: List[int], incoming: List[int]) -> List[int]:4 return [max(a, b) for a, b in zip(local, incoming)]#Quand les vecteurs s’imposent
- Détection de causalité pour des CRDTs ou des systèmes de synchronisation optimistes : savoir que deux mises à jour sont concurrentes permet de les fusionner sans générer de faux conflits.
- Debug distribué : tracer un incident nécessite de reconstruire la cascade d’événements qui a mené à une panne. Les vecteurs offrent cette visibilité.
- Algorithmes de capture globale cohérente (snapshots de Chandy-Lamport) : la notion de frontière cohérente repose sur la relation happens-before.
#Bonnes pratiques d’architecture
- Centraliser l’ordonnancement (séquenceur, horloge hybride) uniquement si la cohérence stricte l’exige ; sinon, privilégiez les protocoles tolérant la concurrence.
- Conservez les horodatages logiques dans les logs applicatifs pour corréler les incidents entre services.
- Testez les algorithmes avec des délais artificiels (chaos réseau) afin de valider les hypothèses sans faire confiance au temps physique.