1
/
5

複雑なロジックと速度のトレードオフに立ち向かうための戦略と実践

Photo by Greg Trowman on Unsplash

はじめまして。Wantedly Visitの検索基盤チームの一條です。普段は検索の改善に取り組んでいます。

Wantedlyではユーザーと企業のマッチングをより良いものにするために募集検索やスカウトでのユーザーの検索を日々改善しています。

今回はWantedlyの募集やスカウトでの検索の速度改善の取り組みについて書きたいと思います。

背景

そもそも検索の速度はユーザーに取って重要な指標の一つです。例えば、2009年の記事ですが、Google検索では意図的に100ms~400msほど遅くしたところ、ユーザーあたりの検索数0.2%~0.6%ほど減る、という実験結果もあります。

実際、過去に社内のサービスも速度を改善することでプロダクトの指標に10%以上の変化が見られたこともあります。

また、検索に関しては検索者の体験を改善するために機械学習などを用いた複雑なロジックによるソートも必要になってきます。このあたりは設計や複雑さ次第で速度面にかなりの影響を与えてきます。

速度目標

検索の速度はロジックとのトレードオフで遅くなっていきます。その際、速度を犠牲にするという判断が積み重なり、じわじわ遅くなっていく、ということがあります。
これは速度目標を置き、定期的にモニタリングや、アラートを設定することで解決できます。実際検索部分の速度、つまり検索条件を与えられてからその要素のIDの一覧を取得する部分に関してはチーム内で平均200ms以下を目標においています。これはこれ以上早くするリソースを使うくらいなら、他の箇所の高速化にリソースを割いたほうがインパクトが大きいという感覚によるものです。

全体の戦略

これらの速度目標を達成するための戦略として大きく次の3つのことを取り組んでいます。

  1. 影響範囲を小さくする
  2. キャッシュ戦略
  3. 言語/ミドルウェアのチューニング

それぞれ解説していきます。

影響範囲を小さくする
自分的にはこれが一番重要だと思っています。検索部分はこの2年間で2回程度大幅に設計変更を行なっています。
ただ、これによりほかのチームが影響を受けたりはほぼないようになっています。あるとしてもバグを入れ込んだ時や、ミドルウェアを新しく用意する時くらいだと思っています。
これを実現するためには、他とのインターフェースが切れていることが重要だと思っています。
そのためには、過去の2つの記事で紹介していますが、マイクロサービス化と他のチームがそのサービスを触るのであれば、直接ではなく日本語やDSLなどでできるだけ内部を隠蔽するのが重要だと思っています。

このような前提がおけることで、破壊的に設計を変えたとしても、それらに対するインターフェースやビジネス的な仕様さえ満たしていれば、影響範囲は最小限に抑えられます。
おそらく、エンジニア全員が触る可能性のあるような部分では、このような変更はできないと思っています。
仕様に関しては、検索は入力とデータの状態で全てテストができるので、そういったテストをE2Eで回すようにしています。これにより、そもそも設計や実装ミスで仕様を満たさない、という状況は起きないようにしています。

キャッシュ戦略
検索のうち共通で使われるものに関しては極力キャッシュをするという戦略にし、キャッシュのレイヤーをいくつか分割する、ということをしています。

キャッシュのレイヤーとその依存関係をスカウトを例に図にすると次のようになります。矢印の向き先が依存先で青がキャッシュ外のロジックであったりデータなどを指しています。ここで保存するのは基本的にID列です。


この図で見てもらうと疑問に思う方もいるかも知れませんが、ソートが最後にあるため、全てのレイヤーでページネーションなどはせずに、全てのID列を保持しています。
例えばスカウトでは10万人以上が検索できるため、これくらいのデータサイズになると、キャッシュに使うミドルウェアへの保存のネットワークコストが馬鹿になりません。
この対応として、キャッシュにはRedisを使い、RedisのSet/ZSetなどのシンプルな計算でソートまでをRedis上で行なっています。

このキャッシュ戦略は管理が大変になるための対応への基本的なアイディアはhttps://github.com/Altech/red_blocks と同じように抽象化しています。一応Goでの実装に関してはhttps://github.com/rerost/redblocks-go にあります。社内ではそこから更に変更を加えたものを使っています。
イメージとしては上の図をコードに落とすと次のようになります。

lister.Reorder(
lister.Intersection(
NewQueryFilterSet(esClient, query),
CompanySearchableUsers(companyID),
),
orderLister,
), nil

ただ、この戦略でも99パーセンタイルでキャッシュが切れて遅くなるなどの問題があるため、後述するGoやミドルウェア周りをチューニングする、ということに取り組んでいます。

言語/ミドルウェアのチューニング

Goやミドルウェアなどの一般的なチューニング方法に関してはISUCONで学ぶのが良いと思っているので、このあたりを触るメンバーは基本的に毎年ISUCONに参加して毎年学んで開発で実践しています。

また高速化のために、JSONでやり取りしている部分のgRPC化にも取り組んだりしています。

またElasticsearchのチューニングに関しては、


を実践するのと、チューニングを実践する上で実験がやりやすいようにmapping更新のフローなどを整備しています。一部外に出せる部分に関しては、Elasticsearch 6系にしか対応していないですがツールとして出しているので興味があればぜひ試してもらえると嬉しいです。

最後に

結局の所、影響範囲を限定していくのが重要で、その上で設計の見直しやキャッシュやチューニングをやりやすい形に整える必要があります。ただ、影響範囲の限定単体では意味がないためこれらを速度目標を元に実践していくことが必要です。
実際、かなりこの影響範囲を限定する方法は複雑なロジックと付き合う上で必須になっています。自分は定期的にサービス改善のために複雑なロジックを導入する際に悩まされていますが、その際にはこういった環境は大きな武器になります。

根本的に難しい問題、例えば計算量的にどうしても受け入れ難いロジックを入れたいときや、サービスの成長で急激に検索対象数が増加していく際などには毎回頭を悩まされています。また、実際そこまでは影響受けないだろうと読んでいた変更が思わぬところで速度を低下させてしまう、などが多々あります。
こういった問題には毎回頭を悩ませていますが、より本質的なところで悩むことができるので、PoCをいくつか作成して試したり、それでも厳しそうであればインターフェースの見直しや、スコアリングロジックに制限を入れるなどをしています。

このあたりについて、他の人たちがどう立ち向かっているか、聞けると嬉しいなと思っているので、こういった問題に悩まされている人や興味がある方はぜひ話を聞きに行きてもらえると嬉しいです

Wantedly, Inc.'s job postings
4 Likes
4 Likes

Weekly ranking

Show other rankings