はじめに
この記事は commmune Advent Calendar 2023 の7日目です。
結論
- イベント駆動型にして処理を非同期化、結果を受け取りたかった
- WebSocketだとうまくいかなかった
- 今回はポーリングを採用した
背景
コミュニティプラットフォームを開発しています。
アクティブユーザ数の増加に伴い、技術的な問題が顕在化してきました。
サービス
誤解を恐れずに超ざっくり説明すると、私達のサービス(の一機能)はSNSのようなものです。
そこにはユーザが作成する「投稿」があり、他のユーザによって「いいね」のようなリアクションを行うことができます。
発生した問題
例えばあるユーザが投稿に対して「いいね」を実行すると、様々な機能が連動して実行されます。例えば
- 「いいねした」というデータを保存する
- 「いいね」を実行したことに対して「ポイント」を付与する
- 「ポイント」が一定溜まったときに「リワード」を付与する
- etc...
もともと、これらの処理が1つのAPI呼び出しの中で(しかも同一トランザクション内で)すべて実行されていました。
そのため平時であっても、この重厚なAPIの呼び出しはそこそこ時間がかかるようになってきました。
加えてリクエストが集中すると、データベースのロック待ちが発生し、さらなるパフォーマンスの劣化が顕在化しました。
アプローチ
「ポイント付与」「リワード付与」といった付随的な機能について、非同期的に処理することにしました。(いわゆるイベント駆動型アーキテクチャと呼ばれる構成です)
これによりトランザクションの時間短縮などでパフォーマンスを向上させることを検討しました。
立ちはだかるビジネス要件
付随的な機能である「リワード」が付与された際にモーダルが表示される、というのが現状の仕様でした。
この機能はどうしても落とせないとのことでした。
(ゆえにこれまでは付随機能がすべて完了するまで待っていたのです)
じゃあどうしたのか
非同期的に処理された「リワード獲得」が完了した際に、サーバサイドからクライアント(ブラウザ)に情報を通知することになりました。
本題
サーバからクライアント(ブラウザ)に情報を通知する。真っ先に考えたのがWebSocketでした。
WebSocketの問題点
WebSocket処理のためのサーバを立て、そこが「リワード獲得」のイベントを受信させ、ブラウザに通知するという構成を考えてみました。
しかしこの方法ではうまくいかないことがわかりました。
というのもWebSocketサーバはスケールアウトする必要があり、複数台存在します。ブラウザはそのなかの1台に接続することになります。イベントもWebSocketサーバの中の1台のみが受信します。
すなわちブラウザの接続しているWebSocketサーバが、偶然にもイベントを受信しないと、ブラウザに通知が届きません。
すべてのWebSocketサーバがイベントを受信する、というアプローチも考えました。しかしWebSocketサーバが受信する大多数イベントは、現在接続されてないユーザのためのイベント、すなわち無駄なイベントです。
そのためサーバ負荷やコストが無駄に発生することになります。
その他検討した方法
- Redis Pub/Sub
- Redisが単一障害点になるためNG
- Firebase FCM
- 特定のベンダーにロックインされるのを嫌ってNG
結局どうしたか
古典的ではありますがポーリングを採用しました。
リワード獲得イベントによりメッセージを保存し、クライアントは定期的にAPIにて新着メッセージが無いかを確認します。
その際にタイムスタンプを利用して可能な限りRedisに情報をキャッシュ、DBへのアクセスが最小限になるようにしました。
ちなみに
ToB向けでコミュニティを開設することもありますが、企業様によってはWebSocketが封じられているケースもあります。これもWebSocketをやめた理由の一つです。
確実なのはHTTP(S)で完結するポーリングなのかもしれません。