r-kaga's log

TopBlogBookmarks

Uberのプッシュプラットフォームのアーキテクチャ 「Uber’s Real-Time Push Platform」を読んだ

20 December, 2020

はじめに

以下の記事で取り上げられている、Uberのクライアント、サーバー間のリアルタイム同期の仕組みがポーリングから、最終的にgRPCベースのプッシュプラットフォームに移行した話が面白かったので個人的なメモとしてまとめてみます。
自分の理解が怪しく意訳してる所や意図的にスキップしてる箇所も多いので参考程度にしてもらると!
Uber’s Real-Time Push Platform

概要

Uberのアプリではサービスの性質上、ピックアップ時間、到着時間、画面上のルートライン、アプリを開いたときの近くのドライバーについての情報など、リアルタイムの情報で更新、同期が必要なものが数多くあります。 また一口にアプリといっても、ライダー向け、ドライバー向け、またそれらの背後にはバックエンドサービスなどがあり同期を行う必要のある対象も複数存在しています。
かつてはリアルタイムでの同期を行うために、ドライバーアプリ、ライダーアプリ共に数秒ごとにサーバーをポーリングするといった実装だったようです。

The driver app can poll the server every few seconds to check if a new offer is available. The rider app can poll the server every few seconds to check if a driver is assigned.

ただ上記のようなシンプルなポーリングだと以下のような課題を抱えてしまいます。

課題感

上記の仕組みだと必然的にユーザー数やリアルタイム同期を必要とする機能の増加に従ってポーリングコール数が増加しますが、それにより当然バックエンドへの負荷も増加するため、このアプローチはスケーラブルとは言えません。
実際にUberでは、ある時点でバックエンドAPIゲートウェイへのリクエストの80%がポーリングコールとなってしまったようで、この辺りが大きな課題感の一つだったようです。

At some point, 80% of requests made to the backend API gateway were polling calls.

またアプリからは、バッテリーの消耗が早くなる、ネットワークが不安定な場所ではポーリングリクエストを何度もリトライしてしまうなどの課題も存在していたようです。
機能の数が増えるにつれ、ピーク時には、何十ものAPIをポーリングしており、UIをレンダリングするための複数の並行APIコールが発生し、重要なコンポーネントがサーバーから取得されるまでアプリはレンダリングできないなどの課題も抱えていました。

これらの課題の解決のため、Uber社ではサーバーからアプリにデータを送信する機能を備えたプッシュメッセージングプラットフォームの構築を行いました。

アーキテクチャ

設計原則

まずは設計原則についてです。
Uberが定義したプッシュメッセージングプラットフォームの主な設計原則は以下の4つで、RAMEN (Realtime Asynchronous MEssaging Network)と命名されています。

  • Easier migration from polling to push

    • 既にポーリングエンドポイントが多数存在した
    • 既存のポーリングAPIのビジネスロジックも活用可能に
  • Ease of development

    • 今までポーリングAPIを開発していた開発者も戸惑う事なく実装する事可能に
  • Reliability

    • 全てのメッセージがネットワーク上で確実に送信され、配信に失敗した場合は再送される事を保証する
  • Wire efficiency

    • 1日に何時間もUberアプリを使っているドライバーを筆頭に、データ使用量のコストがユーザーの課題となっていた
    • サーバーとモバイルアプリ間のデータ転送量を最小限に抑える必要があった

技術スタック

いくつかの変遷を経ているようですが、記事内では、2017年のシステム刷新以降の主な技術スタックとして以下の技術の利用が示されています。

  • Netty

    • プロトコルサーバやクライアントなどのネットワークアプリケーションを素早く簡単に開発できるNIOクライアントサーバフレームワーク
  • Apache Zookeeper

    • 大規模分散システムでよく利用される、同期, 設定管理, グルーピング, 名前管理などのサービスを提供するソフトウェア
  • Apache Helix

    • ZooKeeperの上で動作するクラスタ管理フレームワーク
  • Redis

    • 永続化可能なインメモリデータベース
    • Thundering Herd問題を回避するために、Cassandraに格納されたデータのキャッシュとして使用
  • Apache Cassandra

    • 分散データベース管理システム

またアプリ-サーバー間の配信自体はServer-Sent eventsを使っています。
Server-Sent eventsでは、データはサーバーからアプリにしか送信できないため、at Least Onceの保証のためにアプリケーションレイヤーの上にリトライや確認応答などの処理が実装されています。

全体のシステムアーキテクチャは以下の通りです。
Figure 3: Architecture for the new RAMEN backend server. From Uber Engineering Blog

  • Fireball

    • "いつメッセージをプッシュするか?"の判断を担うマイクロサービス
    • 判断の大部分は設定として保持されており、システム全体で起こっている様々なイベントを元に、ユーザーにプッシュが必要かどうかを決定
  • Streamgate

    • Netty上でRAMENプロトコルを実装し、接続、メッセージ、ストレージの処理に関連するすべてのロジックを持つ
  • StreamgateFE (Streamgate Front End)

    • Apache Helix Spectatorとして動作し、ZooKeeper からのトポロジの変更を受け付ける
    • リバースプロキシを実装
    • クライアント (Fireball、ゲートウェイ、モバイルアプリ) からのリクエストは、トポロジー情報を使ってシャードされ、正しいStreamgateワーカーにルーティングされる
  • Helixコントローラ

    • Apache Helix Controllerプロセスの実行のみを担当する5ノードのスタンドアロンサービス
    • Streamgatノードが起動,停止するたびに、変更を検出し、シャーディング・パーティションを再割り当てする

正直ほとんど知見のない技術スタックのため、理解できていない箇所も多いですが、Uber社では上記のようなアーキテクチャでこのような運用実績を残しているようです。

We have been operating with this architecture for the last few years and were able to achieve 99.99% server-side reliability of the infrastructure. The adoption of this push infrastructure continues to grow, supporting more than ten different types of applications on iOS, Android and Web platforms. We have operated this system with more than 1.5M concurrent connections and pushes over 250,000 messages per second.

reliabilityが99.99%だの、iOS、Android、Webプラットフォームで10種類以上のアプリケーションをサポートし、150万件以上の同時接続で運用してるだの、毎秒25万件以上のメッセージをプッシュしてるだのと書かれてます。

また上記のアーキテクチャは2017年当時のもので、2019年からは以下のような課題を解決するため、次世代のRAMENプロトコルとしてgRPCの採用をしています。

  • Loss of acknowledgements

    • RAMENプロトコルは、データ転送を減らすために最適化されており、確認応答は30秒ごと、またはクライアントが再接続したときにしか報告されない
    • 確認応答が遅延した事により配信の確認に失敗することがある
    • 本当の意味でのメッセージロストと確認応答の失敗を区別することが困難
  • Poor connection stability

    • 異なるプラットフォーム間のクライアントでは、エラー、タイムアウト、バックオフ、アプリのライフサイクルイベント(オープンまたはクローズ)、ネットワーク状態の変化、ホスト名、データセンターのフェイルオーバーの処理に多くの微妙な違いがある結果、バージョン間でパフォーマンスにばらつきが生じていた
  • Transport limitations

    • Server-Sent eventsで実装されているため、データ転送は一方向性で双方向のメッセージ転送に未対応
    • リアルタイムでネットワークの状態を把握出来ない
    • Server-Sent eventsはテキストベースのプロトコルであるため、base64のようなテキストエンコーディングなしでバイナリペイロードを転送する事が難しく、ペイロードのサイズが肥大化

gRPCベースのRAMENプロトコルは、従来のServer-Sent eventsベースのプロトコルと比べて以下のような違いがあります。

  • 確認応答は双方向ストリームで瞬時に送信されるようになり、データ転送量が若干増加した代わりに、確認応答の信頼性が向上した
  • リアルタイムの確認応答により、RTTを測定し、リアルタイムでネットワークの状態を把握できるようになった。メッセージロストとネットワークロストを区別可能になった
  • ストリーム多重化などの機能をサポートするために、プロトコルの上に抽象化レイヤーを提供。データ使用量や通信の遅延の面でより効率的にするために、アプリケーションレベルのネットワーク優先順位付けやフロー制御アルゴリズムを試す事が可能
  • gRPC自体が複数の言語でのクライアント実装が提供されている事により、さまざまなタイプのアプリやデバイスを迅速にサポートすることが可能

ちなみに、2017以前、第一世代のRAMENサーバーは、Uber社内のハッシュ/シャーディングフレームワークを使用してNode.jsで書かれており、全ての接続はUUIDでシャーディングされ、永続化データストアとしてRedisを使用する実装だったようです。
これらの実装はスケーリング、メッセージの損失、タイムアウト、エラーなどの問題を抱えていたようですが、詳しくは記事を参照してください。

おわりに

記事内ではこのプラットフォームがUber社内で成功を納めている理由について以下のように触れらています。

  • Separation of concerns

    • メッセージのトリガー、作成、配信システムの間で責任を明確に分離したことで、ビジネスのニーズの変化に応じて、プラットフォームの異なる部分にフォーカスを移すことができた
  • Industry standard technologies

    • 業界標準の技術を使い構築することで、実装がはるかに強固で、費用対効果の高いものになった
  • Simpler design

    • プロトコルがシンプルなので、簡単にスケール可能

このような高いスケーラビリティが求められる分散システムを構築する際などは参考になるかもと思い、せっかくなのでメモとしてまとめてみました。
今回の内容以外にも、Push Messageの優先度や、Time to live、またあまり触れませんでしたがgRPCの前の実装であるServer-Sent eventsを使った実装についても記事内で取り上げられていますので、興味が湧いたらぜひブログ記事を読んでみてください。

© 2021, Built with Gatsby