How ShareChat engineers rebuilt a low-latency ML feature store on ScyllaDB after an initial scalability failure — and what they learned along the way La demande de magasins de fonctionnalités d'apprentissage automatique à faible latence est plus élevée que jamais, mais la mise en œuvre d'un magasin de fonctionnalités à faible latence reste un défi.Cela est devenu évident lorsque les ingénieurs ShareChat Ivan Burmistrov et Andrei Manakov ont pris la phase P99 CONF 23 pour partager comment ils ont construit un magasin de fonctionnalités ML à faible latence basé sur ScyllaDB. Ce n'est pas une étude de cas ordonnée où l'adoption d'un nouveau produit sauve la journée.C'est une histoire de "leçons apprises", un regard sur la valeur de l'optimisation des performances sans relâche - avec quelques étapes d'ingénierie importantes. L’objectif ultime était de supporter 1 milliard de fonctionnalités par seconde, mais le système a échoué sous une charge de seulement 1 million.Avec une résolution intelligente des problèmes, l’équipe l’a cependant retirée.Voyons comment leurs ingénieurs ont réussi à se détourner de l’échec initial pour atteindre leur objectif de performance élevé sans évoluer la base de données sous-jacente. ShareChat : la première plateforme de médias sociaux en Inde Pour comprendre l’étendue du défi, il est important de connaître un peu ShareChat, la plate-forme de médias sociaux leader en Inde. Sur l’application ShareChat, les utilisateurs découvrent et consomment du contenu dans plus de 15 langues différentes, y compris des vidéos, des images, des chansons et plus. ShareChat héberge également une plate-forme vidéo courte (Moj) similaire à TikTok qui encourage les utilisateurs à être créatifs avec des tags et des concours tendance. Entre les deux applications, ils servent une base d'utilisateurs en croissance rapide qui compte déjà plus de 325 millions d'utilisateurs actifs mensuels. Télécharger sur ShareChat Cette histoire se concentre sur le système derrière les boutiques de fonctionnalités ML pour l'application vidéo à court format Moj. Il offre des flux entièrement personnalisés à environ 20 millions d'utilisateurs actifs quotidiens, 100 millions d'utilisateurs actifs mensuels. Les flux servent 8 000 demandes par seconde, et il y a en moyenne 2 000 candidats de contenu classés sur chaque demande (par exemple, pour trouver les 10 meilleurs articles à recommander). Ivan Burmistrov, ingénieur logiciel principal de ShareChat, a expliqué : Nous calculons les fonctionnalités pour différentes « entités ». Post est une entité, l’Utilisateur est un autre et ainsi de suite. Du point de vue du calcul, ils sont assez similaires. Cependant, la différence importante est dans le nombre de fonctionnalités que nous devons collecter pour chaque type d’entité. Lorsqu’un utilisateur demande un flux, nous collections les fonctionnalités de l’utilisateur pour cet utilisateur unique. Cependant, pour classer tous les messages, nous devons collecter les fonctionnalités pour chaque candidat (post) classé, de sorte que la charge totale sur le système générée par les fonctionnalités du poste est beaucoup plus grande que celle générée par les fonctionnalités de l’utilisateur. Cette différence joue un rôle important dans notre histoire. Nous calculons les fonctionnalités pour différentes « entités ». Post est une entité, l’Utilisateur est un autre et ainsi de suite. Du point de vue du calcul, ils sont assez similaires. Cependant, la différence importante est dans le nombre de fonctionnalités que nous devons collecter pour chaque type d’entité. Lorsqu’un utilisateur demande un flux, nous collections les fonctionnalités de l’utilisateur pour cet utilisateur unique. Cependant, pour classer tous les messages, nous devons collecter les fonctionnalités pour chaque candidat (post) classé, de sorte que la charge totale sur le système générée par les fonctionnalités du poste est beaucoup plus grande que celle générée par les fonctionnalités de l’utilisateur. Cette différence joue un rôle important dans notre histoire. Pourquoi l'architecture de magasin de fonctionnalités initiale n'a pas échelonné Au début, l’accent était mis sur la création d’un magasin de fonctionnalités utilisateur en temps réel car, à ce moment-là, les fonctionnalités utilisateur étaient les plus importantes.L’équipe a commencé à construire le magasin de fonctionnalités avec cet objectif à l’esprit.Mais ensuite les priorités ont changé et les fonctionnalités postales sont aussi devenues le focus.Ce changement s’est produit parce que l’équipe a commencé à construire un tout nouveau système de classement avec deux différences majeures par rapport à son prédécesseur: Les caractéristiques de post en temps réel étaient plus importantes Le nombre de postes à classer a augmenté de centaines à des milliers Ivan a expliqué : « Lorsque nous sommes allés tester ce nouveau système, il a malheureusement échoué. À environ 1 million de caractéristiques par seconde, le système est devenu non réactif, les latences ont traversé le toit et ainsi de suite. » En fin de compte, le problème a découlé de la façon dont l'architecture du système a utilisé des boîtes de données préagrégées appelées carreaux. Par exemple, ils peuvent agréger le nombre de likes pour un message dans une minute donnée ou une autre plage de temps. Cela leur permet de calculer des mesures telles que le nombre de likes pour plusieurs messages au cours des deux dernières heures. Voici un regard de haut niveau sur l'architecture du système.Il y a quelques sujets en temps réel avec des données brutes (j'aime, cliques, etc.). Un travail Flink les agrégue en carreaux et les écrit à ScyllaDB. Ensuite, il y a un service de fonctionnalités qui demande des carreaux à ScyllaDB, les agrégue et renvoie les résultats au service de flux. Le schéma de base de données initial et la configuration de carrelage ont entraîné des problèmes d'évolutivité. À l'origine, chaque entité avait sa propre partition, avec des timestamps de lignes et des noms de fonctionnalités ordonnés par des colonnes de regroupement. [ ]. Les carreaux ont été calculés pour les segments d'une minute, 30 minutes et un jour. Rechercher une heure, un jour, sept jours ou 30 jours a nécessité la collecte d'environ 70 carreaux par fonction en moyenne. Apprenez-en plus dans ce masterclass de modélisation de données NoSQL Si vous faites les mathématiques, il devient clair pourquoi il a échoué. Le système a dû traiter environ 22 milliards de lignes par seconde. Cependant, la capacité de la base de données était seulement 10 millions de lignes/sec. Optimisations de magasin de fonctionnalités précoces: Modélisation des données et changements de carreaux À ce moment-là, l'équipe a entrepris une mission d'optimisation. Le schéma de base de données initial a été mis à jour pour stocker toutes les lignes de fonctionnalités ensemble, sérialisées en buffers de protocole pour un timestamp donné. Parce que l'architecture utilisait déjà Apache Flink, la transition vers le nouveau schéma de carrelage a été assez facile, grâce aux capacités avancées de Flink dans la construction de pipelines de données. Avec cette optimisation, le multiplicateur "Fonctions" a été supprimé de l'équation ci-dessus, et le nombre de lignes requises à récupérer a été réduit de 100X: d'environ 2 milliards à 200 millions de lignes/sec. L’équipe a également optimisé la configuration du carrelage en ajoutant des carreaux supplémentaires pendant cinq minutes, trois heures et cinq jours à une minute, 30 minutes et une journée. Pour gérer plus de lignes/sec sur le côté de la base de données, ils ont changé la stratégie de compression de ScyllaDB d'incrémental à nivellé. Cette option a mieux adapté leurs modèles de requêtes, en gardant les lignes pertinentes ensemble et en réduisant la lecture I/O. Le résultat: la capacité de ScyllaDB a effectivement été doublée. En savoir plus sur les stratégies de compactage La façon la plus facile d'accueillir la charge restante aurait été d'échelonner ScyllaDB 4x. Cependant, les clusters plus / plus grands augmenteraient les coûts et cela n'était tout simplement pas dans leur budget. Améliorer la localisation du cache de stockage pour réduire la charge de base de données L’un des moyens potentiels de réduire la charge sur ScyllaDB était d’améliorer le taux de succès du cache local, de sorte que l’équipe a décidé d’étudier comment cela pourrait être réalisé. Le choix évident était d’utiliser une approche de hachage cohérente, une technique bien connue pour diriger une demande à une certaine réplique du client en fonction de certaines informations sur la demande. Étant donné que l’équipe utilisait NGINX Ingress dans leur configuration Kubernetes, l’utilisation des capacités de hachage cohérent de NGINX semblait être un choix naturel. Cette configuration simple n'a pas fonctionné. en particulier: Le sous-ensemble du client a conduit à un remapage de clés énorme - jusqu'à 100% dans le pire des cas. Puisque les clés de nœuds peuvent être changées dans un anneau de hachage, il était impossible d'utiliser des scénarios de la vie réelle avec l'autoscalage. [Voir la mise en œuvre ingress] Il était difficile de fournir une valeur de hachage pour une demande car Ingress ne prend pas en charge la solution la plus évidente: un en-tête gRPC. La latence a subi une dégradation grave, et il n'était pas clair ce qui provoquait la latence de la queue. Pour soutenir un sous-ensemble des pods, l’équipe a modifié leur approche. Ils ont créé une fonction de hachage en deux étapes : d’abord hachage d’une entité, puis ajout d’un préfixe aléatoire. Ce qui a distribué l’entité sur le nombre souhaité de pods. En théorie, cette approche pourrait provoquer une collision lorsqu’une entité est cartographiée sur le même pod plusieurs fois. Ingress ne prend pas en charge l'utilisation de l'en-tête gRPC en tant que variable, mais l'équipe a trouvé un outil: utiliser la réécriture du chemin et fournir la clé de hachage requise dans le chemin lui-même. Malheureusement, la détermination de la cause de la dégradation de la latence aurait nécessité un temps considérable, ainsi que des améliorations de l'observabilité. Pour respecter le délai, l’équipe a divisé le service Feature en 27 services différents et a divisé manuellement toutes les entités entre eux sur le client. Ce n’était pas l’approche la plus élégante, mais, il était simple et pratique – et il a obtenu d’excellents résultats. Le taux de hit du cache s’est amélioré à 95% et la charge de ScyllaDB a été réduite à 18,4 millions de lignes par seconde. Avec ce design, ShareChat a élargi son magasin de fonctionnalités à 1B de fonctionnalités par seconde d’ici mars. Cependant, cette approche de déploiement de « vieille école » n’était toujours pas la conception idéale. Maintenir 27 déploiements était ennuyeux et inefficace. De plus, le taux de hit du cache n’était pas stable, et l’évolutivité était limitée par la nécessité de maintenir un nombre de pods minimum élevé dans chaque déploiement. La prochaine phase d’optimisation : hashing cohérent, service de fonctionnalités Prêt pour un autre cycle d'optimisation, l'équipe a revu l'approche de hashing cohérente en utilisant un sidecar, appelé Envoy Proxy, déployé avec le service de fonctionnalité. Envoy Proxy a fourni une meilleure observabilité qui a aidé à identifier le problème de la queue de latence. Le problème: les modèles de requête différents au service de la fonctionnalité ont causé une énorme charge sur la couche et le cache gRPC. L’équipe a ensuite optimisé le service Feature. Forked la bibliothèque de mise en cache (FastCache de VictoriaMetrics) et mis en œuvre batch writes et meilleure expulsion pour réduire le contention de mutex de 100x. Forked gprc-go et mis en œuvre buffer pool sur différentes connexions pour éviter la controverse pendant le parallélisme élevé. Le poolage d'objets et les paramètres du collecteur de déchets (GC) sont utilisés pour réduire les taux d'allocation et les cycles de GC. Avec Envoy Proxy gérant 15% du trafic dans leur proof-of-concept, les résultats étaient prometteurs: un taux de hit de cache de 98%, ce qui a réduit la charge sur ScyllaDB à 7,4M rangées/sec. Ils pourraient même évoluer le magasin de fonctionnalités plus: de 1 milliard de caractéristiques/sec à 3 milliards de caractéristiques/sec. Les leçons tirées de l'échelle d'un magasin de fonctionnalités haute performance Voici à quoi ressemblait ce voyage d'un point de vue chronologique: Pour conclure, Andrei a résumé les principales leçons de l’équipe tirées de ce projet (jusqu’à présent): Même si l’équipe de ShareChat a radicalement changé la conception de leur système, ScyllaDB, Apache Flink et VictoriaMetrics ont continué à fonctionner bien. Chaque optimisation est plus difficile que la précédente – et a moins d’impact. Des solutions simples et pratiques (comme la division de la boutique de fonctionnalités en 27 déploiements) fonctionnent vraiment. La solution qui fournit les meilleures performances n’est pas toujours conviviale. Par exemple, leur schéma de base de données révisé donne de bonnes performances, mais est difficile à maintenir et à comprendre. Parfois, vous devrez peut-être forger une bibliothèque par défaut et l'ajuster pour votre système spécifique pour obtenir les meilleures performances.