Partager cet article

L'essentiel en bref

Quand une partition Kafka devient silencieuse, est-elle terminée, ou simplement lente? Vu de l'extérieur, les deux cas sont identiques — et la réponse détermine si vous pouvez agir en toute sécurité sur ce que vous avez. Netflix, Uber et Walmart ont chacun publié leur façon de gérer la question, et chacun y renonce à quelque chose : un second pipeline, une voie de parallélisme, l'exactitude lors du rejeu. De l'ingénierie solide, à chaque fois. Mais aucun ne pose à Kafka la seule question dont Kafka connaît déjà la réponse.

J'ai rencontré ce problème sur un flux d'inventaire pour une entreprise du Fortune 50 : treize topics Kafka, trois partitions chacun, des centaines de millions d'événements par jour, chaque événement d'un produit donné devant être traité dans un ordre strict de temps d'événement — avec sept jours d'historique à lire depuis zéro avant d'enchaîner directement sur le trafic de pointe en direct. La première conception plaçait une fenêtre de trente minutes devant tout le reste, nécessitait malgré tout un second job pour rattraper ce que la fenêtre manquait, et laissait encore passer 5,5 % des enregistrements dans le mauvais ordre.

La version mise en production n'attend pas une horloge. Elle lit les offsets de Kafka.

Voici comment — et pourquoi l'idée est plus ancienne que Kafka.

Le compromis que tout le monde fait / §01

Ce n'est pas le problème d'une seule équipe. Quatre organisations d'ingénierie solides, quatre compromis raisonnables :

  • Walmart épingle chaque couple article–magasin à une seule partition, de sorte qu'un seul consommateur le met à jour. Ordonner en fixant les clés — au prix du parallélisme.
  • Uber exploite un pipeline de rattrapage distinct pour recalculer après les événements tardifs ou désordonnés, car le chemin de streaming « peut causer des inexactitudes en abandonnant tout événement arrivant après le watermark ». Au prix d'un second pipeline.
  • Netflix, en rejouant des pipelines Flink, a constaté que l'historique rejoué produisait un ordre différent et un état incontrôlé. Au prix de l'exactitude et de l'état.
  • Confluent lui-même recommande, pour une partition Kafka inactive : l'attendre et se bloquer, ou l'ignorer et accepter « de nombreux messages tardifs une fois la partition redevenue active ». Au prix de blocages ou de données tardives.

Chacun de ces choix est raisonnable. Leur point commun : tous déduisent la complétude du temps — une fenêtre, un délai d'expiration, une attente — alors que les offsets des partitions indiquent exactement où les données s'arrêtent. C'est cette lacune que notre approche comble.

Pourquoi le flux est brouillé / §02

Un topic Kafka est divisé en partitions, et chaque partition est sa propre voie. À l'intérieur d'une partition, l'ordre est garanti : l'enregistrement un, puis l'enregistrement onze, toujours. Entre les partitions, rien ne coordonne rien — les consommateurs qui les lisent ne se trouvent jamais au même point au même moment. Kafka fait exactement ce qu'il promet, les consommateurs font exactement ce qu'ils promettent, et la sortie combinée reste brouillée pour tout ce qui dépend de la séquence.

On ne peut pas agir sur « vendu » avant « en stock » pour la même unité. Avant de libérer quoi que ce soit, un pipeline doit donc répondre à une question : ai-je tout reçu jusqu'à cet instant ? Presque toutes les réponses en usage aujourd'hui sont des minuteries — la fenêtre, le délai d'inactivité, le job de réconciliation. Chacune est une conjecture sur le silence.

Un remède ancien : signaler, pas attendre / §03

Il existe un précédent, et ce n'est pas une meilleure horloge. Les premiers chemins de fer fonctionnaient à l'intervalle de temps : un train suivant partait une fois un intervalle fixe écoulé, la voie devant « forcément » être dégagée.

En juin 1889, un train est tombé en panne dans une forte rampe près d'Armagh, en Irlande. Les voitures arrière, désaccouplées, ont redescendu la pente et percuté le train qui suivait. Environ quatre-vingts personnes sont mortes.

Le train suivant avait quitté Armagh une fois les vingt minutes réglementaires écoulées. Vingt minutes avaient passé, donc la voie devait être libre. Personne à Armagh n'avait aucun moyen de savoir qu'elle ne l'était pas.

La Grande-Bretagne n'a pas répondu par une meilleure montre. Le Parlement a donné au Board of Trade le pouvoir d'imposer le block system absolu sur les lignes de voyageurs : aucun train n'entre dans une section de voie tant que le poste d'aiguillage suivant n'a pas confirmé que la section est libre. Le dégagement a cessé d'être une durée que quelqu'un attendait pour devenir un fait que quelqu'un signalait. Un train en panne et un train parti ne se ressemblaient plus, parce que plus personne n'interprétait le silence.

C'est toute l'idée. Lire les offsets de Kafka, c'est le poste d'aiguillage.

Ne déduisez pas la complétude du silence. Interrogez le système qui sait.

La montre — exploitation à intervalle de temps
20 minutes écoulées → DÉPART

Le dégagement est une durée qu'on attend. Un train en panne et un train parti se ressemblent trait pour trait.

Le signal — block absolu
Le poste suivant confirme « section libre » → DÉPART

Le dégagement est un fait que quelqu'un signale. Plus personne n'interprète le silence.

La même idée, en streaming
chronomètre = la fenêtre / le délai d'inactivité
section de voie = une partition Kafka
« section libre » = la partition a atteint son offset de fin
signal dégagé = le watermark avance

La construction — trois pièces / §04

Le principe tient en une phrase : le temps n'avance que lorsque la preuve démontre qu'il le peut. Le concrétiser a demandé trois pièces, et la difficulté n'était pas là où on l'attend.

Un : la file de minuteries est le tri. Chaque enregistrement est mis en attente dans l'état par clé sous son propre temps d'événement, avec une minuterie de temps d'événement pour cette milliseconde précise. Rien n'est encore émis. Quand le watermark atteint cet horodatage, la minuterie se déclenche et tout ce qui y est en attente part ensemble. Flink déclenche les minuteries d'une clé par ordre croissant d'horodatage, donc nous ne trions jamais rien. C'est une méthode et un morceau d'état — la difficulté n'a jamais été là.

01 · à l'arrivée

Le stocker, puis enregistrer un déclencheur pour son propre temps d'événement.

state.put(t, list)
registerEventTimeTimer(t)
rien n'est encore émis
02 · au déclenchement

L'horloge l'atteint, lire cette clé, libérer tout ce qui s'y trouve.

onTimer(t)
collect(state.get(t))
B et C partent ensemble
MapState⟨Long, List⟩ · clé : SKU
temps d'événementenregistrementsminuterie
10:00:18—déclenchée, supprimée
10:00:26B, Céchéance atteinte
10:00:33Den attente
10:00:41E, F, Gen attente

◆ La file de minuteries de Flink est ordonnée par horodatage : les minuteries se déclenchent par ordre croissant. C'est ça, le tri.

Deux : le flux sans temps d'événement devient l'horloge. L'un des treize topics n'avait aucun temps d'événement — il ne publiait que les produits valides servant à enrichir tout le reste. Il n'avait donc rien à trier, et pouvait faire ce que les autres ne pouvaient pas : émettre des watermarks à partir du temps de traitement, toujours l'horodatage le plus récent possible. Le piège : l'horloge d'un pipeline est le minimum de ses entrées, donc un flux qui tourne à « maintenant » ne change rien tant que les flux d'historique sont encore pris en compte. J'ai construit cette pièce en premier et elle n'a eu aucun effet — elle n'était pas fausse, elle était dans le mauvais ordre. Le flux de contrôle ne compte qu'une fois que les autres sortent du minimum, et un flux sort du minimum quand il est marqué inactif.

Trois : c'est Kafka qui décide quand un flux est terminé — pas un délai d'expiration. Le mécanisme d'inactivité intégré de Flink est une minuterie : pas de données pendant un certain temps, on marque le flux inactif. Mais un délai d'expiration ne distingue pas « en démarrage » de « terminé », et le démarrage est le pire des cas — les connexions s'établissent à des vitesses différentes, un topic reste silencieux, le délai expire, les autres font avancer l'horloge au-delà de temps d'événement qu'il n'a pas encore lus, et son premier enregistrement arrive déjà en retard. Ce n'est pas un retard, c'est une violation. Le délai d'expiration devait donc disparaître. Au démarrage, avant de lire un seul enregistrement, le job demande à Kafka le dernier offset de chaque partition et le conserve. Un topic n'est inactif que lorsque chaque partition a atteint ce nombre — pas la plupart, chacune. Aucune durée à régler, car le verrou est un fait fourni par Kafka.

// once, at job startup
Map<TopicPartition, Long> endOffsets = consumer.endOffsets(partitions);

// as records arrive
if (offset + 1 >= startupEndOffsets.get(partition)) markCaughtUp(partition);

Ce que cela a rapporté / §05

La fenêtre retenait chaque enregistrement jusqu'à sa fermeture — trente minutes rien que pour relire l'historique, et cela à chaque redémarrage. Désormais, un enregistrement est libéré dès que le watermark le dépasse : en moyenne moins de 100 millisecondes, y compris les enregistrements retenus pour éviter qu'ils sortent dans le désordre. Un enregistrement qui arrive à la minute un part à la minute un. L'ordre a suivi la même trajectoire : deux couches d'inactivité par délai d'expiration laissaient passer 5,5 % d'enregistrements désordonnés ; une couche, 1,5 % ; l'inactivité décidée par les offsets de démarrage de Kafka, zéro. La fenêtre a disparu, et le job de rattrapage avec elle — un seul job lit sept jours d'historique puis passe en direct.

AvantAprès
Attente avant qu'un enregistrement soit libéré dans l'ordreJusqu'à 30 minutes (la fenêtre)< 100 ms en moyenne*
Enregistrements hors de l'ordre du temps d'événement5,5 % (inactivité par délai d'expiration)0
Jobs à exploiterFlux + rattrapageUn seul

*Latence mesurée de bout en bout côté client, sur des centaines de millions d'événements par jour. Ordre mesuré sur l'implémentation de référence que j'ai reconstruite pour cet article.

Deux couches d'inactivité par délai d'expiration
5,5 %
Une couche d'inactivité par délai d'expiration
1,5 %
Inactivité déterminée par les offsets de démarrage de Kafka
0

Enregistrements libérés hors de l'ordre du temps d'événement. Implémentation de référence reconstruite pour cet article.

Ce que cela ne promet pas / §06

Déterministe ne veut pas dire clairvoyant. Un événement n'existe pour le pipeline qu'une fois arrivé dans Kafka. Si un producteur retient un enregistrement et qu'il arrive après que l'horloge a déjà dépassé son temps d'événement, il sort immédiatement, hors de son tour, et le compteur de violations l'enregistre. Aucun processeur de flux ne peut ordonner un événement qu'il ne voit pas encore. À l'intérieur de cette frontière, la garantie est exacte, et le décalage du watermark détermine la largeur de la frontière.

horloge de temps d'événement · watermark = maintenant − décalageclockEnregistrement Aarrive avant que l'horloge atteigne son heurelibéré dans l'ordreEnregistrement Barrive après que l'horloge a déjà dépassé son heurehors de son tour · violation +1

Aucun processeur ne peut ordonner un événement qu'il ne voit pas encore. À l'intérieur de la frontière, la garantie est exacte — le décalage du watermark en détermine la largeur.

La même question, partout / §07

Une fois qu'on l'a vue, on la retrouve dans tout système qui établit la vérité à partir de flux qui n'arrivent pas ensemble.

Une station de mesure fluviale devient silencieuse pendant une crue. La rivière est-elle stable, ou l'eau a-t-elle atteint le capteur ? En 2018, l'USGS a signalé des stations hydrométriques mises hors service par l'ouragan Florence — précisément celles qui comptaient le plus. « Pas de nouvelles » est la mauvaise interprétation de ce silence.

Un dispositif de caméras. Filmez un athlète avec plusieurs caméras : chacune est son propre flux, comme une partition ; impossible de fusionner une pose 3D pour un instant donné tant que chaque caméra ne l'a pas rapporté. Une caméra silencieuse a peut-être perdu une image, ou observe quelqu'un d'immobile. Chez i3d.ai, c'est la même question, des dizaines de fois par seconde.

Un agent. Un agent d'IA appelle un outil et n'entend rien en retour. Les agents excellent à interpréter le silence comme un succès — aucune erreur n'est revenue, donc étape suivante, à la vitesse de la machine. Un agent qui agit sur le compte d'un client tel qu'il était il y a une heure, c'est un train expédié sur minuterie. Le remède est celui que les chemins de fer ont trouvé : aucune étape ne procède sur le silence ; chaque étape procède sur une confirmation qu'elle peut désigner.

Une ville sous la tempête
Une station de mesure fluviale cesse de transmettre
silencieux : calme, ou disparu ?

La rivière se maintient-elle — ou l'eau vient-elle d'atteindre le capteur ?

Un corps en mouvement
Une caméra n'envoie rien
silencieux : calme, ou disparu ?

Une image perdue — ou un athlète parfaitement immobile ?

Un agent au travail
Un appel d'outil ne renvoie rien
silencieux : calme, ou disparu ?

A-t-il réussi, échoué — ou est-il encore en cours ?

Aucune étape ne procède sur le silence. Chaque étape procède sur une confirmation qu'elle peut désigner.

Le gain de fiabilité le moins coûteux en streaming n'est ni un moteur plus rapide ni une fenêtre plus large. C'est de cesser de déduire « terminé » du silence et d'interroger le système qui sait déjà. La fenêtre attend le pire des cas pour chaque enregistrement ; la vérification d'offset attend exactement aussi longtemps que la preuve l'exige, et pas une milliseconde de plus.

L'implémentation de référence épurée — générateur, gestionnaire, suivi du rattrapage — est ma prochaine publication.

Votre pipeline attend-il une horloge, ou lit-il le manifeste ?

Jeffrey Patrick Carter · NeoSavant

Fondement historique

Les compromis de Walmart, Uber, Netflix et Confluent sont cités des sources d'ingénierie ci-dessous. Le précédent du block system absolu (Regulation of Railways Act 1889) et le système à intervalle de temps qu'il a remplacé sont tirés des archives publiques. Les mécanismes de Flink et de Kafka s'appuient sur les documents de conception et le code source des projets eux-mêmes. Les chiffres d'ordonnancement ont été mesurés sur une reconstruction du job de production ; le chiffre de latence est mesuré de bout en bout côté client sur le système de production d'origine.

Sources

Uber Engineering — Designing a Production-Ready Kappa Architecture for Timely Data Stream Processing

Netflix — Backfill Streaming Data Pipelines in Kappa Architecture (Data + AI Summit 2022)

Confluent — Why Is My Apache Flink Job Not Producing Results?

Apache Flink — FLIP-180: Adjust StreamStatus and Idleness definition · FLINK-35886: Idleness incorrectly detected · Generating Watermarks (DataStream) · InternalTimerServiceImpl (ordre de déclenchement des minuteries)

Apache Kafka — KIP-32: Add timestamps to Kafka message (CreateTime / LogAppendTime)

Wikipedia — Regulation of Railways Act 1889 (block system absolu) · Catastrophe ferroviaire d'Armagh

USGS — Working to keep vital flood information flowing (ouragan Florence, 2018)

Library of Congress — Jack Delano — Indiana Harbor Belt RR switchman with a fusee, 1943 (item 2017878076)

Utile ? Partagez-le !

Jeffrey Patrick Carter

Principal Streaming Engineer, NeoSavant

Jeffrey Patrick Carter is a Principal Streaming Engineer at NeoSavant, with 10+ production Flink and Kafka implementations — including Allstate and i3d.ai.

LinkedIn →