MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

高速マッチングシステムで実践した Goチューニングテクニック - Go Conference 2023


※この文を削除し、「一覧ページでの見出し用の文章」として最大300字程度で自己紹介+概要の記入をお願いします。文内にリンクは置けますが、画像・数式・埋め込みなどのテキスト以外の要素を貼ることはできません。


Go Conference 2023

開催から時間が経ってしまいましたが、Go Conference 2023 が6/2に開催されていました。弊社では、Platinum “Go”ld Sponsor として関わらせていただきました。4/1付けで、社名を株式会社GOへと変更した後であり、Go Conference に支援させていただけたのは、感慨深いものがあります。

この時には、スポンサーセッションとして、『タクシーアプリ『GO』高速マッチングシステムで実践したGoチューニングテクニック』というタイトルでお話しさせていただきました。

ここでは、『GO』サービスの根幹を担うマッチングアルゴリズムを担う Go で実装したシステムを刷新し、その時に行われた数々の Go のチューニングについて述べ、複数のマイクロサービスと連携する時のチューニングと、数秒単位で複数のプロセスが連携して動作する時にどのようなことを考えて実装したのかをお話ししました。

本記事では、Goのチューニングテクニックを、テックブログでもダイジェストで掲載します。

なお、本記事ではタクシーアプリのサービスを”『GO』”、プログラミング言語を Go と表記します。

高速マッチングシステムとは

タクシーアプリ『GO』では「タクシーを呼ぶ」ボタンを押すと、近隣のタクシー車両を手配してユーザの元へ向かってもらいます。このどのユーザに、どの車両を割り当てるのか決めるのがマッチングシステムの役割です。

最も近いタクシーを順番に割り当てていくだけでは、マッチングが遅れてしまうユーザが発生したり、一部遠い車両への割り当てが行われたりしてしまいます。あるユーザの到着時間は少し伸びてしまうこともありますが、より多くのマッチングが成立させるように、これを全体最適を目指してマッチングさせようとしています。

An image from Notion

従来のマッチングを担うシステムは、ユーザの配車依頼をごとにサーバで処理し、一部DB経由で情報を連携することで、マッチングの効率化を目指していましたが、個別最適になりやすい状態になっていました。この状態を打破すべく、マッチングに関わるアーキテクチャーを刷新するプロジェクトが立ち上がりました。

新システムでは以下のようなアーキテクチャーに変更しました。

  • 配車依頼をDBにため込んで、「数秒間隔」x「タクシー交通圏」ごとにバッチ起動して、一括して処理する。
  • ETA、車両位置情報収集サービスなど、複数のマイクロサービスと連携する。
  • ステートを持って「バッチ駆動タイミングと、マッチングのためのデータ収集」を担うシステム(Goで実装)と、ステートレスに「マッチングアルゴリズムの実行」を担うシステム(Python実装)に分ける。
An image from Notion

An image from Notion

これにより、より全体最適を実現する高速マッチングシステムを構築することができました。

高速マッチングシステムの課題

このアーキテクチャーですが「ため込んで、複数の配車依頼をバッチ処理する」仕組みになった都合、多くの課題が見えてきました。

  1. 複数の配車依頼を一括処理するため、レイテンシが大きくなるが、マッチング完了までのレイテンシをキープできるか。
  2. 「数秒間隔」x「全国のタクシー交通圏」のタスクをどうやって割り振るか。
  3. 可用性が高くできるか。

チューニング1 レイテンシキープを並列化と非同期化で達成する

以前のアーキテクチャーでは1つの配車依頼について処理すれば良かったものから、複数の配車依頼を同時に処理すると、そのままでは扱うデータが増えた分、データの取得や処理に時間がかかるようになってしまいます。特に、多くの他のマイクロサービスからマッチングに必要なデータを集めいてくる必要があるため、その処理が

方針としては以下のようにしました。

  • なるべく複数のレコードをやりとりできるAPIへ更新し、相手先システムの責務で並列化する。
  • レコード単位でリクエストするものは、クライアント側で並列化する。

並列化の実装は、Goルーチンをリクエストの数だけ作成し、セマフォを使って最大並列数を制御するようにしました。Goルーチンは1ms以上の処理であれば、リクエスト毎に使い捨てても支障ないと、経験上考えています。また、分散させた各Goルーチンで個別に発生するエラーは、golang.org/x/sync/errgroup というパッケージを使って収集する様にしました。

An image from Notion

また、組み合わせを決めた後に、実際にタクシー上のアプリにリクエストを飛ばすなどの配車依頼ごと処理は全て新しいGoルーチンを起動して、非同期化させるようにしました。

An image from Notion

非同期化後のGoルーチンでも、エラーは集めて管理しますが、先ほど説明したerrgroupパッケージでは、複数のGoルーチンでエラーが発生しても、1つのエラーしか収集できないため、複数のエラーに対応した errgroup を自作しました。

この2つのチューニングを、処理時間が長いタスクから適用して、サービス品質を落とさないレベルまで達成しました。

チューニング2 タスクのスケジューリングと、可用性を、プロセスの外で担保する


We're Hiring!

📢
GO株式会社ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @goinc_techtalk のフォローもよろしくお願いします!