Blitz(後編):リアルタイムユーザー解析エンジンを実現する技術 - 強整合な解析 -

Introduction

前編の記事では、Personalizationに特化したリアルタイムユーザー解析エンジン(Blitz)のコンセプトとPre-Aggregation手法を説明しました。

今回は刷新された解析エンジンのコア要素である「強整合な解析」を実現するアーキテクチャのポイントを紹介します。

強整合な解析とは何か?

プレイドの新しい解析エンジンでは、とあるユーザーのイベントが発生した際、過去も含めたイベント履歴やユーザーごとの統計値を元にフィルタリングを行うことができます。

強整合な解析とは、フィルタリングを行うユーザーの統計値が必ず最新の状態であること、つまり過去全てのイベント履歴が反映されていることを指します。

やりたいことはとてもシンプルですが、秒間最大13.4万イベントが発生する高トラフィック、かつ数百ms内の低レイテンシーが求められる状況下で、ユーザーの統計値を常に最新に保つことは技術的に難しい問題です。

これからその技術的課題とどういうアーキテクチャを元に解決したのかを話していきます。

{
  $meta: {
    name: "nashibao",
    isMember: 1
  },
  $buy: {
    items: [{
			sku: "xxx",
			price: 1000,
    }]
  }
}

[図1: イベントデータ]

match("userId-xxx",
  DAY.Current('$meta.isMember', 'last') = 1,
  ALL.Current('$buy.items.price', 'avg') >= 10000,
  WEEK.Previous('$buy.items.price', 'sum') <= 100,
  WEEK.Previous('$session', 'count') > 10,
  ...
)

[図2: ルール定義の擬似コード]

これは “userId-xxx” が “会員” であり、1年間では購買単価が1万円以上で、先週のセッション数が10以上に関わらずほとんど買い物に繋がらなかったユーザー なのかどうかを検証するルールを示した疑似コードです。

刷新前の解析エンジンのアーキテクチャとその課題

今回刷新される前の解析エンジンのアーキテクチャとその当時の課題をまずお話しします。

以前はリアルタイム解析を行うコンポーネント(Track)と非同期にでユーザーの統計値を更新するコンポーネント(Analyze)の二つで構成されていました。

Track_Architecture.png

[図3: 刷新前の解析エンジンのアーキテクチャ]

このアーキテクチャのポイントは

  • リアルタイム側(Track)では、事前に生成されたユーザーの統計値をKey-Value Storeから読み込みするのみで書き込みは行わない
  • 非同期側(Analyze)でキューに積まれたイベントをまとめて処理する

です。

高速なKey-Value Storeからの読み込みによってリアルタイム解析を1秒以内に抑えつつ、非同期側で効率良く処理し、Key-Value Storeの書き込みの負荷を抑えていました。

ただ、タイミングによっては非同期側(Analyze)で処理中のイベントがユーザーの統計値に反映されない状態が発生しえます。そのため、リアルタイム側では常にユーザーの統計値を一貫した状態に保つことが難しい状況でした(結果整合な解析)。

結果整合 → 強整合な解析にするためのアーキテクチャ

上で説明した技術的な課題を踏まえ、図4が刷新後のアーキテクチャです。既存からの変更点としては、

  • リアルタイム側のサーバーを二つに分割し、まずフロントエンドサーバーは分散キューにイベントを書き込む
  • リアルタイム側のバックエンドサーバーは、イベントを分散キューから読み込み、解析を行う

Blitz_Architecture.png

[図4: 刷新後の解析エンジンのアーキテクチャ)

この変更によって、ユーザーの統計値に未反映のイベントが存在したとしても、リアルタイム解析のバックエンドサーバー側で分散キューを読み込むことで、常にユーザーの統計値を最新の状態に保つことができるようになりました。

つまり分散キューを前段に置くことで強整合な解析を実現しました。

ただ、解析を数百ms以内に行うという解析エンジンの制約があるため、この分散キューに求められる性能要件はとても厳しいものとなります。

次にどうこの分散キューを実現したのかの話をしていきます。

高スケーラビリティかつ低レイテンシな分散キューの実現方法

この分散キューには以下の通り、厳しい性能要件があります。

  • 高スケーラビリティ
    • 高トラフィックなイベント数に対してスケールできる
    • 参考値:日中のピーク時に書き込みリクエストが30,000 ops、書き込みデータ量が300MiB/s
  • 低レイテンシー
    • 書き込み、読み込み共に10ms以内

既存のメッセージング系のサービスだと、高スケーラビリティと低レイテンシーを同時に満たすものがありませんでした。

サービス 高スケーラビリティ 低レイテンシー(10ms以内)
Redis Queue ×
Cloud PubSub
(with Ordering) ×
Kafka

そこで僕らは、低レイテンシーなKey-Value Storeと知られたCloud Bigtableを用いて分散キューを実現しました。これからその具体の方法を紹介していきます。

Cloud Bigtableは主にKey-Valueストアとして知られ、一桁ミリ秒単位の低レイテンシーかつ水平スケールすることができます。僕らが着目したのは、行キーの開始と終了を指定したレンジスキャンも同じく高速であるという点です。

具体的な分散キューのスキーマを説明すると以下のような形になります。

行キー = ${プレフィックス}_${ユーザーID}_${イベントのtimestamp}
バリュー = イベントデータ

ポイントは末尾につけたイベントのtimestampです。これによって、イベントのtimestampの開始・終了を指定してレンジスキャンすることができるようになります。

また、Cloud Bigtableのガベージコレクション機能を用いてTTLを設定することでキューとして古いデータを削除する機能を簡単に実現することができました。

まとめると、Cloud Bigtableの行キーのスキーマ設計やガベージコレクション機能を工夫することで、水平スケール可能かつ10ms以内のレイテンシーで収まる分散キューを作ることができました。

最後に

今回はリアルタイムユーザー解析エンジン(Blitz)のコアである強整合な解析を実現するためのアーキテクチャ・具体の実装方法を紹介しました。

解析エンジンの中には高いパフォーマンス(低レイテンシーや安定的な高スループット)を出すための技術的な工夫がまだまだいっぱい施されています。

特に低レイテンシーを実現するためのキャッシュ戦略について、他のエンジニアから今後話してもらう予定です。是非楽しみにしていてください。