メインコンテンツまでスキップ

Figmaにおけるスピードの探求

検索速度の遅さについて数か月にわたる調査を行った結果、パフォーマンスを向上させるだけでなく、今後の拡張の基盤となるソリューションが見つかりました。

Figmaにおけるスピードの探求を共有

イラストとアニメーション: Chou Chia Yu

Figmaにおける検索機能の改善に取り組み始めたのは、今年の初めです。当社の成長に伴い、Figmaの設立当初から存在する検索インフラストラクチャでは、ユーザーの求めるコンテンツを検索システムで確実に見つけることがますます難しくなっていました。そこで私たちは、今後数か月から数年で構築できる盤石な検索機能の基盤を確立したいと考えたのです。

ElasticSearchは、数十億のドキュメントを検索できるカスタマイズ可能な検索エンジンです。

2023年末までは、旧バージョンのElasticSearchをFigmaの検索機能に使用していました。その後、AWSのマネージドOpenSearch Serviceの一部として実行されるOpenSearchへのアップグレードを開始しました。OpenSearchは2021年、ElasticSearchライセンスが変更されたときに作成されたElasticSearchのフォークです。両者の互換性は高いのですが、過去3年間で小さな違いが蓄積され、移行が予想以上に困難になっていました。

大規模な検索

Figmaの検索チームに加わった当初の私は、ElasticSearchやOpenSearchの実務経験はほとんどないものの、大規模なWebサービスのスケーリングに関しては豊富な経験がありました。そこで、いつもWebサービスの問題に取り組むときと同じように、OpenSearchの内部で何が起こっているのかをまず理解しようとしたのです。

理想を言えば、OpenSearchが報告する検索時間と当社のアプリケーションが報告する時間がほぼ同じになるのが望ましいのですが、当時は120倍の違いがありました。当社の検索サービスは*それほど*複雑ではないのに、どこに時間が使われているのでしょう。

DataDogのネイティブOpenSearch統合から、当社の「平均的な検索」には約8ミリ秒(ms)かかることが分かりました。データが何百ものインデックスシャードに分割され、並行して検索可能であったとしても、数テラバイトのデータを検索するには驚くほど(そしてありそうもないほど)高速であるように思えました。また、当社サービスの検索APIの99パーセンタイルのレイテンシがほぼ1秒であることも分かっていましたが、何かが腑に落ちませんでした。

さらに調査を進めるため、コードにインストルメンテーションを追加し、どこに時間が費やされているかを確認できるようにして、検索を支える内部コードの大きなブロックのほとんどに指標とトレースを追加しました。これにより、いくつかの重要なインサイトを得ることができました。

  1. OpenSearchは平均レイテンシが8ミリ秒と主張していましたが、当社APIライブラリへの呼び出しでは平均レイテンシが150ミリ秒、99パーセンタイルのレイテンシが200〜400ミリ秒でした。当社の最低レイテンシは40ミリ秒を超えており、OpenSearchが報告していた最大レイテンシをはるかに上回っていました。
  2. OpenSearchにクエリを送信する前に、クエリの作成に多くの時間を費やしていました。
  3. クエリの結果が返ってきた後、いわゆる後処理で、権限のないユーザーにファイルの結果を返さないようにするために、さらに多くの時間をかけて権限の確認を行っていました。
  4. パフォーマンスに安定性を欠いており、時間や日ごとに大きく変動していました。ピーク時には、週末に比べて数百ミリ秒遅くなっていたくらいです。

この時点で、矛盾する2つの情報を調整しようとしていました。OpenSearchが約8ミリ秒と主張していた検索時間が、当社のコードでは約150ミリ秒かかることになっていたのです。この2つが地球の反対側にあるのなら納得できる話ですが、実際には、同じAWSのアベイラビリティーゾーンで稼働しており、わずか数ミリ秒しか離れていません。そこで、ドキュメントを徹底的に調査した結果、私たちが注目していた8ミリ秒の指標は、実際にはクエリ全体にかかる時間ではなく、単一シャードのクエリにかかる平均時間であることが判明しました。

FigmaでのOpenSearch使用方法

ここに至るまでの経緯を理解するには、OpenSearch(およびElasticSearch)の動作原理と、Figmaでの活用方法を理解することが役立ちます。Figmaには、ほとんどのWebサービスと同様、ユーザーのアクションとさまざまなバックエンドサービスをつなぐAPIレイヤーがあります。OpenSearchはクエリを受信すると、「コーディネーターノード」にアクセスし、クエリのコピーを「ワーカーノード」に送信します。このとき、クエリ対象のインデックスの各シャードに対して1つのクエリが送信されます。通常、すべてのノードが交代でコーディネーターの役割を担います。これをOpenSearch検索の「クエリ」フェーズと呼びます。その後、コーディネーターは結果を収集し、並べ替えます。このとき、通常は最も良い結果を返したシャードに追加情報を求めます。これをOpenSearch検索の「フェッチ」フェーズと呼び、最後に結果をクライアントに返します。

8ミリ秒という時間は、コーディネーターノードとワーカーノード間の個々のシャードごとのクエリのみを対象としたものでした。初期設定では、ユーザーのクエリごとにシャードあたり最大500件のクエリが発生する可能性があり、その多くが並行して発生しますが、すべてではありません。矛盾が生じたのはこの部分です。私たちは誤った指標を見ていました。これまで測定していたものに代わるレイテンシー指標が必要でした。コーディネーターノードから見たクエリを対象とするものです。ドキュメントを確認し、GoogleやStack Overflowで徹底的に調査した私たちは、AWSのアカウントマネージャーに支援を求めました。その結果、OpenSearchの指標やログは、クエリ全体の時間ではなく、実際はシャードごとの時間のみを記録していることが分かりました。

実際、OpenSearchが全体的なクエリパフォーマンスを報告する唯一の場所は、クエリAPIの応答です。この応答には、OpenSearchがクエリに応答するのにかかったミリ秒数を示すフィールド(「took」)が含まれています。そこで、各検索応答からこの値を解析し、監視システムに追加しました。これにより、OpenSearch APIコールの周囲に配置したタイミングラッパーとほぼ一致するバックエンドのレイテンシー数値が得られました。いくつかのデータを比較した結果、当社の監視システムが伝えている内容をより明確に把握することができたのです。

残念ながら、当社の調査によると、OpenSearchでの待機に費やされる時間はクエリAPIにかかった合計時間の30%未満で、検索よりも前処理と後処理に多くの時間がかかっていたことが分かりました。ユーザーがアクセス可能なファイルに関するたくさんの情報を収集し、アクセスできないファイルをほぼ除外するOpenSearchフィルター条件を作成するのが前処理で、実際に返されたすべてのファイルに対してユーザーが実際にアクセス権を持っていることを確認するのが後処理です。

どちらの処理にも時間がかかっていましたが、後処理が特に遅かったため、権限チームと協力して後処理のパフォーマンスを把握し、改善しました。統計分析の結果、権限システムの異なる部分を異なる順序で評価することで、より迅速に同じ結果を得られる可能性があることが分かりました。また、権限システム内でのRuby実行時型安全性チェックに驚くほどの時間を費やしていたことも分かり、最も煩わしい部分を無効にすることで、大幅なスピードアップが可能になりました。

時間のかかる検索トレースについて調査し始めた際、異常に遅いデータベースクエリが多数あることに気づきました。データベース自体は高速で、当社とDBの間の負荷分散プロキシも高速でしたが、実際にクエリを発行するのに数十ミリ秒かかることがあったのです。ソースコードとトレースを数時間にわたり確認した後、新しいスレッドで新しいデータベース接続を設定する方法に問題があることを、チームメンバーが特定しました。当社のデータベース接続プールが十分に大きくなかったため、各スレッド内で不要な高コストのセットアップとティアダウン操作が行われていたのです。基本的な問題を修正すると、検索だけでなくFigma全体で大幅な速度向上が見られました。このインサイトを念頭に置き、私たちは過去に行ったスレッドに関する実験を再評価しました。すると、スレッドでデータベースを並列に読み取ることが、パフォーマンスの向上にほとんどつながっていませんでした。新しい初期化コードにより、クエリを並列で発行するほぼすべての機会で、Figmaの速度が向上しました。

パフォーマンスの評価と向上

妥当なパフォーマンス指標に基づいて足並みを揃え、大きな問題をいくつか改善した私たちは、さらに掘り下げて調査することにしました。以前は、重要な質問に答えるためのシグナルが足りませんでしたが、ここでは、以下のデータが私たちを導いてくれました。

  • 当社のOpenSearchのクエリはどのくらい優れているか
  • インデックスに正しいデータがあるか
  • OpenSearchの設定は正しいか

当社のOpenSearchのクエリはどのくらい優れているか

少し手間をかければ、OpenSearchのクエリプロファイラーが作業負荷と時間がかかる部分を教えてくれます。ここで私たちは、クエリの大半がインデックスシャードごとにある数百万のドキュメントのほとんどをスキップしていることを発見しました。クエリのシャードあたり数百のドキュメントしか参照していなかったのです。これは、前処理段階で構築したフィルターによって、ユーザーがアクセスできないファイルの大部分を排除しており、OpenSearchのクエリ最適化機能がこれをフルに活用しているためです。つまり、クエリに問題はありませんでした。

正しいデータがあるか

この問題は、パフォーマンスだけでなく、少なくとも部分で気には検索の関連性に関わる問題であるため、まだ解決できていません。ユーザーが探しているものを見つけるのに十分なデータがありますか。実際にそれを見つけることができますか。これは単純な問題ではなく、当社でも常に改善を試みています。ですが、OpenSearchに投入していたデータの大部分があまり役立たないことが分かりました。当社では、インデックスサイズの縮小を繰り返し行い、最初に50%、その後さらに90%縮小しましたが、関連性に目に見える影響はありませんでした。これにより、すべてがより速く、より手軽になり、コストも抑えられました。

OpenSearchの設定は正しいか

OpenSearchは柔軟性が非常に高いですが、複雑性も伴います。たとえば、AmazonはCPUおよびメモリのオプションが豊富で、価格帯も1時間あたり0.02~17ドルまでと幅広い、139種類の異なる OpenSearchサーバーインスタンスタイプを揃えています。各インデックスを複数のシャードに分割する必要がありますが、どの状況にも適したシャード数を決定するのは容易ではありません。また、OpenSearchはCPU、メモリ、ディスクの使用率の間でトレードオフを可能にする圧縮タイプや検索の同時実行性といった多くの機能をサポートしています。これらはすべて調整可能で、どれをとっても明確な「最適」値はありません。Amazonでは、OpenSearchノードごとのデータ量とノードに対するシャードの比率に重点を置いたサイジングガイドを提供していますが、その推奨事項のほとんどは、遅延に敏感なドキュメント検索ではなく、ログクエリのようなスループット重視のワークロードに基づいていることが判明しました。

つまるところ、これらのオプションが検索パフォーマンスにどのように影響するかを把握する唯一の方法は、テストを実施してその影響を測定することでした。そこで私たちは、負荷テストシステムの構築を開始しました。非本番環境のOpenSearchクラスターを新たに構築し、データをロードします。その後、クエリを実行し、結果を測定して変更を加え、再度テストを行いました。OpenSearchには、opensearch-benchmarkというわかりやすい名前を持つ独自のベンチマークツールがありますが、このツールから一貫した結果を得ることはできませんでした。このツールは、OpenSearchの開発におけるパフォーマンス回帰テストを行うために設計されていますが、既存のOpenSearchインスタンスに大量のランダム化されたクエリの送信にはあまり適していません。また、なぜかサーバー側の「took」レイテンシー数値を使用することを好まないため、すべてのレイテンシー指標がクライアント側のパフォーマンスに基づいており、当社の環境で繰り返し実行することが難しくなります。結局、私たちは午後にGoで独自のベンチマークツールを書くことになりました。

このことで、いくつかの重要なインサイトが得られました。

  • インデックスにシャードが多すぎました。最初450個あったシャードが180個まで減少し、60%の削減となりました。これにより、超過レイテンシーが発生する前の最大クエリレートが50%以上向上しました。また、コーディネーターがデータを収集するシャードの数を減らすことで、P50レイテンシー(レイテンシーの中央値)も減少したことには驚きました。
  • インデックス内のデータ量を減らすことで大幅な改善が見られました。これこそ、私たちがシャード数を安全に削減できると確信した理由の1つです。データ量を減らすことで、ディスクキャッシュのヒット率が向上し、全体のパフォーマンスを予測しやすくなりました。インデックスサイズを50%削減する最初の最適化により、クエリのレイテンシーが減少し、一貫性が向上しました。また、未使用データを取り除くことでさらに90%削減でき、データセット全体がオペレーティングシステムのディスクキャッシュに収まったことで、システムが大幅に高速化されました。
  • Amazonの推奨サイズが当社の使用状況に適していませんでした。AWSの推奨サイズを検証する中で、当社の測定結果と推奨内容が大きく異なることが判明しました。Amazonは、シャードを50GB未満に保ち、クラスター内の1.5CPUごとに約1つのインデックスシャードを設定することを提案していました。これは、ログ型の検索では理にかなっているかもしれませんが、レイテンシーが重要なドキュメント型の検索では、コーディネーターが各クエリで数千のシャードを管理するために過剰な労力を費やすことになります。当社のフィルターは非常に効果が高かったため、実際にはシャードの数を減らして大きくすることで、パフォーマンスが向上しました。
  • OpenSearchノードにCPUが多すぎ(高価)、RAMが足りない(はるかに安い)状態でプロビジョニングしていました。インデックスのサイズを小さくする前でも、ノードあたりの価格が約半分で、CPUが3分の1、RAMが25%多いノードに切り替えることができ、全体的なパフォーマンスがわずかに向上しました。
  • Zstandard(zstd)圧縮による大きな成果は見られなかったものの、悪影響もなかった。
  • 当社でのユースケースでは、同時セグメント検索はあまり効果がありませんでした。最初は低いクエリレートで数ミリ秒のレイテンシーが追加され、クエリ負荷が増加するにつれてレイテンシーはさらに急速に増加しました。まだ多くの空きCPUがあったため、並列度を増やすことでレイテンシーが減少すると予想したのですが、そうではなかったので驚きました。

結果として、Figmaの検索チームはAPIのレイテンシーを約60%削減し、1秒あたりの最大クエリ数を少なくとも50%増加させ、総コストを50%以上削減することができました。これは、モニタリングやバグの修正からインデックスサイズの縮小、構成の最適化に至るまで、複数の領域にわたる細かい作業を通じて達成されたものです。一発で効く特効薬はありませんでしたが、私たちの連携により、Figmaの検索性能が大幅に向上し、将来の拡張性が確保されました。スピードの探求が終わることは決してありませんが、私たちは大きく成長し、どんな課題が来ても対応する準備が整っています。

Figmaを使った制作とコラボレーション

無料で始める