この記事は、合併前の旧ブログに掲載していた記事(初出:2023年9月5日)を、現在のブログへ移管したものです 。現時点の情報に合わせ、表記やリンクの調整を行っています。
Overview
30万を超える同時接続数を持つチャットサービスにおいて、リアルタイムでメッセージの受信などのイベントを配信するメッセージブローカーとして、私たちはRedis ClusterのPub/Subを使用していました。
私たちのサービスでは、ユーザー数の増加に伴い、Redis Clusterのシャード数を増やすことでクラスターの性能を向上させてきました。しかし、Redis ClusterのPub/Subでは、シャード数の増加に伴ってネットワーク帯域が圧迫される問題が生じ、これ以上シャードを追加することができない状況になりました。
この課題を解決するために、メッセージブローカーをRedis Pub/SubからRedis Streamsに切り替え、スケールアウトによる性能向上が可能となるように改善しました。
サービスについて
LINE公式アカウント(以下、OAと呼びます)は、企業や店舗経営者がLINEを通じてお客様とつながるためのサービスです。
LINE公式アカウントには、OAオーナーがLINEユーザーと直接チャットで対応できる「チャット」機能(以下、OAチャットと呼びます)が備わっています。今回は、このOAチャットについて説明します。
従来のアーキテクチャとその問題点
OAチャットでは、メッセージの受信などのイベントをリアルタイムでOAオーナーに通知する仕組みが必要となります。
このリアルタイム配信の裏側で利用していたのがRedis ClusterのPub/Subでしたが、このRedis ClusterのPub/Subはシャード数が増えるとネットワーク帯域を圧迫する問題があり、 スケールアウトによる性能向上が難しい状況にありました。
以下では、アーキテクチャとRedis ClusterのPub/Subの問題点についてまとめていきます。
アーキテクチャ
OAチャットにおいて、LINEユーザーから送られたメッセージをOAオーナーに対して継続的にリアルタイムで配信する従来のアーキテクチャについて説明します。
リアルタイムにメッセージを配信する仕組みは次のようになっています。

まず、OAオーナーがストリーミングサーバーに対して接続します(図の1)。
このとき、ここまでのメッセージを受信しているというパラメータを含めることで、それ以降に配信された過去のメッセージを受信することができます。特にクライアントがスマートフォンなどのモバイル回線の場合に一時的にネットワークが切断されることがよくあり、再接続時に欠損なくメッセージが取得できるように必要な仕組みです。
ストリーミングサーバーはそのパラメータをもとに過去配信されたイベントが保存されているRedisからそれ以降のものを取得し、OAオーナーに対して通知します(図の2)。
次に、それ以降配信されるメッセージを受信するために、メッセージブローカーであるPub/Subで適切なチャンネルをsubscribeします(図の3)。
LINEユーザーがOAにメッセージを送信すると、LINEプラットフォームを経由しOAチャットに関するイベントを処理するサーバーに到達します(図のA, B)。このサーバーでは、先述したクライアントの再接続時に過去メッセージも取得できるようにするためイベントをRedis Listsに追加し(図のC)、即時配信のためにPub/Subにpublishします(図のD)。
その後、Pub/SubチャンネルをsubscribeしていたOAオーナーに対して、ストリーミングサーバーを経由してメッセージが通知されます(図のE, F)。
Redis ClusterにおけるPub/Subの問題点
Redis Clusterの場合、クラスターを構成している任意のシャードで任意のチャンネルをpublish、subscribeできます。これは、ある一つのシャードに対してpublishしたメッセージがクラスター内のすべてのシャードに対して伝搬されるためです。
下の図はこれを示したものであり、shard1にpublishしたmessageAはクラスターを構成する残りのシャードであるshard2、3に対して伝搬されます。同様に、shard2にpublishしたmessageBも残りのシャードであるshard1、3に伝搬されます。
これにより、messageA、Bとも直接publishされていないshard3でもmessageA、Bを受信することができます。

ここからわかるように、publishしたメッセージは残りすべてのシャードに対して伝送されるため、シャード数が多ければ多いほどクラスター内での伝送が多くなり、ネットワーク帯域を圧迫します。
このように、Redis ClusterのPub/Subにはネットワーク帯域がボトルネックとなり、スケールアウトが難しくなる問題があります。
私たちのサービスでは、24シャード、48ノード構成のRedis ClusterでPub/Subによるメッセージ配信を行っていましたが、ノードあたり平常時で500Mbps、ピーク時で1.5Gbpsの帯域を消費している状態でした。
ユーザー数の増加に伴いRedis Clusterのシャード数を増やすことでクラスターの性能を向上させてきましたが、ネットワーク帯域の限界が近く、これ以上スケールアウトすることが困難な状況でした。

クラスタレベルでの水平シャーディング
上で説明したRedis ClusterのPub/Subにおいてシャード数が多くなるに連れてネットワーク帯域を圧迫してしまう問題に対して、まずは一時的な対応として、クラスターレベルでの水平シャーディングを行うことでアウトバウンドトラフィックを抑えました。
ちなみに、水平シャーディングを行う場合、Redis Clusterの代わりにSentinel構成にすることも可能でした。
しかし、subscriberの数が多いためSentinel構成では接続数がボトルネックになりやすい点と、私たちのチームはSentinelの運用経験がなく管理が難しい点が挙げられたため、Redis Clusterを利用して水平シャーディングを行いました。
以前の構成では、24シャード、48ノードで構成される1つのRedis Clusterで運用していました。この場合、図のように、あるシャードにpublishされたメッセージは残りの他23シャードに伝搬されます。つまり、アウトバウンドトラフィックがインバウンドの23倍となっていました。

そこで、この1つのクラスターを図のように8つのクラスターに分割し、クラスターレベルで水平シャーディングを行います。
この場合、1クラスターあたり3シャードとなるので、アウトバウンドトラフィックがインバウンドの2倍となり、以前の23倍と比べると大幅に削減できていることがわかります。
つまり、全体としてのRedisノード数を変えることなく、クラスターを8分割しシャーディングすることでトラフィックを約1/8に減らすことができました。

ただし、このようなクラスターシャーディングを行っている場合、スケールアウトの方法としては、クラスター数を増やすか、もしくは、クラスター内のシャード数を増やす、という2択がありますが、
- クラスター数を増やす: Pub/Subチャンネルのシャードへの振り分けロジックは自前で実装する必要があり、無停止で安全に増やすのには手間とリスクが伴う
- クラスター内のシャードを増やす: アウトバウンドトラフィックが増える問題は依然として存在する
といったように、依然としてスケールアウトに対する障壁が大きいです。
この課題に加え、新しい技術に挑戦してみたいという点からも、Redis Pub/SubからRedis Streamsへの移行を行うことで、これらの問題を根本的に解決することを決めました。
Redis Streams
Redis Streamsとは?
Redis Streamsは、Redis 5から導入されたデータタイプで、主に時系列 データを順に追加していくことに特化した形式です。
保存されたデータはインクリメンタルなIDを持ち、そのIDをもとに1要素あたりO(1)で取得することができます。また、IDの範囲を指定してその範囲のデータを取得することも可能であり、通常はタイムスタンプがIDとして用いられるため、指定した期間のデータを効率的に取得することができます。
また、次のデータが追加されるまで待機することもできるため、リアルタイムなメッセージブローカーとしても利用できます。
Redis Pub/Sub vs Redis Streams
私たちのアプリケーションにおいて比較すべき点は大きく分けて3つあります。
- データの配信(持続時間)
- ネットワーク帯域
- 接続数
特に、3の接続数は、OAチャットにおいてはユーザ数が多くなるにつれて、非常に大きな問題となってきます。我々の目安としては、20,000 接続/nodeを上限と設定しており、ユーザ数が増えていくにつれてこの接続数の問題に直面します。
以下の表には、Redis Pub/SubとRedis Streamsの比較をまとめました。以降でそれぞれの項目について詳しく説明します。
| Redis Pub/Sub | Redis Streams | |
|---|---|---|
| データの保持期間 | 配信したらすぐに消える | 配信と同時に保存もするので、あとから参照できる |
| ネットワーク帯域 |
シャード数が増えるとその分クラスター内のトラフィックが増加 クラスター内の 帯域がボトルネックとなりスケールアウトが難しい |
シャード数が増えてもクラスター内のトラフィックに変化なし クラスター内の帯域によるスケールアウトの障壁はない |
| 接続数 |
複数のチャンネルをsubscribeするケースでも1 接続で良い = 任意のシャードで任意の複数のチャンネルをsubscribeできる |
N個のストリームをコンシュームするケースではN 接続必要になる = あるストリームをコンシュームできるのはそのストリームが割り当てられたシャードのみ |
なお、Redis ClusterのPub/Subにおいてシャード数が増加することによりネットワーク帯域を圧迫してしまう問題に対しては、Redis 7からSharded Pub/Subが導入されています。
通常のPub/Subでは、publishしたメッセージがクラスタ内のすべてのシャードに対してブロードキャストされますが、Sharded Pub/Subでは他の一般的な型と同じようにチャンネル(キー)が特定のシャードにのみ割り当てられます。
このため、Sharded Pub/Subでは他のシャードへのブロードキャストがなくなり、ネットワーク帯域を節約できます。一方で、publishしたシャードでのみsubscribeできるという制限も加わります。
検討時点で私達のインフラ環境ではRedis 7がサポートされていなかったことと、後述する過去に配信したデータも参照できる点においてStreamsが優れていたため、OAチャットではRedis Streamsの利用を選択しました。
1.データの保持期間
Redis Pub/Subでは、配信するメッセージは保存されず、配信が終わればRedis上からはそのメッセージは消えてしまいます。
一方、Redis Streamsではデータを保存できるため、過去に配信したメッセージについても取得することが可能です。
私たちのアーキテクチャでは、クライアントの再接続に備えて過去に配信したメッセージも取得できる必要があります。
従来のアーキテクチャでは、Redis Pub/Subで配信したメッセージは過去に遡って取得することができないため、別の場所(従来のアーキテクチャではRedis Lists)に保存しておく必要がありました。一方、Redis Streamsでは即時配信と過去分の保存の両方が可能であり、よりシンプルな構成にすることができます。
2. ネットワーク帯域
Redis Clusterにおいて、Pub/Subではあるシャードに対して発行したメッセージが他のすべてのシャードに対して伝搬されることを説明しました。
一方、Redis Streamsについては、Redisの一般的な型と同様にキーが特定のシャードに割り当てられるため、読み書きはそのシャードのみで行えます。
つまり、図に示したように、messageAをshard1に書き込んだ場合それを読み込むことができるのはshard1のみであり、Pub/Subのように他のノードで受信することはできません。

このように、異なるシャード間でのメッセージ伝搬が行われないため、Pub/Subのようにシャード数が多くなるほどネットワーク帯域を圧迫してしまう問題が起きません。