Architecture systèmeArchitecte Système

Élaborez l'architecture d'une plateforme de traitement de flux d'état à l'échelle planétaire qui permet des sémantiques exactement-une fois pour des agrégations de fenêtres en fonction du temps d'événement à travers des flux de données non bornés, fournit un rééquilibrage automatique lors des changements de topologie, et maintient un rétablissement de point de contrôle en dessous de la seconde tout en supportant plusieurs langages de traitement et une réplication active-active inter-régions sans dépendances de stockage partagé ?

Réussissez les entretiens avec l'assistant IA Hintsage

Historique de la question

Les architectures de traitement de flux ont évolué du traitement à au moins une fois d'Apache Storm vers les garanties exactement-une fois modernes introduites par Apache Flink et Spark Structured Streaming. Alors que les entreprises migrent des architectures batch Lambda vers des flux continus Kappa, la complexité se déplace de simples transformations à la gestion d'état distribué pour les agrégations sur fenêtres et la sessionisation. L'émergence des exigences de souveraineté des données et des contraintes de latence régionales a nécessité des déploiements actifs-actifs sans dépendre d'un stockage partagé NFS ou SAN, créant de nouveaux défis pour la cohérence de l'état lors de basculements géographiques.

Le problème

Le traitement de flux avec état nécessite de maintenir des gigaoctets d'état d'opérateur (fenêtres indexées, magasins de session) localement sur les nœuds de traitement tout en ingérant des millions d'événements par seconde. Les sémantiques exactement-une fois exigent des engagements atomiques à travers trois composants : le suivi des décalages des sources, les mises à jour de l'état de l'arrière-plan et les écritures des puits. La réplication active-active inter-régions sans stockage partagé introduit des risques de schisme cérébral lorsque des partitions réseau se produisent, tandis que l'autoscaling nécessite une migration d'état en direct sans perdre des enregistrements en vol ou violer les garanties de temps de traitement. Le support de plusieurs langages (Java, Python, Go) impose traditionnellement des surcharges de sérialisation ou un verrouillage au niveau du runtime spécifique au langage.

La solution

L'architecture emploie un design découplé avec Apache Kafka ou Apache Pulsar comme journal unifié, des nœuds de traitement s'exécutant sur Kubernetes avec des conteneurs gRPC agnostiques au langage pour le support polyglotte. La gestion de l'état utilise RocksDB intégré avec des points de contrôle incrémentaux asynchrones vers un stockage d'objets compatible S3, coordonné par un service de coordination distribué léger (etcd ou ZooKeeper). Les sémantiques exactement-une fois sont réalisées grâce à l'algorithme d'instantané Chandy-Lamport pour l'état et aux protocoles de commit en deux phases (2PC) pour les puits transactionnels (transactions Kafka ou écritures idempotentes JDBC). La réplication inter-régions utilise l'expédition d'état basé sur le journal via Kafka MirrorMaker 2 ou Pulsar Geo-Replication, avec résolution des conflits à l'aide de compteurs commutatifs basés sur CRDT pour les agrégations et la propriété primaire versionnée pour l'état indexé.

Réponse à la question

La plateforme se compose de quatre couches logiques : ingestion, traitement, gestion de l'état et coordination.

Couche d'Ingestion

Les clusters Apache Kafka fonctionnent dans plusieurs régions avec MirrorMaker 2 permettant une réplication bidirectionnelle des sujets. L'idempotence des producteurs et les IDs transactionnels garantissent une ingestion exactement-une fois même en cas de défaillance du producteur entre les régions.

Couche de Traitement

Apache Flink ou des processeurs de flux similaires s'exécutent en tant que StatefulSets sur Kubernetes. Chaque TaskManager expose un conteneur gRPC qui accepte des tâches sérialisées en Protobuf, permettant l'exécution de fonctions définies par l'utilisateur (UDFs) en Python et Go à l'intérieur des conteneurs gRPC, tandis que le runtime Java gère l'état et les points de contrôle. Le JobManager répartit la topologie entre les TaskManagers en utilisant un hachage cohérent sur les clés des enregistrements.

Gestion de l'État

Les arrière-plans d'état d'opérateur utilisent RocksDB avec enableIncrementalCheckpointing. Les points de contrôle écrivent les changements d'état delta dans des buckets S3 régionaux de manière asynchrone toutes les 15 secondes. Pour la cohérence inter-régions, les déploiements actifs-actifs utilisent des CRDTs d'ensemble d'éléments LWW pour des agrégations monotoniques (comptes, sommes) et l'affinité de clé primaire pour les opérations non commutatives. Lors d'un basculement régional, les TaskManagers en attente hydratent l'état depuis S3 en utilisant des Savepoints.

Garanties Exactly-Once

Le système implémente une garantie exactement-une fois de bout en bout grâce à :

  • Commit en Deux Phases : Les puits participent à la TwoPhaseCommitSinkFunction de Flink, pré-engageant des données dans Kafka ou PostgreSQL pendant les points de contrôle et s'engageant sur notification de point de contrôle réussi.
  • Producteurs Idempotents : Les producteurs Kafka en amont utilisent une livraison idempotente avec des numéros de séquence pour dédupliquer les tentatives.
  • Isolation de Transaction : Les points de contrôle agissent comme des frontières transactionnelles ; les données non engagées restent invisibles pour les consommateurs en aval.

Situation de la vie réelle

Une plateforme mondiale de covoiturage nécessitait des calculs de tarification dynamique en temps réel agrégant la disponibilité des conducteurs et la demande de course par geohash à travers AWS us-east-1 et AWS eu-west-1. L'architecture précédente utilisait un cluster Redis à écriture unique avec un retard de réplication, provoquant des fenêtres de basculement de 2 secondes où les calculs de tarification produisaient des multiplicateurs de tarification dynamique obsolètes ou dupliqués lors des pannes régionales, entraînant des calculs de tarifs incorrects et des plaintes de clients.

Solution 1 : Actif-Passif avec Stockage Partagé

L'équipe a envisagé de monter EFS (NFS partagé) à travers les régions pour le stockage d'état. Avantages : Basculement simplifié avec des sémantiques d'écriture unique, forte cohérence. Inconvénients : La latence de EFS dépassait 100 ms pour l'accès inter-régional, violant le SLA de traitement de 50 ms ; de plus, les problèmes de cohérence d'écritures NFS ont causé la corruption des points de contrôle pendant les partitions réseau.

Solution 2 : Architecture Lambda

Implémenter une couche de vitesse avec Kafka Streams et une couche batch avec Spark pour les corrections. Avantages : Tolérance aux pannes à travers des journaux immuables, récupération simple. Inconvénients : Complexité opérationnelle de la maintenance de deux chemins de code ; les corrections batch arrivaient trop tard pour la tarification dynamique nécessitant une précision en dessous de la seconde pour équilibrer l'offre et la demande.

Solution 3 : Traitement de Flux Actif-Actif avec CRDTs

Déployer Apache Flink dans les deux régions avec état RocksDB, points de contrôle S3 incrémentaux, et compteurs basés sur CRDT pour le nombre de courses. Avantages : Latence de traitement local inférieure à 20 ms, résolution automatique des conflits pour les mises à jour régionales concurrentes, basculement sans temps d'arrêt. Inconvénients : Nécessité de refactoriser les agrégations pour qu'elles soient commutatives (en utilisant des G-Counters et PN-Counters), coûts de stockage augmentés pour des points de contrôle régionaux doubles.

L'équipe a choisi Solution 3 car le besoin commercial de 99,99 % de disponibilité avec un basculement en dessous de la seconde ne pouvait pas tolérer la fenêtre de 2 secondes de la Solution 1 ou la latence du stockage partagé. Ils ont mis en œuvre des G-Counters pour les comptes de conducteurs et des LWW-Registers pour les derniers multiplicateurs de tarification.

Résultat

Le système a atteint des calculs de tarification dynamique exactement-une-fois avec une latence p99 de 15 ms dans les deux régions. Lors d'une panne simulée de us-east-1, eu-west-1 a continué à traiter sans interruption en utilisant un état répliqué localement sans calcule de tarifs en double. Le temps de récupération des points de contrôle était en moyenne de 800 ms, bien dans les exigences de less de une seconde.

Ce que les candidats manquent souvent

Comment l'optimisation de l'intervalle de point de contrôle interagit-elle avec les mécanismes de pression en amont dans les processeurs de flux avec état ?

De nombreux candidats optimisent les intervalles de point de contrôle pour le temps de récupération sans tenir compte de la propagation de la pression en amont. Lorsque les barrières de point de contrôle s'alignent lentement en raison de la pression en amont, l'algorithme Chandy-Lamport suspend l'exécution du pipeline, ce qui peut entraîner des délais d'attente en cascade. L'approche correcte implique d'aligner les délais d'expiration des points de contrôle avec les seuils de pression en amont, en utilisant des points de contrôle non alignés (où les barrières dépassent les tampons) pendant les charges élevées, et en séparant les phases de point de contrôle synchrones et asynchrones. Les points de contrôle incrémentaux de RocksDB doivent être régulés à l'aide de configurations RateLimiter pour éviter que la compactage SST n'inonde les E/S du disque et n'aggrave la pression en amont.

Quelle est la différence fondamentale entre la livraison au moins une fois combinée avec des puits idempotents et les vraies sémantiques de traitement exactement une fois ?

Les puits idempotents garantissent que le traitement en double produit le même état de sortie (par exemple, des opérations UPSERT dans PostgreSQL ou HBase), mais ils exposent des états intermédiaires pendant les réessais. Si un puits écrit les enregistrements A, B, puis plante et réessaie d'écrire A, B, C, les observateurs en aval voient momentanément A, B, A, B, C avant déduplication. La vraie exactement-une-fois (effectivement-une-fois) utilise une isolation transactionnelle où les données pré-engagées restent invisibles jusqu'à l'achèvement du point de contrôle. Cela nécessite que le puits supporte des transactions (par exemple, des transactions Kafka avec isolation.level=read_committed) ou des protocoles de commit en deux phases. Les candidats manquent souvent que l'idempotence résout le problème de la correction mais pas le problème de cohérence/visibilité pendant la récupération.

Comment la fenêtre temporelle d'événements doit-elle gérer les données arrivant tard lors des scénarios de basculement inter-régions ?

Lorsque le basculement se produit de la Région A à la Région B, les enregistrements en vol dans les tampons réseau de la Région A peuvent être perdus ou retardés au-delà de l'horizon de la marque temporelle. Les candidats suggèrent souvent d'étendre les marques temporelles indéfiniment, ce qui brise les garanties de complétude des fenêtres. L'architecture correcte utilise des Sorties Secondaires (dans la terminologie Flink) pour la capture des données tardives combinées avec des spécifications de Lateness Autorisée. Lors du basculement, le système doit hydrater les fenêtres à partir des Savepoints S3 avec des horodatages, puis fusionner les enregistrements arrivant tard de la file d'attente des lettres mortes de la région échouée dans les fenêtres suivantes ou déclencher des gestionnaires spécifiques de données tardives. De plus, la génération de marque temporelle doit être idempotente à travers les régions; l'utilisation de l'heure murale pour les marques de temps entraîne une divergence pendant le basculement, donc les marques de temps doivent être dérivées de l'extraction de temps d'événements monotoniques à travers les deux régions actives.