Naviguer jusqu'au contenu principal

La quĂȘte de rapiditĂ© dans Figma

Notre enquĂȘte de plusieurs mois sur la lenteur des temps de recherche a dĂ©bouchĂ© sur une solution qui a permis non seulement d'amĂ©liorer les performances, mais aussi de jeter les bases d'une Ă©volution future.

Partager La quĂȘte de rapiditĂ© dans Figma

Illustration et animation de Chou Chia Yu

En début d'année, nous avons entrepris d'améliorer le fonctionnement de la recherche chez Figma. Bien que l'infrastructure de recherche ait toujours été intégrée à Figma, notre croissance a rendu de plus en plus difficile pour notre systÚme de recherche de localiser de maniÚre fiable le contenu que les utilisateurs recherchent. Pour les mois et années à venir, nous souhaitions établir une base solide pour les recherches.

ElasticSearch est un moteur de recherche qui peut ĂȘtre personnalisĂ© pour rechercher parmi des milliards de documents.

Jusqu'à la fin de l'année 2023, nous nous appuyions sur une ancienne version d'ElasticSearch pour effectuer des recherches dans Figma. Nous avons ensuite lancé la mise à niveau vers OpenSearch, exécuté dans le cadre du service OpenSearch géré par AWS. OpenSearch est un dérivé d'ElasticSearch, créé lorsque la licence d'ElasticSearch a changé en 2021. Bien que les deux soient en grande partie compatibles, de petites différences se sont accumulées au cours des trois derniÚres années, rendant la migration plus difficile que prévu.

Recherche à l'échelle

J'ai rejoint l'équipe de recherche de Figma avec trÚs peu d'expérience de production avec ElasticSearch ou OpenSearch, mais une grande expérience dans la mise à l'échelle de grands services Web. Comme toujours lorsque je m'attaque à des problÚmes avec les services Web, je cherche à comprendre ce qui se passe en arriÚre-plan avec OpenSearch.

Dans un monde idĂ©al, le temps de recherche indiquĂ© par OpenSearch et le temps indiquĂ© par notre application devaient ĂȘtre trĂšs proches, mais dans ce cas, l'un Ă©tait 120 fois plus long que l'autre. Notre service de recherche n'est pas « si » compliquĂ©. Alors, qu'est-ce qui a pris du temps ?

Nous avons appris grĂące Ă  l'intĂ©gration native OpenSearch de DataDog que notre « recherche moyenne » prenait environ huit millisecondes (ms). Un dĂ©lai aussi court semblait incroyable (et peu probable) pour une recherche Ă  travers de nombreux tĂ©raoctets de donnĂ©es, mĂȘme si ces donnĂ©es Ă©taient fragmentĂ©es en centaines de fragments d'index consultables en parallĂšle. Dans le mĂȘme temps, nous savions que l'API de recherche de notre service avait une latence du 99e centile de prĂšs d'une seconde. Quelque chose ne collait pas.

Pour approfondir la question, nous avons ajouté une instrumentation supplémentaire à notre code afin de voir ce qui avait pris du temps. Nous avons ajouté des métriques et des traces autour de la plupart des grands blocs de code interne derriÚre la recherche. Cela nous a permis de comprendre plusieurs idées clés :

  1. OpenSearch aurait pu prétendre qu'il avait une latence moyenne de huit millisecondes, mais nos appels à leur bibliothÚque d'API ont enregistré une latence moyenne de 150 ms et une latence du 99e centile de 200 à 400 ms. Notre temps de latence minimum était supérieur à 40 ms, ce qui était bien plus élevé que le temps de latence maximum indiqué.
  2. Nous avons passĂ© beaucoup de temps Ă  Ă©laborer nos requĂȘtes avant mĂȘme de les transmettre Ă  OpenSearch.
  3. Nous avons consacrĂ© encore plus de temps Ă  vĂ©rifier les autorisations sur les rĂ©sultats des requĂȘtes aprĂšs leur rĂ©ception, ce que nous appelons le post-traitement, pour nous assurer de ne jamais renvoyer des rĂ©sultats de fichiers Ă  des utilisateurs qui n'ont pas l'autorisation de les consulter.
  4. Nos performances n'étaient pas trÚs stables et variaient considérablement d'une heure et d'un jour à l'autre. Aux heures de pointe, nous étions des centaines de millisecondes plus lents que pendant le week-end.

À ce stade, nous tentions de rĂ©concilier deux informations contradictoires : OpenSearch indiquait que les recherches prenaient environ huit millisecondes alors que notre code indiquait qu'elles prenaient environ 150 ms. Cela n'aurait de sens que si les deux parties se trouvaient Ă  l'autre bout du monde, mais en rĂ©alitĂ©, elles fonctionnent dans la mĂȘme zone de disponibilitĂ© AWS, Ă  seulement quelques millisecondes l'une de l'autre. Nous avons donc Ă©tudiĂ© en profondeur la documentation et avons dĂ©couvert que la mesure de huit millisecondes que nous avions examinĂ©e Ă©tait en fait le temps de requĂȘte moyen sur un seul fragment, et non le temps de requĂȘte global.

OpenSearch chez Figma

Pour comprendre comment nous en sommes arrivĂ©s lĂ , il faut comprendre le fonctionnement d'OpenSearch (et d'ElasticSearch) et leur utilisation au sein de Figma. Comme la plupart des services Web, nous avons un calque API qui traduit les actions de l'utilisateur en divers services back-end. Lorsque OpenSearch reçoit une requĂȘte, il atteint un « nƓud coordinateur » qui envoie ensuite une copie de la requĂȘte aux « nƓuds de travail », une requĂȘte par fragment pour l'index interrogĂ©. En gĂ©nĂ©ral, tous les nƓuds se relaient pour assurer la fonction de coordinateur. C'est ce qu'on appelle la phase de « requĂȘte » d'une recherche OpenSearch. Le coordinateur recueille ensuite les rĂ©sultats, les trie et demande gĂ©nĂ©ralement des informations complĂ©mentaires aux fragments qui ont fourni les meilleurs rĂ©sultats. C'est ce qu'on appelle la phase de « rĂ©cupĂ©ration » d'une recherche OpenSearch. Enfin, il renvoie les rĂ©sultats au client.

Le dĂ©lai de huit millisecondes ne couvrait que chacune des requĂȘtes individuelles par fragment entre le nƓud coordinateur et les nƓuds de travail. Dans notre configuration initiale, il y avait potentiellement 500 requĂȘtes par segment et par utilisateur. Beaucoup de ces processus se dĂ©roulent en parallĂšle, mais pas tous. C'est lĂ  que l'Ă©cart est apparu : nous n'utilisions pas la bonne mĂ©trique. Nous avions besoin de la mĂ©trique de latence qui couvre la vue du nƓud coordinateur de la requĂȘte. AprĂšs avoir consultĂ© la documentation et fait des recherches approfondies sur Google et Stack Overflow, nous nous sommes tournĂ©s vers notre gestionnaire de compte chez AWS pour obtenir de l'aide. Il s'avĂšre qu'aucune des mĂ©triques et aucun des journaux d'OpenSearch ne suit en fait le temps de requĂȘte global, mais seulement le temps par segment.

En fait, le seul endroit oĂč OpenSearch fait Ă©tat des performances globales de la requĂȘte est dans la rĂ©ponse de l'API de la requĂȘte, qui comprend un champ (« took ») indiquant le nombre de millisecondes nĂ©cessaires Ă  OpenSearch pour rĂ©pondre Ă  la requĂȘte. Nous avons donc extrait cette valeur de chaque rĂ©ponse Ă  une recherche et l'avons intĂ©grĂ©e Ă  notre systĂšme de suivi. Cela nous a permis d'obtenir un temps de latence qui correspondait en grande partie au wrapper de temps que nous avions placĂ© autour des appels de l'API OpenSearch. AprĂšs avoir regroupĂ© plusieurs ensembles de donnĂ©es, nous avons pu mieux comprendre le rĂ©cit que nous prĂ©sentait notre systĂšme de surveillance.

Malheureusement, le rĂ©cit nous a principalement indiquĂ© que nous avons passĂ© moins de 30 % de notre temps total d'API de requĂȘte Ă  attendre OpenSearch, et en rĂ©alitĂ©, le prĂ©traitement et le post-traitement ont pris plus de temps que la recherche. La phase de prĂ©traitement rĂ©cupĂšre un ensemble d'informations Ă  propos des fichiers auxquels l'utilisateur peut accĂ©der et crĂ©e une clause de filtrage OpenSearch qui exclut principalement les fichiers auxquels il ne peut pas accĂ©der. La phase de post-traitement vĂ©rifie ensuite que l'utilisateur a effectivement accĂšs Ă  chaque fichier renvoyĂ©.

Les deux Ă©tapes Ă©taient lentes, en particulier le post-traitement. Nous avons travaillĂ© avec notre Ă©quipe d'autorisations pour comprendre et amĂ©liorer les performances de post-traitement. L'analyse statistique a rĂ©vĂ©lĂ© que l'Ă©valuation des diffĂ©rentes parties du systĂšme d'autorisation dans un ordre diffĂ©rent pourrait produire les mĂȘmes rĂ©sultats en beaucoup moins de temps. Nous avons Ă©galement dĂ©couvert que nous consacrions Ă©normĂ©ment de temps Ă  vĂ©rifier la sĂ©curitĂ© des types au moment de l'exĂ©cution en Ruby dans le systĂšme d'autorisations et la dĂ©sactivation des parties les plus intrusives de ce systĂšme a permis de rĂ©duire les dĂ©lais de façon substantielle.

Lorsque nous avons commencĂ© Ă  examiner les traces de recherche lente, nous avons remarquĂ© un certain nombre de requĂȘtes de base de donnĂ©es Ă©trangement lentes. La base de donnĂ©es elle-mĂȘme Ă©tait rapide, de mĂȘme que le proxy d'Ă©quilibrage de charge entre nous et la base de donnĂ©es, mais l'Ă©mission de requĂȘtes prenait parfois des dizaines de millisecondes. AprĂšs des heures passĂ©es Ă  Ă©tudier le code source et les traces, un membre de l'Ă©quipe a identifiĂ© un problĂšme dans la maniĂšre dont nous Ă©tablissons de nouvelles connexions Ă  la base de donnĂ©es dans de nouveaux threads. Notre pool de connexions Ă  la base de donnĂ©es n'Ă©tait pas assez important, ce qui nous obligeait Ă  effectuer des opĂ©rations coĂ»teuses de configuration et de dĂ©montage qui n'Ă©taient jamais nĂ©cessaires dans chaque thread. Le problĂšme principal a Ă©tĂ© rĂ©solu et nous avons constatĂ© des accĂ©lĂ©rations substantielles, non seulement dans la recherche, mais aussi pour l'ensemble de Figma. Avec cette vision en tĂȘte, plusieurs expĂ©riences de threading que nous avions tentĂ©es auparavant ont Ă©tĂ© réévaluĂ©es, alors que des lectures parallĂšles de bases de donnĂ©es dans les threads avaient que rarement amĂ©liorĂ© les performances. GrĂące au nouveau code d'initialisation, la majoritĂ© des occasions d'exĂ©cuter des requĂȘtes en parallĂšle ont permis une accĂ©lĂ©ration de Figma.

Évaluation et amĂ©lioration des performances

Une fois que nous nous sommes alignés sur des indicateurs de performance raisonnables et que nous avons amélioré quelques-uns des problÚmes majeurs, il était temps d'aller plus loin. Jusque-là, nous n'avions pas suffisamment de signaux pour répondre à certaines questions clés, mais à ce stade, nous pouvions laisser les données nous guider :

  • Quelle est la qualitĂ© de nos requĂȘtes OpenSearch ?
  • Nos index contiennent-ils les bonnes donnĂ©es ?
  • Avons-nous la bonne configuration OpenSearch ?

Quelle est la qualitĂ© de nos requĂȘtes OpenSearch ?

Avec un peu de travail, vous pouvez faire en sorte que le profileur de requĂȘtes d'OpenSearch vous indique la quantitĂ© de travail qu'il effectue et oĂč il passe du temps. Nous avons dĂ©couvert que la plupart de nos requĂȘtes contournaient la plupart des millions de documents que nous avons par fragment d'index. Elles ne prenaient en compte que quelques centaines de documents par fragment et par requĂȘte. Cela est dĂ» au fait que le filtrage que nous avons construit dans notre Ă©tape de prĂ©traitement parvient Ă  Ă©liminer la majoritĂ© des fichiers auxquels les utilisateurs ne peuvent pas accĂ©der et l'optimisation des requĂȘtes d'OpenSearch en tire pleinement parti. Cela signifie que le problĂšme n'Ă©tait pas liĂ© Ă  nos requĂȘtes.

Disposons-nous des bonnes données ?

La question reste ouverte, car il s'agit, au moins en partie, d'une question de pertinence de la recherche, et pas seulement de performance. Avons-nous suffisamment de données pour trouver ce que l'utilisateur recherche ? Le trouvons-nous réellement ? Ce n'est pas un problÚme simple, et nous menons constamment des expériences pour essayer de l'améliorer. Cependant, nous avons découvert que la grande majorité des données que nous avions introduites dans OpenSearch n'étaient pas trÚs utiles. Nous avons pu réduire la taille de notre index à plusieurs reprises, d'abord de 50 %, puis de 90 % supplémentaires, sans impact mesurable sur la pertinence. Ainsi, tout est plus rapide, plus simple et moins coûteux.

Avons-nous la bonne configuration OpenSearch ?

OpenSearch offre une grande flexibilitĂ©, qui ne va pas sans une certaine complexitĂ©. Par exemple, Amazon rĂ©pertorie 139 types d'instances de serveur OpenSearch diffĂ©rents avec une large gamme d'options de processeur et de mĂ©moire, avec des tarifs allant de 0,02 $ Ă  17 $ de l'heure. Chaque index doit ĂȘtre divisĂ© en un certain nombre de fragments, mais le nombre optimal de fragments dans une situation donnĂ©e est loin d'ĂȘtre Ă©vident. OpenSearch prend en charge un certain nombre de fonctionnalitĂ©s qui vous permettent de faire des compromis entre l'utilisation du processeur, de la mĂ©moire et du disque, comme les types de compression et la simultanĂ©itĂ© de la recherche. Toutes sont ajustables et aucune n'a de valeurs « spĂ©cifiques ». Amazon fournit des conseils sur le dimensionnement, notamment en ce qui concerne la quantitĂ© de donnĂ©es par nƓud OpenSearch et le rapport entre le nombre de fragments et le nombre de nƓuds, mais nous avons dĂ©couvert que la plupart de leurs recommandations Ă©taient basĂ©es sur des charges de travail intensives en dĂ©bit, comme l'interrogation de journaux, et non sur la recherche de documents sensible Ă  la latence.

Finalement, la seule maniĂšre de comprendre comment toutes ces options affectaient nos performances de recherche Ă©tait de rĂ©aliser des tests et d'en mesurer l'impact. Nous avons donc commencĂ© Ă  mettre en place un systĂšme de test de charge. Nous avons créé de nouveaux clusters OpenSearch non productifs, y avons chargĂ© des donnĂ©es, exĂ©cutĂ© des requĂȘtes, mesurĂ© les rĂ©sultats, puis apportĂ© des modifications et recommencĂ© les tests. OpenSearch dispose de son propre outil de benchmarking, (qui porte le nom trĂšs original d'opensearch-benchmark), mais nous n'avons jamais vraiment Ă©tĂ© en mesure d'en tirer des rĂ©sultats cohĂ©rents. Il est conçu pour effectuer des tests de rĂ©gression des performances pour le dĂ©veloppement d'OpenSearch, et n'est pas aussi performant pour envoyer un grand nombre de requĂȘtes alĂ©atoires aux instances OpenSearch existantes. De plus, curieusement, il n'aime pas vraiment utiliser le nombre de latences « pris » cĂŽtĂ© serveur, ce qui signifie que toutes les mesures de latence sont basĂ©es sur les performances cĂŽtĂ© client. Il est donc difficile d'obtenir des exĂ©cutions rĂ©pĂ©tables dans notre environnement. Finalement, nous avons créé notre propre outil de benchmarking en Go en une aprĂšs-midi.

Cela nous a conduits à quelques conclusions clés :

  • Nous avions trop de fragments dans nos index. Nous avons commencĂ© avec 450 fragments et avons fini avec 180 fragments, soit une rĂ©duction de 60 %. Cela nous a permis d'augmenter de plus de 50 % notre taux maximal de requĂȘtes avant que la latence excessive ne s'installe. Étonnamment, en rĂ©duisant le nombre de fragments Ă  partir desquels le coordinateur devait collecter des donnĂ©es, notre latence P50, c'est-Ă -dire la latence mĂ©diane, a Ă©galement diminuĂ©.
  • La rĂ©duction de la quantitĂ© de donnĂ©es dans nos index a reprĂ©sentĂ© une amĂ©lioration significative. C'est en partie ce qui nous a convaincus que nous pouvions rĂ©duire notre nombre de fragments en toute sĂ©curitĂ©. En rĂ©duisant la quantitĂ© de donnĂ©es, nous avons amĂ©liorĂ© notre taux de rĂ©ussite du cache disque, ce qui a rendu toutes les performances plus prĂ©visibles. L'optimisation initiale qui a rĂ©duit la taille de notre index de 50 % a permis de rĂ©duire la latence de nos requĂȘtes et de la rendre plus cohĂ©rente ; une rĂ©duction supplĂ©mentaire de 90 %, en supprimant les donnĂ©es inutilisĂ©es, a permis Ă  l'ensemble de notre ensemble de donnĂ©es de s'insĂ©rer dans le cache disque du systĂšme d'exploitation et de le rendre beaucoup plus rapide.
  • Les recommandations d'Amazon n'Ă©taient pas adaptĂ©es Ă  notre utilisation. En examinant les recommandations de dimensionnement d'AWS, nous avons constatĂ© que nos mesures ne correspondaient pas du tout Ă  leurs recommandations. Ils nous ont suggĂ©rĂ© de limiter les fragments Ă  50 Go et de prĂ©voir environ un fragment d'index pour 1,5 processeur du cluster. Bien que cela puisse avoir du sens pour les recherches de type journal, dans le cas de recherches de type document oĂč la latence est importante, cela signifie que la tĂąche de vos coordinateurs n'est pas gĂ©rable pour des milliers de fragments Ă  chaque requĂȘte. Étant donnĂ© que nos filtres Ă©taient trĂšs efficaces, nous avons constatĂ© de meilleures performances avec moins de fragments consĂ©quents.
  • Nous avions configurĂ© les nƓuds OpenSearch avec trop de processeurs (coĂ»teux) et pas assez de RAM (beaucoup moins cher). Nous avons pu passer Ă  des nƓuds avec un tiers de processeur et 25 % de RAM en plus pour environ la moitiĂ© du prix par nƓud et obtenir des performances lĂ©gĂšrement meilleures dans l'ensemble, mĂȘme avant de rĂ©duire la taille de nos index.
  • La compression Zstandard (zstd) n'a pas Ă©tĂ© un succĂšs majeur, mais elle n'a pas causĂ© de problĂšme non plus.
  • La recherche par segments simultanĂ©s n'a jamais Ă©tĂ© une rĂ©ussite pour notre cas d'utilisation. Tout a commencĂ© avec quelques millisecondes de latence supplĂ©mentaire Ă  des taux de requĂȘte faibles et la latence a augmentĂ© beaucoup plus rapidement Ă  mesure que la charge de requĂȘte augmentait. Cela nous a surpris, car nous avions encore beaucoup de processeurs libres et nous nous attendions Ă  ce que l'augmentation du parallĂ©lisme rĂ©duise la latence, mais cela ne semblait pas ĂȘtre le cas.

Dans l'ensemble, l'Ă©quipe de recherche de Figma a rĂ©ussi Ă  rĂ©duire la latence de l'API d'environ 60 %, Ă  augmenter le nombre maximal de requĂȘtes par seconde d'au moins 50 % et Ă  rĂ©duire le coĂ»t total de plus de 50 %. Ce rĂ©sultat a Ă©tĂ© obtenu grĂące Ă  un travail approfondi dans de nombreux domaines, allant de la surveillance et de la correction des bugs Ă  la rĂ©duction de la taille de l'index et Ă  l'optimisation des configurations. Bien qu'il n'existe pas de solution miracle, notre effort collaboratif a considĂ©rablement amĂ©liorĂ© les performances de recherche de Figma et nous a prĂ©parĂ©s Ă  une Ă©volutivitĂ© future. La recherche de vitesse n'est jamais vraiment terminĂ©e, mais nous avons fait un grand bond en avant et nous sommes prĂȘts Ă  relever tous les dĂ©fis qui se prĂ©senteront.

Créer et collaborer avec Figma

Se lancer gratuitement
Powered by Fruition