Reponse A: OpenAI GPT-5.5
Un service de notification évolutif doit être construit comme une plateforme asynchrone et pilotée par les événements qui sépare l'ingestion des événements, l'évaluation des préférences, la persistance des notifications, la livraison spécifique au canal et le suivi de la livraison. Le principe de conception le plus important est que les systèmes de produits sources, tels que les suivis, les likes, les commentaires et les messages directs, ne doivent pas appeler de manière synchrone les fournisseurs de notifications push mobiles, les fournisseurs d'e-mails ou les appareils des utilisateurs. Ils doivent publier des...
Afficher la reponse complete ▼
Un service de notification évolutif doit être construit comme une plateforme asynchrone et pilotée par les événements qui sépare l'ingestion des événements, l'évaluation des préférences, la persistance des notifications, la livraison spécifique au canal et le suivi de la livraison. Le principe de conception le plus important est que les systèmes de produits sources, tels que les suivis, les likes, les commentaires et les messages directs, ne doivent pas appeler de manière synchrone les fournisseurs de notifications push mobiles, les fournisseurs d'e-mails ou les appareils des utilisateurs. Ils doivent publier des événements durables dans une couche de messagerie, et la plateforme de notification doit traiter ces événements indépendamment avec de fortes garanties de nouvelle tentative et d'idempotence. À un niveau élevé, l'architecture contient ces composants : producteurs d'événements, une API d'ingestion, un journal d'événements durable, des processeurs de notifications, un service de préférences utilisateur, un service de modèles et de personnalisation, un magasin de notifications, des files d'attente de diffusion par canal, des workers de livraison spécifiques au canal, des intégrations de fournisseurs tiers, une passerelle en temps réel pour la livraison dans l'application, et une infrastructure d'observabilité/de nouvelle tentative. Les services de produits génèrent des événements de notification lorsque des actions visibles par l'utilisateur se produisent. Par exemple, le service de graphe social émet un événement de nouveau follower, le service de publication émet un événement de like ou de commentaire, et le service de messagerie émet un événement de message direct. Chaque événement contient un ID d'événement, un type d'événement, un ID d'utilisateur acteur, un ID d'utilisateur destinataire ou un ensemble de destinataires, un ID d'objet, un horodatage de création et les métadonnées nécessaires au rendu. Les producteurs envoient ces événements à une API d'ingestion de notifications ou directement à un bus de messages durable. L'API d'ingestion valide le schéma, authentifie le producteur, attribue ou vérifie une clé d'idempotence, et écrit l'événement dans le journal durable avant d'accuser réception au producteur. Cela évite la perte de notifications si les processeurs en aval échouent. Pour le backbone de messagerie durable, j'utiliserais Apache Kafka, Amazon MSK, Google Pub/Sub ou Pulsar. Kafka/Pulsar conviennent bien car ils offrent un débit élevé, un ordonnancement partitionné, une rétention, une relecture, des groupes de consommateurs et un stockage durable. À 50 000 requêtes de notification par seconde, le flux d'événements doit être partitionné par ID d'utilisateur destinataire pour l'ordonnancement au niveau de l'utilisateur si nécessaire, ou par ID d'événement lorsque l'ordonnancement strict par utilisateur est moins important. Le partitionnement par destinataire permet d'éviter les notifications dans l'application dans le désordre pour un seul utilisateur, mais il peut créer des partitions chaudes pour les comptes de célébrités ou les événements de groupe. Pour les cas de diffusion à grande échelle, où un événement produit des notifications à des millions de followers, un service de diffusion distinct doit diviser les destinataires en lots et publier des tâches de notification dérivées par destinataire sur de nombreuses partitions. Les processeurs de notifications consomment les événements bruts du journal d'événements durable. Leurs responsabilités sont de déterminer les destinataires, de récupérer les préférences utilisateur, d'appliquer les limites de débit et les heures de silence, de dédupliquer les événements, de générer des enregistrements de notification spécifiques au canal et de publier des tâches de livraison. Pour les événements directs comme un commentaire sur la publication d'un utilisateur, l'ensemble des destinataires est petit. Pour les événements de diffusion tels qu'une célébrité qui publie, le processeur doit éviter de tout diffuser de manière synchrone. Il doit créer une tâche de diffusion et traiter les destinataires par fragments, en utilisant des lectures par lots du magasin de graphes sociaux. Cela évite qu'un événement très volumineux ne bloque le chemin à faible latence pour les notifications normales. Le service de préférences utilisateur stocke la configuration, comme si un utilisateur souhaite des notifications push, dans l'application ou par e-mail pour les likes, les commentaires, les followers et les messages directs. Les préférences doivent être stockées dans une base de données hautement disponible telle que DynamoDB, Cassandra, ScyllaDB ou une base de données relationnelle sharding. Le modèle d'accès est principalement une recherche clé-valeur par ID utilisateur et type de notification, donc un magasin clé-valeur distribué ou à colonnes larges est approprié. Pour respecter la cible de latence de 2 secondes, les préférences doivent également être mises en cache dans Redis, Memcached ou un cache local en mémoire avec des TTL courts. Les mises à jour de préférences sont écrites dans la source de vérité de la base de données et propagées aux caches par des événements d'invalidation. Le compromis est que la fraîcheur du cache peut faire qu'une préférence récemment modifiée prenne quelques secondes à s'appliquer ; si une cohérence stricte des préférences est requise, les processeurs peuvent lire directement dans la base de données en cas de cache manqué ou pour les utilisateurs récemment mis à jour. Le service de modèles et de personnalisation rend le contenu des notifications. Il mappe les types d'événements à des modèles tels que « Alex a aimé votre publication » ou « Maya a commenté : ... ». Il gère la localisation, les liens profonds, les URL d'images et les contraintes de charge utile spécifiques au canal. Les définitions de modèles peuvent être stockées dans une base de données de configuration et mises en cache de manière agressive car elles changent peu fréquemment. Le rendu doit avoir lieu avant la publication des tâches de livraison afin que chaque tâche soit autonome et puisse être retentée en toute sécurité. Le magasin de notifications est la source de vérité pour les notifications dans l'application visibles par l'utilisateur et l'état de livraison. Un bon choix est Cassandra, DynamoDB, ScyllaDB ou un autre magasin horizontalement évolutif partitionné par ID d'utilisateur destinataire et trié par horodatage de notification. Le modèle d'accès principal est « récupérer les dernières notifications pour l'utilisateur X », donc la table peut utiliser recipient_user_id comme clé de partition et created_at ou notification_id comme clé de tri. Le service écrit un enregistrement de notification dans l'application avant ou de manière atomique avec la publication de la tâche de livraison dans l'application. Les enregistrements incluent l'ID de notification, le destinataire, le type, le contenu, le statut, l'état lu/non lu, les horodatages et la clé de déduplication. Ce magasin garantit que même si la livraison WebSocket échoue, l'utilisateur peut toujours voir la notification en ouvrant l'application. Après l'application des préférences et des modèles, le processeur publie des tâches dans des files d'attente de canaux distinctes : file d'attente push, file d'attente dans l'application et file d'attente e-mail. La séparation des files d'attente est importante car chaque canal a des caractéristiques de latence et de fiabilité différentes. Les files d'attente push et dans l'application sont sensibles à la latence et doivent être provisionnées pour un débit élevé avec un minimum de backlog. L'e-mail est moins sensible à la latence et peut tolérer des délais plus longs, le throttling des fournisseurs et le traitement par lots. Des files d'attente distinctes empêchent également un fournisseur d'e-mails lent d'affecter la livraison push. Les workers de livraison push consomment à partir de la file d'attente push et envoient des notifications à Apple Push Notification service, Firebase Cloud Messaging ou d'autres fournisseurs de notifications push mobiles. Les jetons d'appareil sont stockés dans un registre d'appareils indexé par ID utilisateur, avec le jeton, la plateforme, la version de l'application, la locale et l'horodatage de la dernière connexion. Le registre peut utiliser un magasin clé-valeur distribué et mettre en cache les jetons actifs. Les workers push doivent gérer les réponses des fournisseurs, supprimer les jetons invalides, retenter les échecs transitoires avec un backoff exponentiel et enregistrer les tentatives de livraison. Les accusés de réception des fournisseurs push ne garantissent pas que l'utilisateur a vu la notification, seulement que le fournisseur l'a acceptée, donc le système doit distinguer l'acceptation du fournisseur de la réception réelle par l'utilisateur. La livraison dans l'application a deux chemins. Premièrement, la notification est persistée dans le magasin de notifications. Deuxièmement, un worker de livraison dans l'application l'envoie aux appareils connectés de l'utilisateur via une passerelle en temps réel. La passerelle peut être implémentée à l'aide de WebSockets, de flux HTTP/2 ou d'une infrastructure de connexion persistante de type push mobile. Les nœuds de passerelle maintiennent l'état de connexion de l'utilisateur en mémoire et publient des informations de présence à un service de présence distribué. Une couche de routage ou une carte de présence basée sur NATS/Redis indique au worker dans l'application quel nœud de passerelle possède actuellement la connexion d'un utilisateur. Si l'utilisateur est hors ligne ou si l'envoi de la passerelle échoue, aucune notification n'est perdue car la notification persistée sera récupérée via l'API de la boîte de réception des notifications de l'application lors de la prochaine session. Pour une faible latence, les nœuds de passerelle doivent être déployés régionalement à proximité des utilisateurs et la file d'attente dans l'application doit être traitée par des workers dans la même région si possible. Les workers de livraison d'e-mails consomment à partir de la file d'attente d'e-mails et envoient via des fournisseurs tels que SES, SendGrid ou Mailgun. Ils doivent prendre en charge le basculement du fournisseur, la gestion des rebonds, les listes de suppression, la conformité de désabonnement et les limites de débit par fournisseur. Les notifications par e-mail peuvent être traitées par lots ou regroupées pour les types d'événements à faible priorité comme les likes, tandis que les messages directs ou les événements liés à la sécurité peuvent être envoyés immédiatement. Comme l'e-mail est plus lent et plus coûteux, les préférences utilisateur et la limitation du débit sont particulièrement importantes. La fiabilité est assurée par des écritures durables, un traitement au moins une fois, l'idempotence, les nouvelles tentatives et les files d'attente de lettres mortes. La couche d'ingestion n'accuse réception aux producteurs qu'après que l'événement a été écrit de manière durable dans Kafka/Pulsar. Les consommateurs ne valident les offsets qu'après avoir écrit avec succès les enregistrements de notification et publié les tâches de canal en aval. Comme les nouvelles tentatives peuvent créer des doublons, chaque événement et notification doit avoir des clés d'idempotence stables. Par exemple, une clé de notification de like pourrait être recipient_id + actor_id + post_id + event_type, tandis qu'une clé de notification de commentaire pourrait inclure comment_id. Le magasin de notifications impose l'unicité sur cette clé, ou les processeurs effectuent des écritures conditionnelles. Les workers de livraison doivent également utiliser des ID de tentative et des transitions d'état idempotentes afin que les tâches dupliquées ne créent pas d'enregistrements dans l'application dupliqués ou d'e-mails dupliqués lorsque cela est évitable. Le système garantit une livraison au moins une fois, pas une livraison exactement une fois, donc les clients doivent également dédupliquer par ID de notification. Les files d'attente de lettres mortes sont nécessaires pour les messages empoisonnés, les événements malformés, les échecs répétés du fournisseur ou les enregistrements qui ne peuvent pas être rendus. Un outil de relecture doit permettre aux opérateurs de corriger les problèmes et de retraiter les événements à partir du journal durable d'origine ou de la file d'attente de lettres mortes. La rétention de Kafka doit être suffisamment longue pour permettre la récupération opérationnelle, par exemple plusieurs jours. Les métadonnées critiques et l'état de livraison doivent également être persistés dans la base de données de notification pour l'auditabilité. Pour répondre à l'exigence d'échelle de 100 millions d'utilisateurs actifs quotidiens et 50 000 requêtes de notification par seconde, tous les services majeurs doivent être horizontalement évolutifs et sans état si possible. Les API d'ingestion s'adaptent derrière des équilibreurs de charge. Les sujets Kafka/Pulsar sont suffisamment partitionnés pour supporter le débit de pointe et le parallélisme des consommateurs. Les processeurs et les workers de livraison s'exécutent dans des groupes d'autoscaling ou des déploiements Kubernetes et s'adaptent en fonction du décalage de la file d'attente, du CPU, de la latence du fournisseur et du taux de requêtes. Les bases de données sont partitionnées par ID utilisateur pour répartir la charge. Les problèmes de clé chaude doivent être gérés avec des tâches de diffusion sharding, une gestion spéciale des utilisateurs célèbres et une backpressure. Pour une diffusion extrêmement large, le système peut utiliser une diffusion pull pour les notifications à faible priorité : au lieu d'écrire une notification par follower immédiatement, il stocke l'événement une fois et le matérialise lorsqu'un utilisateur ouvre l'application. Cela réduit l'amplification des écritures mais augmente la complexité des lectures et peut ne pas convenir aux messages directs ou aux commentaires. La cible de latence de 2 secondes pour 99 % des notifications push et dans l'application est atteinte en maintenant le chemin critique court : producteur vers journal durable, recherche de préférences du processeur depuis le cache, écriture de l'enregistrement de notification, publication dans la file d'attente du canal, et livraison immédiate par des workers chauds. Les workers push et dans l'application doivent être surprovisionnés pour la charge de pointe, et les files d'attente doivent utiliser des voies prioritaires afin que les messages directs et les commentaires soient traités avant les likes à faible priorité. Le déploiement régional réduit la latence réseau. Pour les utilisateurs dans plusieurs régions, le routage peut être basé sur la région d'origine du destinataire, avec une réplication inter-régions pour la reprise après sinistre. La conception doit mesurer la latence de bout en bout, de la création de l'événement à l'acceptation du fournisseur ou à l'envoi de la passerelle, et non pas seulement le temps de traitement interne. L'observabilité est essentielle. La plateforme doit suivre le taux de requêtes, le décalage de la file d'attente par partition, la latence de traitement, le taux de réussite du cache de préférences, les échecs de création de notifications, les taux d'erreur des fournisseurs, les nombres de nouvelles tentatives, le volume de lettres mortes, le succès de la livraison WebSocket, la latence d'acceptation du fournisseur push, et la latence p50/p95/p99 de bout en bout. Le traçage distribué doit transporter l'ID d'événement d'origine à travers chaque composant. Des alertes doivent se déclencher en cas de croissance du décalage de la file d'attente, d'échecs élevés, de throttling du fournisseur, de latence d'écriture de base de données et de SLO de latence manqués. Il existe plusieurs compromis. Kafka ou Pulsar ajoutent une complexité opérationnelle mais fournissent la durabilité, la relecture et le débit nécessaires à une livraison au moins une fois à cette échelle. Cassandra ou DynamoDB offrent une évolutivité et une disponibilité d'écriture élevées mais une capacité de requête ad hoc limitée, donc les analyses doivent être envoyées à un entrepôt de données séparé. La mise en cache des préférences améliore la latence mais introduit des problèmes de cohérence de courte durée. La livraison au moins une fois est beaucoup plus simple et plus fiable que la livraison exactement une fois, mais le système doit tolérer les doublons en utilisant des clés d'idempotence et une déduplication côté client. La diffusion à l'écriture donne des lectures rapides de la boîte de réception mais peut être coûteuse pour les événements à fort nombre de followers ; la diffusion à la lecture économise les écritures mais complique le classement et la matérialisation au moment de la lecture. Un modèle hybride est généralement le meilleur : diffusion à l'écriture pour les notifications directes, importantes et de volume normal, et diffusion à la lecture ou diffusion par lots pour les événements massifs à faible priorité. En résumé, le service doit utiliser une architecture événementielle durable avec des flux de messages partitionnés, des processeurs sans état, des recherches de préférences mises en cache, un stockage persistant des notifications, des files d'attente de canaux séparées, des workers spécifiques aux canaux et des mécanismes solides de nouvelle tentative/idempotence. Cette conception s'adapte horizontalement à 50 000 requêtes de notification par seconde, maintient la livraison push et dans l'application dans la cible p99 de 2 secondes grâce à la mise en cache et aux files d'attente à faible latence, et garantit que les notifications ne sont pas perdues en persistant les événements et les enregistrements de notification avant d'accuser réception des progrès.
Resultat
Votes gagnants
3 / 3
Score moyen
Score total
Commentaire global
La réponse A présente une architecture de haut niveau complète et cohérente avec des responsabilités claires pour les composants, un flux de données réaliste et une forte attention aux aspects opérationnels. Elle aborde directement tous les canaux requis, les objectifs de latence, la sémantique de livraison au moins une fois, la gestion des préférences, les scénarios de diffusion à grande échelle (fanout), l'idempotence, les nouvelles tentatives, la persistance et l'observabilité. La discussion technologique est équilibrée et nuancée, avec des compromis concrets tels que le fanout à l'écriture par rapport au fanout à la lecture, la cohérence du cache et la complexité opérationnelle de Kafka/Pulsar. La principale faiblesse est qu'elle est quelque peu longue et pourrait être plus condensée, mais techniquement, elle est solide et bien alignée avec la consigne.
Afficher le detail de l evaluation ▼
Qualite de l architecture
Poids 30%L'architecture est bien structurée et de bout en bout : ingestion, journal durable, processeurs, service de préférences, service de modèles, stockage de notifications, files d'attente par canal, agents de diffusion, passerelle en temps réel et observabilité s'articulent de manière cohérente. Elle distingue également l'état persistant dans l'application de la diffusion en temps réel et gère la diffusion à grande échelle (fanout) comme une préoccupation de premier ordre.
Completude
Poids 20%Elle couvre tous les types de notifications requis, les préférences utilisateur, l'échelle, la latence, la fiabilité, les choix technologiques et les compromis. Elle ajoute également des préoccupations pratiques importantes manquantes telles que le registre des appareils, les files d'attente de messages rejetés (dead-letter queues), les clés d'idempotence, le traitement par lots pour le fanout, le déploiement régional, l'observabilité et les outils de récupération.
Analyse des compromis
Poids 20%La réponse fournit un raisonnement comparatif solide pour Kafka/Pulsar, les choix de bases de données NoSQL, la cohérence du cache, la livraison au moins une fois par rapport à exactement une fois, et le fanout à l'écriture par rapport au fanout à la lecture. Ces compromis sont concrets et directement liés à la charge de travail et au comportement du produit.
Scalabilite et fiabilite
Poids 20%C'est une force majeure. La conception explique clairement la mise à l'échelle horizontale, le partitionnement, l'isolement des files d'attente par canal, l'atténuation des clés 'chaudes' (hot-key), les nouvelles tentatives, la gestion des offsets des consommateurs, les écritures conditionnelles pour la déduplication, les files d'attente de messages rejetés, la relecture et la durabilité avant acquittement. Elle prend en charge directement la livraison au moins une fois et l'objectif de 2 secondes avec des mécanismes réalistes.
Clarte
Poids 10%L'explication est claire, logiquement ordonnée et précise malgré sa longueur. Elle communique bien le flux de données, bien que sa longueur la rende légèrement plus dense et moins immédiatement consultable qu'une réponse plus structurée.
Score total
Commentaire global
La réponse A fournit une conception de système exceptionnellement détaillée et robuste. Elle démontre une compréhension approfondie des défis complexes des systèmes distribués, tels que le fanout pour les comptes de célébrités, la construction spécifique de clés d'idempotence et les nuances de la livraison au moins une fois. L'architecture est très granulaire, bien raisonnée et aborde explicitement toutes les exigences avec des solutions sophistiquées et des discussions sur les compromis, reflétant l'expertise attendue d'un ingénieur logiciel senior.
Afficher le detail de l evaluation ▼
Qualite de l architecture
Poids 30%La réponse A présente une architecture très détaillée et logique, séparant clairement les préoccupations et fournissant des solutions robustes pour des scénarios complexes tels que le fanout à grande échelle et la livraison en deux étapes dans l'application. Les interactions entre les composants sont bien définies.
Completude
Poids 20%La réponse A aborde de manière exhaustive toutes les exigences, y compris les sujets avancés tels que des exemples spécifiques de clés d'idempotence, une observabilité détaillée et des stratégies de fanout nuancées (à l'écriture vs à la lecture), démontrant une compréhension très complète.
Analyse des compromis
Poids 20%La réponse A intègre des discussions sur les compromis tout au long de la conception et met explicitement en évidence les compromis fondamentaux de la conception de systèmes (par exemple, au moins une fois vs exactement une fois, stratégies de fanout), démontrant une compréhension approfondie des implications au-delà des simples choix technologiques.
Scalabilite et fiabilite
Poids 20%La réponse A offre une excellente couverture de la scalabilité et de la fiabilité, détaillant des mécanismes spécifiques tels que les stratégies de partitionnement, les commits de décalage des consommateurs, les écritures durables avant acquittement, la gestion des clés chaudes et les files d'attente prioritaires, démontrant une solide maîtrise des détails d'implémentation.
Clarte
Poids 10%La réponse A est très claire, bien structurée et utilise un langage professionnel, ce qui rend la conception complexe facile à suivre malgré sa profondeur. Le flux logique est excellent.
Score total
Commentaire global
La réponse A propose une conception de système axée sur la prose et profondément réfléchie, qui aborde des problèmes subtils et importants : partitions chaudes pour le fanout de célébrités, hybride fanout-on-write vs fanout-on-read, construction de clés d'idempotence, routage de présence pour WebSockets, déploiement régional, files d'attente prioritaires, et la distinction entre acceptation par le fournisseur et réception par l'utilisateur. Les compromis sont discutés en contexte plutôt que listés superficiellement. Le récit est long mais cohérent et démontre une profondeur de niveau senior. Faiblesses mineures : manque un schéma visuel et des titres/tableaux structurés qui faciliteraient la lecture rapide.
Afficher le detail de l evaluation ▼
Qualite de l architecture
Poids 30%Décomposition complète des composants avec une gestion sophistiquée du fanout, du partitionnement par destinataire, du routage de présence, des files d'attente de canaux séparées et d'un magasin de notifications persistant comme source de vérité. Traite des problèmes subtils tels que le fanout de célébrités et les files d'attente prioritaires.
Completude
Poids 20%Couvre l'ingestion, le journal durable, les processeurs, les préférences, les modèles, le magasin de notifications, les files d'attente de canaux, les workers, la passerelle WebSocket, le registre des appareils, la DLQ, l'observabilité, le déploiement régional et la gestion explicite des quatre exigences.
Analyse des compromis
Poids 20%Discute des compromis concrets en contexte : au moins une fois vs exactement une fois, hybride fanout-on-write vs fanout-on-read, fraîcheur du cache vs cohérence, partitionnement par destinataire vs ID d'événement, complexité opérationnelle de Kafka vs avantages de durabilité.
Scalabilite et fiabilite
Poids 20%Solide argumentaire de fiabilité : écritures durables avant acquittement, validation des offsets après succès en aval, clés d'idempotence avec exemples concrets, DLQ avec outils de rejeu, atténuation des clés chaudes, déploiement régional pour la latence, files d'attente prioritaires.
Clarte
Poids 10%Prose bien structurée mais très longue avec peu d'aides visuelles ; les paragraphes denses rendent la lecture plus difficile malgré un flux logique.