コネヒト開発者ブログ

コネヒト開発者ブログ

API Gateway WebSocket API と Lambda で作る、ママリのリアルタイムチャット機能(サークル機能)を支えるサーバーレスなインフラ設計

本記事はコネヒト Advent Calendar 2025の13日目のエントリーになります。

adventar.org

こんにちは、コネヒトでインフラエンジニアをしております @sasashuuu です!

私ごとではありますが、一年前くらいに小田原へ引っ越しまして、自然に囲まれたのどかな生活を送っております。

地方の良さをしみじみと感じている今日この頃です!

はじめに

さて、本日は2025年にリリースされたママリのリアルタイムチャット、その名も「サークル機能」に関するインフラ設計に関する記事をお届けします。

※以下は最近の開発環境のキャプチャです。本機能のイメージを掴んでいただければと思います。

開発環境の UI

サークル機能の施策背景や詳細なコンセプト、アプリケーションの実装側に関する内容については、本記事では割愛させていただきますのでご了承ください。

また、メインで利用しているクラウドプラットフォームが AWS となりますので、AWS を用いた内容が中心となります。

これから AWS を用いたリアルタイム性の高いアプリケーションを作る方のご参考となれば幸いです。

アプリケーション開発の要件・想定するワークロード

複数人が参加する空間でのリアルタイムチャットを既存のアプリケーション実装していくというのがざっくりとした要件となります。悩めるママさん同士のつながりや、継続的な交流をより強く意識していくことがテーマとなっていたので、従来の掲示板形式のコミュニケーションからアップデートしていく必要がありました。

ワークロードに関しては、結論として正確に見積もるのが難しいという状況でした。そのため、PdM 主導のもと、一定の KPI(メッセージ投稿数、UU 投稿数等)を定める形とし、その指標を前提として耐えうる設計を進めていく形となりました。また、考慮すべき変数などはいくらかありつつも、開発時点のママリ登録者数約400万を最大値とし、既存のアクティブユーザー数に耐えうる基盤も視野に入れる必要がありました。

アーキテクチャ

はじめに全体のアーキテクチャをお見せします(※既存のママリのインフラに関しては一部を除き基本的に割愛しています)。

全体のインフラアーキテクチャ

今回、サークル機能のために採用した主要なマネージドサービスは以下となります。

  • API Gateway WebSocket API
  • Lambda
  • RDS Proxy
  • SNS(Simple Notification Service)

終端に API Gateway の WebSocket API(※以下、WebSocket API と称します)を置いて WebSocket 通信を行い、チャット機能そのものに関する API サーバーやデータストアには Lambda と DynamoDB を利用しています。データストアに一部ママリの既存データ(ユーザーの属性データ等)を保持している Aurora MySQL や Elasticache Redis を利用する必要があり、Aurora では特に負荷に対する懸念の対策を打ちたかったため、コネクションプールとして RDS Proxy を置いてデータベースへのコネクション数による負荷を軽減するという構成になっています。また、Push 通知には SNS を利用しています。

実際の処理を抜粋して解説すると以下のようなフローです(クライアントA、B がメッセージを送受信する例)。

接続時

  1. クライアント A、B から WebSocket API へ接続リクエスト
  2. Lambda Authorizer による認可処理(ステートフルのため認可処理はこのタイミングのみ)
  3. WebSocket API によるコネクションの確立
  4. WebSocket API への接続リクエスト成功をトリガーに Lambda が起動、コネクション確立時の処理の実行
    • DynamoDB へコネクション ID の保存等

メッセージ送受信時

  1. クライアント A から WebSocket API へメッセージ送信リクエスト(API サーバー向け)
  2. WebSocket API から Lambda へのルーティング、メッセージの処理
    • DynamoDB へメッセージの保存
    • DynamoDB からコネクション ID の取得
    • 対象のコネクション ID をもとに WebSocket API へメッセージ送信リクエスト(クライアント向け)
  3. クライアント B は WebSocket API を通じてメッセージを受信

切断時

  1. クライアント A、B によるコネクションの切断
  2. コネクション切断をトリガーに Lambda が起動、コネクション切断時の処理の実行
    • DynamoDB からコネクション ID の削除

※他にもユーザー同士のブロックに関する除外処理があるなど、もう少し細かな実装になっていますがここでは割愛しています。

少し補足をしておくと、WebSocket の接続そのものは WebSocket API を通じて、クライアントと WebSocket API 間で完結しており、具体のビジネスロジック(メッセージ送信等)は Lambda が担っているというイメージです。Lambda が接続中のクライアントに向けて REST API を WebSocket API へリクエストし、WebSocket API を経由してメッセージがクライアントに届くという仕組みになっています(ただし、接続に関するコネクション ID の管理という意味ではある種 Lambda も WebSocket の接続に関わってはいるかなと思います)。図解すると以下のようなイメージです。

WebSocket を用いた処理の補足

技術選定の背景

ここからは技術選定について解説をしていきます。データストア、WebSocket サーバー、API サーバーなどの観点から分けて解説します。

データストア

DynamoDB を採用しました。ママリで利用していた既存の Aurora MySQL に相乗りする形の案もあがっていました。社内で馴染みのある RDB を用いた方が開発速度が出せること、コンピューティングリソースの使用状況とリリースの初期フェーズで想定するワークロードを加味すると既存の RDB でも耐えられないことはないだろうという見解となっていました。しかし、書き込みに対する水平スケーリングへの課題感、他のサブシステムやプロダクトからも利用される共用の RDB であるという影響範囲の広さ、中長期で見た将来的な可用性やスケーリング、移行コストを考えるとこの時点で NoSQL をベースに作り込んでおいた方が良いだろうという結論となり、Aurora MySQL の採用は見送る結果となりました。

WebSocket サーバー

API Gateway の WebSocket API を採用しました。こちらは観点を分けて説明します。

そもそもの通信方法に関する観点

ポーリング形式で HTTP 通信を採用する案もあがっておりました(既存のママリの API である CakePHP に相乗りする形で実装するという方針)。しかし、ユーザー体験(メッセージ送受信の遅延)や将来を見据えた機能の拡張性(サーバー負荷等)を踏まえ、あらたに WebSocket 通信を採用し、最適な実行環境を選定していくという方針となりました。

どのように WebSocket 通信を実現するかの観点

社内に WebSocket を用いたプロダクト開発の知見が豊富ではなかったため、なるべく WebSocket に関する関心ごとの負担を減らし、プロダクト開発へよりフォーカスできるようにすることなども加味し、WebSocket API を採用しました(もちろん WebSocket に対する技術的好奇心はメンバー間で話題にあがっており、WebSocket そのものにフォーカスした技術検証なども楽しんで取り組んでいました)。

今回のケースで WebSocket API を利用することのメリットはざっくりと以下の点です。

  • WebSocket サーバーそのものの管理が不要
    • コンピューティングリソースの消費やスケーリング、可用性観点での懸念がない(※クォータの制限等は除く)
  • WebSocket の接続管理が不要
    • 既に用意されているルート(\$connect、\$disconnect)を元に簡単に WebSocket の接続・切断のハンドリングが可能
    • ルーティング先の Lambda のビジネスロジックの実装に集中できる

余談として他にも WebSocket 接続を抽象化してくれるマネージドサービスとして、AppSync や Cloud Firestore(データストアも含めた採用案) などの案もあがっていましたが、社内での採用実績がなかった GraphQL(※AppSync は GraphQL がベースとなる)のキャッチアップコストや、Firestore のコスト面の懸念の理由により、どちらも採用には至りませんでした。

API サーバー

Lambda を採用しました。既存のママリの API サーバー(CakePHP と Fargate)を使う案もありましたが、以下の点で見送り、Lambda を採用する方針となりました。

  • 常駐型のアプリケーションよりイベント駆動のようなアーキテクチャが適している
  • コスト面
  • WebSocket API に併せたサーバーレスな構成
  • 可用性のためのスケーリング速度

ちなみにランタイムは Go が採用されました。Lambda では Go のマネージドランタイムが非推奨となっていますが、OS 専用ランタイムを用いてバイナリの実行環境を用意すれば問題なく稼働することができるため、問題なく採用することができました。

その他のサブシステム

Push 通知には SNS を利用しています。SNS を用いた Push 通知自体は既にママリでの利用実績があるため、あえて他の選択肢を取るという判断にはなりませんでした。既に運用中である現行のPush 通知基盤に相乗りする形となりました。

コスト面について

本記事では詳しく解説しておりませんが、今回採用したマネージドサービスは、今回のようなケースではコストパフォーマンスに優れていると感じます(少なくともリリース初期〜安定するまでなど)。特に WebSocket 接続の部分に関しては、利用者数がはっきりと見込めず、ワークロードが安定しないようなシチュエーションであれば WebSocket API を軸に置いたアーキテクチャから始めるという選択肢は有力かと思います。もちろんトレードオフではあると思うので、初めから多額のコストがかかることが分かっている、開発工数や運用にそれなりのコストをかけられるという場合は WebSocket に特化したマネージドサービス以外の選択肢もあるかなと思います。また、データストアで採用した DynamoDB に関しては、オンデマンドとプロビジョニングといった料金体系の使い分けもありますので、まずはオンデマンドから始め、傾向が見えてきたらプロビジョニングへ切り替えるといった形で段階を踏むことでコスト対策を打っていけるかと思います。

おまけ

Distributed Load Testing を用いた負荷試験も行いました。本記事では詳細や使い方などは割愛させていただきますが、過去に弊社のテックブログでも取り上げられておりますので、詳細や使い方等気になる方はそちらもご覧ください。

tech.connehito.com

どの程度耐えうるのか限界性能試験を行いたかったのですが、API Gateway のクォータ制限に引っかかってしまい、ベストエフォートでの実施となりました。可能な範囲で行えた試験(クォータ制限の影響が出る前のテスト)を一部ご紹介します。

ざっくりとしたテスト内容

  • Distributed Load Testing 側の各種設定
    • TASK COUNT(負荷試験リクエスト元の Fargate タスク数):5
    • CONCURRENCY(タスクごとの同時接続数):100
    • Ramp Up(CONCURRENCY に達するまでの時間):5m
    • Hold for(CONCURRENCY の持続時間):1m
  • jmx ファイルのシナリオ
    1. コネクション接続
    2. 単調なメッセージ送信リクエスト(例:「これは負荷試験のテストです」等)
    3. Ping/Pong
    4. 1000ms 秒待機
    5. メッセージ削除リクエスト
    6. コネクション切断

結果

テスト項目 結果
リクエストの総数 434,648
成功したリクエストの総数 434,635
エラーの総数 13
秒間平均リクエスト 1,210

上記の試験では、99.99%以上の成功率となり、安定した稼働となっていました。

おわりに

WebSocket API と Lambda のクォータ(同時実行数等)や、Lambda に関するコールドスタートのパフォーマンス等については状況により気にする必要がありそうですが、基本的にはサーバーレス構成の恩恵を受けているので、コンピューティングリソースや可用性、スケーリング等に関する懸念が少なく済んでいます。また、リアルタイムチャットなどの開発における実績や知見が社内になかったことも相まって、皆で協力して意見を出し合い調査を行ったり、AWS ソリューションアーキテクトの方への技術相談を行ったりと開発のプロセス自体も価値のある取り込みだったと思います!以降のアドベントカレンダーではアプリケーション側の実装に関する記事も公開予定ですのでお楽しみに!