Un handler qui perd des mises à jour d'order book et les livre dans le désordre sous les rafales, ce sont en général deux pannes distinctes derrière un seul symptôme : la gestion de la séquence côté ingestion, et une couche de distribution où une session lente change ce que reçoivent toutes les autres. Voici comment se construisent la séquence, la reprise, le fan-out et la back pressure, et où la conflation cesse d'être honnête.
Un handler qui perd des mises à jour L2 et les livre dans le désordre sous les rafales, ce sont en général deux pannes distinctes derrière un seul symptôme. L'une est du côté ingestion, là où la séquence du flux est suivie et où un gap doit être détecté plutôt qu'absorbé. L'autre est du côté distribution, là où un carnet normalisé est diffusé en fan-out vers de nombreuses sessions et où un seul lecteur lent change ce que reçoivent les autres.
Elles se corrigent différemment, et appliquer la mauvaise correction déplace le symptôme au lieu de le supprimer. Voici la forme du pipeline quand la séquence doit survivre à une rafale : ce qu'est vraiment un gap, comment la reprise joint un snapshot à un flux live, comment le fan-out décide l'ordre une seule fois, et où la conflation est honnête.
La réponse courte est structurelle. L'ordre se décide en un seul endroit, en amont de toutes les sessions : un seul writer par instrument assemble le flux numéroté en un carnet, et les sessions reçoivent des vues dérivées de cet assemblage - elles ne réordonnent jamais rien pour leur compte. Ce qu'amBrain peut étayer publiquement : une mini-bourse que nous avons construite tourne en production en colocation MOEX, nous avons construit le terminal de trading Spectre Trade, et la latence des données de marché que nous publions est mesurée - moins de 5 ms sur les chemins que nous construisons. Ce chiffre décrit nos chemins, ce n'est pas un benchmark de la conception décrite ci-dessous.
Un numéro de séquence promet l'ordre, pas la livraison
Les flux numérotent leurs mises à jour, et ce numéro est la seule autorité d'ordre dont vous disposez. L'heure d'arrivée n'en est pas une : les chemins multicast réordonnent, plusieurs canaux portent un même instrument, les files de réception sont réparties sur les cœurs, et une rafale étire tout cela. Un handler qui ordonne par arrivée n'est correct que tant que le réseau est calme - la condition dont personne ne s'inquiétait.
Six propriétés du flux doivent être connues avant d'écrire la logique de reprise. Chacune change ce que signifie un gap.
- L'unité que couvre la séquence - canal, instrument ou carnet. Un numéro par canal ne vous dit pas quel instrument a perdu une mise à jour, et un numéro par instrument ne vous dit pas qu'un canal s'est arrêté
- La règle d'incrément : strictement consécutif à l'intérieur de l'unité, ou croissant avec des trous autorisés. Les deux existent, et lire la seconde comme la première produit des reprises qui n'ont jamais été nécessaires
- Si les numéros repartent à zéro à la frontière d'une séance et ce qui la marque - un redémarrage lu comme un gap envoie tous les instruments en reprise au même moment
- Si les heartbeats portent le numéro de séquence courant. Sans eux, une connexion morte et un instrument calme se ressemblent
- Si la retransmission existe et sur quelle fenêtre. Si elle n'existe pas, la reprise par snapshot est le seul chemin de retour, et elle doit être assez peu coûteuse pour servir souvent
- Avec quel numéro de séquence un snapshot est aligné. Sans lui, un snapshot ne peut tout simplement pas être joint à un flux live
Là où une propriété est vraiment inconnue, mesurez-la plutôt que de coder une supposition. Chacune des six devient une branche du chemin de reprise, et une hypothèse fausse à cet endroit se découvre plus tard sous la forme d'un carnet qui, discrètement, ne concorde plus avec la place de marché.
Un gap et un désordre se ressemblent pendant quelques millisecondes
Les deux commencent pareil : la mise à jour suivante ne porte pas le numéro attendu. La différence est le temps : la classification ne se fait pas à l'arrivée, mais quand une attente bornée expire.
- Hors séquence : vous attendiez N, vous avez reçu N+2, et N+1 arrive pendant que l'attente est encore ouverte. Rien ne manque, et le seul coût est l'attente
- Doublon ou retransmission : un numéro inférieur ou égal au dernier appliqué. Jeté sans toucher au carnet, et compté, parce qu'un taux de doublons qui monte dit quelque chose sur le chemin
- Gap : l'attente a expiré et N+1 n'est jamais arrivé. Le carnet ne peut pas avancer au-delà du trou, et cet instrument part en reprise
- Périmé : le bon numéro, trop tard pour servir. Les octets sont arrivés, et en aval c'est une perte
Une seule règle empêche la corruption de devenir silencieuse : une mise à jour n'est appliquée que si son numéro de séquence est exactement celui attendu. Tout le reste part dans le buffer d'attente ou en reprise. Un carnet qui accepte un delta hors séquence continue de servir des prix et a l'air sain - le désaccord avec la place de marché est découvert plus tard, par un client, sur une exécution qui n'avait pas de sens.
L'attente est une structure bornée, pas une file qui grossit. Elle garde les mises à jour en avance sur le numéro attendu, indexées par numéro de séquence : les libérer est une recherche plutôt qu'un tri.
- La libération est une boucle : appliquer le numéro attendu, puis appliquer ce qui est déjà bufferisé tant que les numéros restent consécutifs
- L'échéance s'exprime en temps, pas seulement en nombre de mises à jour en attente - une rafale remplit une fenêtre exprimée en nombre bien plus tôt que la conception ne le prévoyait
- Tout ce qu'attend le buffer, chaque consommateur l'attend. Dimensionnez l'échéance à partir du désordre mesuré sur votre propre chemin, pas d'après un chiffre qui semblait prudent
- Le débordement du buffer est en soi une déclaration de gap : l'attente est bornée en mémoire autant qu'en temps
- L'attente est par instrument ou par canal, jamais globale. Un instrument calme ne doit pas retenir tout ce qui l'entoure
La reprise, c'est un snapshot joint à un flux que vous bufferisiez déjà
La jointure est la partie qui se passe mal. Un snapshot est un carnet arrêté à un certain numéro de séquence, et il est périmé dès l'instant où il est produit ; ce qui le rend utilisable, c'est le flux incrémental bufferisé pendant qu'on allait le chercher.
- Bufferisez le flux incrémental avant de demander le snapshot. Un snapshot sans flux live derrière lui est déjà en retard sur le marché quand il arrive
- Lisez le numéro de séquence avec lequel le snapshot est cohérent. Si le flux n'en publie aucun, le flux est en pratique snapshot-only, et la conception doit le dire à voix haute
- Jetez les mises à jour bufferisées dont le numéro est inférieur ou égal à celui du snapshot, puis appliquez le reste dans l'ordre. Si la première n'est pas la mise à jour immédiatement postérieure au snapshot, la jointure a échoué et la reprise recommence
- Si le buffer se remplit avant l'arrivée du snapshot, redémarrez la reprise plutôt que d'en appliquer une partie - une reprise partiellement appliquée est indiscernable d'un carnet sain
- Publiez l'instrument comme dégradé pendant sa reprise, sous la forme d'un état explicite sur le flux. Un carnet troué, servi comme courant, est pire que pas de carnet du tout
- Vérifiez après la jointure : le checksum que publie le flux, s'il en publie un, ou la concordance entre votre carnet assemblé et le snapshot suivant
La reprise est un événement normal, pas un incident, et son coût a sa place dans le plan de capacité : combien de temps prend la récupération d'un snapshot, quelle quantité de flux est bufferisée pendant ce temps, et combien d'instruments peuvent reprendre en même temps avant que le service de snapshots ne devienne le goulot d'étranglement.
Fan-out : normaliser une fois, encoder une fois, envoyer en nombre
Des centaines de sessions de terminal veulent le même carnet. L'erreur qui se multiplie sous une rafale, c'est de faire par session un travail qui n'est pas par nature par session : reconstruire un carnet pour chaque abonné, ou sérialiser la même mise à jour une fois par socket.
- Un seul writer par shard d'instruments possède le carnet. Les readers ne le modifient jamais, ce qui supprime à la fois le verrou et la question de savoir quelle version fait foi
- Le writer publie des mises à jour versionnées dans un ring buffer que les readers suivent à leur propre rythme : un reader qui décroche ne ralentit personne
- Chaque mise à jour est encodée une fois par format de transport et partagée entre les sessions par référence. Seuls le framing et le contrôle de flux sont par session
- Chaque session porte son propre numéro de séquence sortant : un client peut détecter ses propres pertes sans rien savoir du flux amont
- L'ordre est garanti par instrument, parce que c'est la garantie dont dépendent les clients. L'ordre entre instruments est soit promis explicitement et implémenté, soit pas promis du tout
- Au-delà d'un seul processus, le fan-out devient un étage de relais : chaque relais prend un abonnement en amont et sert une part des sessions, si bien que le travail du writer reste constant
Le coût du fan-out est décidé par le nombre de fois qu'une mise à jour est transformée, pas par le nombre de sockets qui la reçoivent. Encoder une fois et passer une référence passe à l'échelle avec les sessions ; reconstruire un carnet par session, non.
Un consommateur lent est une politique que vous choisissez, pas un accident qui arrive
Quelque part, il y a une session sur un mauvais réseau, ou un terminal dont la boucle de rendu s'est bloquée, et son buffer sortant se remplit. Il y a quatre comportements possibles, et deux d'entre eux ne se choisissent que par accident.
- Bloquer le writer jusqu'à ce que la session lente se vide : jamais. Cela transforme une mauvaise connexion en incident de latence pour tout le monde sur le shard
- Faire croître la file sans limite : un consommateur lent devient un épuisement mémoire, puis une panne sans rapport avec la session d'origine
- File bornée avec conflation : correct pour l'état du carnet, où le client veut l'image courante plutôt que chaque étape intermédiaire
- File bornée avec déconnexion au high watermark : correct pour les flux qui ne supportent pas la conflation, où jeter un élément jette du sens
- Quelle que soit la politique, la file est par session, et le retard est mesuré en continu - profondeur de file, et écart entre le numéro de séquence publié et le numéro écrit sur la socket
- Une déconnexion annonce sa raison. Une fermeture inexpliquée est retentée en boucle ; une fermeture expliquée est suivie d'un réabonnement
La back pressure est le point de rencontre des deux côtés. Si le chemin sortant peut refouler vers le writer du carnet, un terminal lent finit par retarder l'assemblage du flux, et la détection de gap se met à se déclencher pour des raisons qui n'ont rien à voir avec la place de marché. Un ring borné entre les deux coupe cette chaîne.
La conflation est honnête pour l'état et fausse pour les événements
Un carnet est un état : le client veut les niveaux courants, et une valeur déjà remplacée n'a plus de sens en soi. Une bande de transactions est un journal d'événements, où chaque élément est un fait survenu, qu'on ne peut pas résumer sans le perdre.
- Avec conflation : mises à jour de niveaux de prix, top of book, profondeur agrégée, et statistiques dérivées comme le dernier prix ou le volume de la séance
- Sans conflation : trades et prints, rapports d'ordre et d'exécution, enchères et changements de phase, et tout ce qu'un client agrège dans le temps - une bande construite sur un flux avec conflation est un chiffre faux tenu avec assurance
- Conflation par clé, pas par flux. Garder la dernière mise à jour de chaque niveau de prix préserve le carnet ; garder la dernière mise à jour globale jette tous les niveaux qui n'ont pas changé en dernier
- Une mise à jour issue de la conflation porte le numéro de séquence de l'état qu'elle représente : le client sait à quel point elle correspond
- L'intervalle de conflation fait partie de la latence que vous annoncez. Un flux avec conflation sur un intervalle n'est pas décrit par la latence mesurée sur le flux sans conflation
- Un client qui a besoin de chaque état intermédiaire - un backtest, un enregistrement de conformité - prend le flux sans conflation et paie en bande passante
La conflation est un changement de forme, pas un réglage de compression. Une fois qu'un flux est passé par la conflation, un client ne peut pas reconstruire ce qui s'est passé entre deux mises à jour, et on ne doit pas lui dire que le flux est complet. Publier les deux - un flux de carnet avec conflation et un flux d'événements sans conflation - est ce qui garde corrects les deux types de clients.
Une reconnexion est une resynchronisation, et elles arrivent toutes ensemble
Quand une session revient, le carnet qu'elle détient ne vaut rien tant que le serveur ne peut pas prouver la continuité. Par défaut : un snapshot neuf par abonnement, avec son numéro de séquence, appliqué à un client qui a d'abord jeté son état local.
- La reprise à partir d'un numéro de séquence n'est offerte que là où existe un buffer de replay borné. Quand le numéro demandé est trop vieux, le serveur le dit et bascule sur un snapshot plutôt que d'envoyer un flux troué
- L'état de session à travers une reconnexion est une décision explicite : soit le serveur garde les abonnements pendant une durée bornée sous un token de session, soit le client les redéclare à la connexion. Les deux marchent ; un mélange implicite, non
- Une livraison en double après une reprise est attendue, et le client écarte par numéro de séquence. At-least-once plus numérotation de séquence est plus facile à implémenter correctement qu'exactly-once
- Les reconnexions arrivent groupées, parce que ce qui a déconnecté une session en a en général déconnecté beaucoup. Un backoff avec jitter côté client et un contrôle d'admission côté serveur empêchent la reprise de devenir la deuxième panne
- Les snapshots pour cette foule viennent d'un cache par instrument rafraîchi à cadence fixe : le writer sérialise un snapshot selon un calendrier plutôt qu'une fois par session qui se reconnecte
- Le carnet côté client est reconstruit, jamais rapiécé. Un terminal qui garde ses anciens niveaux et applique de nouveaux deltas par-dessus reporte l'erreur d'avant la déconnexion dans un carnet qui a maintenant l'air neuf
La panne qui mérite d'être prévue à la conception n'est pas une reconnexion isolée. C'est un incident réseau qui ramène des centaines de sessions dans la même seconde, chacune demandant un snapshot de chaque instrument qu'elle suivait, pendant que le côté ingestion se remet du gap produit par ce même incident.
Ce qu'amBrain peut étayer publiquement : nous construisons des plateformes de trading à faible latence, des matching engines et des systèmes de real-time bidding en Rust depuis Erevan, en Arménie, et la latence des données de marché que nous publions - moins de 5 ms - est mesurée sur les chemins que nous construisons. Si votre handler perd la séquence sous les rafales, la conversation qui vaut la peine est celle qui sépare le côté ingestion du côté distribution avant de réécrire l'un ou l'autre.