同時接続数30万超のチャットサービスのメッセージ配信基盤をRedis Pub/SubからRedis Streamsに移行した事例
大規模チャットサービスで、30万同時接続を処理するメッセージ配信基盤をRedis Pub/SubからRedis Streamsに移行した技術事例。
キーポイント
LINE公式アカウントのチャットサービスで、同時接続数30万超のリアルタイムメッセージ配信基盤をRedis Pub/SubからRedis Streamsへ移行した実践事例
Redis Cluster Pub/Subではシャード数増加に伴いメッセージが全シャードに伝搬され、ネットワーク帯域がボトルネックとなりスケールアウトが困難だった課題
Redis Streamsへの移行により、クラスターレベルの水平シャーディングが可能となり、ネットワーク帯域消費を大幅に削減し、スケーラビリティを向上させた
24シャード・48ノード構成で平常時500Mbps、ピーク時1.5Gbpsの帯域消費を抱えていたが、移行後は帯域消費を削減し、ユーザー増加に対応可能な基盤を構築
影響分析・編集コメントを表示
影響分析
大規模リアルタイム通信サービスにおけるRedisの適切なデータ構造選択の重要性を示した実践的知見であり、同様のスケーラビリティ課題に直面する開発チームにとって貴重な参考事例となる。特にPub/SubからStreamsへの移行によるネットワーク最適化の具体的手法は、高負荷システムの設計に直接応用可能な価値がある。
編集コメント
大規模サービスにおけるRedisの実運用ノウハウが具体的な数値とともに公開されており、システムアーキテクチャ設計の参考になる良質な技術記事。特にネットワーク帯域のボトルネック解消手法は多くの開発者が直面する課題への解決策を示している。
この記事は、合併前の旧ブログに掲載していた記事(初出: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 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 のようにシャード数が多くなるほどネットワーク帯域を圧迫してしまう問題が起きません。
3. 接続数
Redis Cluster における Pub/Sub では、任意のシャードから任意の複数チャンネルを subscribe できます。つまり、複数のチャンネルを subscribe する場合でも、ある 1 つのシャードへの 1 つの接続を保持すればよいのです。
一方、Streams の場合、特定のメッセージは書き込んだシャードからしか読むことができないと先ほど説明しました。これからわかるように、N 個のストリームを consume するケースでは、最悪の場合それぞれのシャードに接続する必要があり、その結果 N 個の接続を保持する必要があります。
私たちのアプリケーションでは、1 つのクライアントあたり 2 つのチャンネル(ストリーム)を読むだけでよいので、接続数は Redis Streams の場合、Redis Pub/Sub に比べて高々 2 倍程度となります。しかし、スケールアウトによりシャードあたりの接続数を抑えることができるようになるため、これは大きな問題ではありませんでした。
Redis Streams への切り替え
Streams で永続的にメッセージを受信する方法
Streams からデータを読む XREAD コマンドは、Pub/Sub の SUBSCRIBE コマンドとは異なり、永続的にコマンドが実行されるわけではなく、データが取得されるとコマンドの実行が完了してしまいます。
Streams で継続的にデータを受信するためには、前回の実行で取得されたメッセージからそれ以降を取得するコマンドを繰り返し実行することで、新規メッセージの受信を継続的に行うことができます。
以下に擬似コードで示します。
var lastId = "0"
while(true) {
elements = redis.call("XREAD BLOCK 0 STREAMS ${mystream}${lastId}") // "BLOCK 0" は次のメッセージが届くまでずっと待機する
elements.foreach {
send(elements.body)
}
lastId = elements[-1].id
}
アーキテクチャ
Redis Streams に切り替えた後のアーキテクチャは次の図のようになりました。

Redis Pub/Sub を利用していた従来のアーキテクチャと比べると、即時配信(Redis Pub/Sub)と過去配信分の保存(Redis Lists)の 2 つのクラスターが、Redis Streams の 1 つのクラスターで完結し、ストリーミングサーバーと Redis Streams のやり取りも 1 つのコマンド実行で完結するため、シンプルな構成となりました。
無停止での移行
今回の Redis Pub/Sub から Redis Streams への移行には、ユーザ数が多いため無停止で移行すること、比較的大きな改修であるためモニタリングしながら進められるように徐々に移行割合を増やしていくこと、という 2 点が求められました。
そこで、以下のような移行ステップを踏むことで無停止で移行を行い、各ステップ内で適用割合を増やすことで問題があった場合に影響を最小限に抑え、切り戻しを容易にできるようにしました。
各ステップ内での適用割合を動的に変更する方法として、LINE の OSS である Central Dogma を用いました。Central Dogma は各種設定を集中管理し、設定に変更がある場合はアプリケーションに即時通知することで、アプリケーションの再起動なしに設定を適用できるようになります。
Step0. 従来の構成

Step1. Redis Streams cluster を用意し、event を追記する。既存の Redis Pub/Sub,Redis Lists への書き込みも継続する。

Step2. Streaming server で、読み込み先を Redis Pub/Sub, Lists cluster から Redis Streams cluster へ切り替える。

ステップ 3。Redis Pub/Sub およびリストへの書き込みを停止し、それらのクラスターを撤収する。

結果
その結果、Redis Cluster の Pub/Sub を利用していた従来の構成では、ノードあたり平常時で 500Mbps、ピーク時で 1.5Gbps の帯域を消費していたのが、Redis Streams を利用した構成に切り替えたことで、平常時で 6Mbps、ピーク時で 11Mbps 程度に抑えることができました。
(ただし、従来から性能不足となっていたため、Redis Streams のクラスターでは従来の Redis Pub/Sub のクラスターと比較して 2 倍のシャード数、ノード数で運用しています。)


したがって、Redis Cluster の Pub/Sub を利用していた従来の構成で問題となっていたネットワーク帯域の圧迫問題は、Redis Streams へ切り替えることで解消されました。今後、メッセージブローカーとして利用している Redis Cluster が性能不足となった場合でも、スケールアウトによって性能強化することが可能となります。
また、従来のアーキテクチャでは、メッセージの即時配信とメッセージの保存でそれぞれ Redis Cluster を用意する必要がありましたが、Redis Streams に切り替えたことで一つの Redis Cluster で済むようになり、よりシンプルな設計にすることができました。
原文を表示
この記事は、合併前の旧ブログに掲載していた記事(初出: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 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/SubRedis 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のようにシャード数が多くなるほどネットワーク帯域を圧迫してしまう問題が起きません。
3. 接続数
Redis ClusterにおけるPub/Subでは、任意のシャードから任意の複数チャンネルをsubscribeできます。つまり、複数のチャンネルをsubscribeする場合でも、ある1つのシャードへの1つの接続を保持すればよいのです。
一方、Streamsの場合、特定のメッセージは書き込んだシャードからしか読むことができないと先ほど説明しました。これからわかるように、N個のストリームをconsumeするケースでは、最悪の場合それぞれのシャードに接続する必要があり、その結果N個の接続を保持する必要があります。
私たちのアプリケーションでは、1つのクライアントあたり2つのチャンネル(ストリーム)を読むだけでよいので、接続数はRedis Streamsの場合、Redis Pub/Subに比べて高々2倍程度となります。しかし、スケールアウトによりシャードあたりの接続数を抑えることができるようになるため、これは大きな問題ではありませんでした。
Redis Streamsへの切り替え
Streamsで永続的にメッセージを受信する方法
Streamsからデータを読むXREADコマンドは、Pub/SubのSUBSCRIBEコマンドとは異なり、永続的にコマンドが実行されるわけではなく、データが取得されるとコマンドの実行が完了してしまいます。
Streamsで継続的にデータを受信するためには、前回の実行で取得されたメッセージからそれ以降を取得するコマンドを繰り返し実行することで、新規メッセージの受信を継続的に行うことができます。
以下に擬似コードで示します。
var lastId = "0"
while(true) {
elements = redis.call("XREAD BLOCK 0 STREAMS ${mystream}${lastId}") // "BLOCK 0" は次のメッセージが届くまでずっと待機する
elements.foreach {
send(elements.body)
}
lastId = elements[-1].id
}アーキテクチャ
Redis Streamsに切り替えた後のアーキテクチャは次の図のようになりました。

Redis Pub/Subを利用していた従来のアーキテクチャと比べると、即時配信(Redis Pub/Sub)と過去配信分の保存(Redis Lists)の2つのクラスターが、Redis Streamsの1つのクラスターで完結し、ストリーミングサーバーとRedis Streamsのやり取りも1つのコマンド実行で完結するため、シンプルな構成となりました。
無停止での移行
今回のRedis Pub/SubからRedis Streamsへの移行には、ユーザ数が多いため無停止で移行すること、比較的大きな改修であるためモニタリングしながら進められるように徐々に移行割合を増やしていくこと、という2点が求められました。
そこで、以下のような移行ステップを踏むことで無停止で移行を行い、各ステップ内で適用割合を増やすことで問題があった場合に影響を最小限に抑え、切り戻しを容易にできるようにしました。
各ステップ内での適用割合を動的に変更する方法として、LINEのOSSであるCentral Dogmaを用いました。Central Dogmaは各種設定を集中管理し、設定に変更がある場合はアプリケーションに即時通知することで、アプリケーションの再起動なしに設定を適用できるようになります。
Step0. 従来の構成

Step1. Redis Streams clusterを用意し、eventを追記する。既存のRedis Pub/Sub,Redis Listsへの書き込みも継続する。

Step2. Streaming serverで、読み込み先をRedis Pub/Sub, Lists clusterからRedis Streams clusterへ切り替える。

Step3. Redis Pub/Sub, Listsへの書き込みを停止し、それらのclusterを撤収する。

結果
結果として、Redis ClusterのPub/Subを利用していた従来の構成では、ノードあたり平常時で500Mbps、ピーク時で1.5Gbpsの帯域を消費していたのが、Redis Streamsを利用した構成に切り替えたことで、平常時で6Mbps、ピーク時で11Mbps程度に抑えることができました。
(ただし、従来から性能不足となっていたので、Redis Streamsのクラスターでは従来のRedis Pub/Subのクラスターと比べ2倍のシャード、ノード数で運用しています。)


したがって、Redis ClusterのPub/Subを利用していた従来の構成で問題となっていたネットワーク帯域の圧迫問題は、Redis Streamsへ切り替えることで解消されました。今後、メッセージブローカーとして利用しているRedis Clusterが性能不足となった場合でも、スケールアウトによって性能強化することが可能となります。
また、従来のアーキテクチャでは、メッセージの即時配信とメッセージの保存でそれぞれRedis Clusterを用意する必要がありましたが、Redis Streamsに切り替えたことで一つのRedis Clusterで済むようになり、よりシンプルな設計にすることができました。
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み