Redis 7 の Sharded Pub/Sub まとめ

こちらは [AWS for Games Advent Calendar 2022](https://qiita.com/advent-calendar/2022/aws-for-games) の 4日目の記事です.

リーダーボード, メッセージング, キャッシュなどゲームワークロードに必要なコンポーネントの実装するのに便利な機能を提供してくれる Redis.
本記事では, このうちのメッセージングに焦点を当て, ゲームにおけるクライアント通知のユースケースと Redis 7 で 追加された Sharded Pub/Sub に書いています.
## クライアント通知のユースケースと Redis Pub/Sub

マッチメイクの成立, ロビーの入退室, アチーブメント表示, チャット投稿などサーバー側や他プレイヤーの状態変化をクライアントにリアルタイムに通知するといったユースケースが挙げられます.
クライアント通知の実現方式はいくつかありますが, WebsocketとRedis Pub/Sub の組み合わせが多くの方にとって手慣れた方法ではないでしょうか. ここでは Redis Pub/Sub についておさらいし, Redis 7 で新規に追加された Sharded Pub/Sub についてまとめてみました.

### Redis Pub/Sub の概要

Redis Pub/Sub はパブリッシャーがチャネルにメッセージを送信し, サブスクライバーがチャネルに送信されたメッセージを受け取る仕組みを提供します. サブスクライバーは, あらかじめサブスクライブしているチャネル宛に送信されたメッセージだけを受け取ることができます. 
![ElastiCache-Redis-PubSub.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2983763/a9235e87-417b-bd30-b93b-0fd187906308.png)

ゲームにおいては例えばこんなプレイヤー体験の実現に活用できそうです. マルチプレイのゲームで, 一緒に戦ってくれる仲間をロビーで待っているとします. ロビーに誰かが入室したら効果音とともに ”〇〇さんが入室しました.” といったメッセージが画面の端に表示されるような仕組みについて考えてみます. 仮にこれが, Redis Pub/Sub で実装されているとしたら, 同じロビーにいるプレイヤーたち（のクライアント）が同一のチャンネルをサブスクライブ(`SUBSCRIBE`)し, 入室したタイミングで, そのチャネルにメッセージを送信(`PUBLISH`)すれば, このような表現ができそうです.

※ 正確には「ゲームのクライアント(コンソール, モバイル, PC など）= Redis クライアント」ではなく, Node.js などでつくられた WebScoket サーバーがその役割を果たします.

### Redis Pub/Sub の課題

クライアント通知のバックエンドを `SUBSCRIBE`, `PUBLISH` といった操作でシンプルに実装できるのは Redis Pub/Sub の良いところですが, 実は課題もあります.

パブリッシュされたメッセージはクラスターバスを介して, 全てのノードにブロードキャストされるのでノード数が多くなるほど, このブロードキャストがより多くの帯域を使用することになります. クラスターをスケールさせたくてノードを増やしているにも関わらず, 反対に全体のスループットを低下させてしまうことにつながります.
大規模な Redis クラスターの運用を想定する場合には注意が必要で, スループット低下の影響が無視できなければレプリケーショングループを分割したり, Redis Stream (Redis 5 以降) で Redis Pub/Sub を代替する実装をするなどのワークアラウンドを取る必要があります.

ちなみに Pub/Sub メッセージのブロードキャストが一度に捌ききれないと, 大量の CPU/メモリ リソースを消費してしまうというクラスターバスのバッファ管理機構のバグもあります. 本記事の執筆時点の最新の安定版である Redis 7.0.5 にはまだ Fix はマージされていません. これに関しては , ElastiCache for Redis のドキュメントでも注意喚起されています.

>クラスターモードが有効なクラスタでは、発行済みメッセージはクラスターバスを介して他のすべてのノードにブロードキャストされます。クラスターバスのバッファ管理により、Pub/Sub トラフィックがロードされたときに EngineCPUUtilization が高くなる可能性があります。


### Sharded Pub/Sub

Sharded Pub/Sub は Redis 7 で追加された新しい Pub/Sub 機能です. クラスターモードにおける従来の Pub/Sub がパブリッシュされたメッセージをクラスタ内の全てのノードにブロードキャストしていたのに対し, Sharded Pub/Sub がメッセージをブロードキャストするのはシャード内のノードに限定されます. そのため, クラスターバスを通過するデータ量が従来よりも削減されます. なお, Redis の公式ドキュメントでは, これまでの Pub/Sub のことを “Global Pub/Sub” と呼ぶことで Sharded Pub/Sub と区別していました. なので, ここでも “Global Pub/Sub” 使ってこうと思います.

ちなみに, 2022 年 11 月 8 日に ElastiCache for Redis が Redis 7 をサポートしました. せっかくなので, ElastiCache for Redis で Sharded Pub/Sub を試してみたいと思います.

## Sharded Pub/Sub 触ってみる

### ElastiCache for Redis クラスタの作成
* CLI で作成していきます.
* サブネット, セキュリティグループは既存のものを使うので割愛.
* コマンド実行結果も割愛

#### 1. サブネットグループを作成する
```
aws elasticache create-cache-subnet-group \
--cache-subnet-group-name redis-subnet-grp \
--cache-subnet-group-description "Testing" \
--subnet-ids [subnet-id-1a] [subnet-id-1b]
```

#### 2. クラスターを作成する
* `create-replication-group` を使うとクラスターモード有効となります.
* `--num-node-groups` でシャード数を指定します.

```
aws elasticache create-replication-group \
   --replication-group-id my-redis7-repl-grp \
   --replication-group-description "Cluster mode enabled" \
   --multi-az-enabled \
   --num-node-groups 2 \
   --replicas-per-node-group 1 \
   --cache-node-type cache.r6g.large \
   --engine redis \
   --engine-version 7.0 \
   --cache-parameter-group-name default.redis7.cluster.on \
   --cache-subnet-group-name redis-subnet-grp
```
#### 3. セキュリティグループを設定する
* <font color="gray">なぜだか, クラスタ作成時にセキュリティグループの設定がうまくできなかったので, 別途実施</font>
```
aws elasticache modify-replication-group \
--replication-group-id my-redis7-repl-grp \
--security-group-ids [sg-id]
```
ここまでで, Redis クラスターの準備はOKです.

### いざ, Sharded Pub/Sub！
まずは, ElastiCache クラスターと同一の VPC 内にいる EC2 インスタンスから redis-cli で設定エンドポイントに接続します. ここでは 2つのクライアントを用意して, それぞれで SSUBSCRIBE と SPUBLISH コマンドを実行していきます.
```
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379>
[ec2-user@ip-10-0-13-148 ~]$ redis-cli -c -h my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com
```
* `SSUBSCRIBE` コマンドでチャネルをサブスクライブします. ここでは, “lobby_001” というチャンネルをサブスクライブしています.
```
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379> SSUBSCRIBE lobby_001
Reading messages... (press Ctrl-C to quit)
1) "ssubscribe"
2) "lobby_001"
3) (integer) 1
```
もう, 一方のクライアントで `SPUBLISH` コマンドでメッセージを送信します.
```
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379> spublish lobby_001 'Alice is here!'
(integer) 0
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379> spublish lobby_001 'Bob is here!'
(integer) 0
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379> spublish lobby_001 'Alice left...'
 (integer) 0
```
* パブリッシュしたメッセージは即時にサブスクライバーに届きます.
```
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379> ssubscribe lobby_001
Reading messages... (press Ctrl-C to quit)
1) "ssubscribe"
2) "lobby_001"
3) (integer) 1
1) "smessage"
2) "lobby_001"
3) "Alice is here!"
1) "smessage"
2) "lobby_001"
3) "Bob is here!"
1) "smessage"
2) "lobby_001"
3) "Alice left..."
```

というように, 基本的な使い方と挙動はこれまでの Pub/Sub と変わりがないように見えます.

その他にも `SUNSUBSCRIBE` コマンドや `PUBSUB SHARDCHANNELS`, `PUBSUB SHARDNUMSUB` といったサブコマンドも用意されており, それぞれ Global Pub/Sub の `UNSUBSCRIBE`, `PUBSUB CHANNELS`, `PUBSUB NUMSUB` に相当する機能を持っています.

ですが, "Sharded" という名のとおり, Sharded Pub/Sub はシャードを超えて共有されないため, これまでの Pub/Sub と挙動が変わるところもあります. なお, Sharded Pub/Sub についても, 通常のキーをハッシュスロットにマップするのと同じアルゴリズムで分散されてノードに割り当てられます.

下のように “lobby_001” にメッセージを送っていたクライアントが “lobby_002” にメッセージを送信すると, “lobby_002” のキースロットがマップされているシャードにリダイレクトしてくれます. これは “lobby_001” と “lobby_002” が別のシャードに割り当てられているためです. 

```
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379> spublish lobby_002 'hello'
→ Redirected to slot [7392] located at 10.0.149.64:6379
(integer) 0
10.0.149.64:6379>
```

異なるシャードの チャンネルを `SSUBSCRIBE` でまとめてサブスクライブしようとすると, CROSSSLOT Keys エラーになります. 
```
my-redis7-repl-grp.aaa000.clustercfg.usw2.cache.amazonaws.com:6379> ssubscribe lobby_001 lobby_002
Reading messages... (press Ctrl-C to quit)
(error) CROSSSLOT Keys in request don't hash to the same slot
```
Global Pub/Sub で使用できたパターンマッチングサブスクリプション（PSUBSCRIBE）に相当するコマンドは現時点では実装されていませんが, 将来的には実装されるかもしれません.

[Sharded pubsub implementation #8621](https://github.com/redis/redis/pull/8621)
>PSUBSCRIBELOCAL (Pattern subscribe) is currently not implemented, but has no technical blockers. The motivation is that the behavior is less obvious which pattern should be associated with which slot. The decision for now what to omit it for now.




## まとめ
Pub/Sub をシャーディングすることで, クラスターバスを通過するデータ量を抑制するアプローチをとった Sharded Pub/Sub によって, 大規模にスケールする Pub/Sub が構築できるようになることが期待されます. 一方で, シャードを意識しないで済んでいた Global Pub/Sub と比べるとやはり実装面での考慮ポイントは当然あるよねということがわかります. Sharded Pub/Sub の今後のアップデートにも注目していきましょう.
Sharded Pub/Sub を Shared Pub/Subと見間違え, クラスターモードで使える共有 Pub/Sub !? なんじゃそれと思って調べていたことが, このような形のアウトプットにつながって良かったです.


## 参考資料
* Redis ドキュメント, GitHub Issues
    * [Sharded Pub/Sub](https://redis.io/docs/manual/pubsub/#sharded-pubsub)
    * [Redis Cluster PubSub - Scalability Issues #2672](https://github.com/redis/redis/issues/2672)
    * [Sharded pubsub implementation #8621](https://github.com/redis/redis/pull/8621)
    * [Understanding Pub/Sub Clustering Architecture #1927](https://github.com/redis/redis/issues/1927)
    * [[BUG] Clusterbus buffer management can consume significant memory and CPU utilization during pubsub #10863](https://github.com/redis/redis/issues/10863)
    * [Re-design cluster link send buffer to improve memory management #11343](https://github.com/redis/redis/pull/11343)
* AWS ドキュメント, ブログなど
    * [Common ElastiCache Use Cases and How ElastiCache Can Help - Messaging (Redis Pub/Sub)](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/elasticache-use-cases.html#elasticache-for-redis-use-cases-messaging)
    * [Best practices: Pub/Sub](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/BestPractices.PubSub.html)
<br>
(免責) 本記事の内容は個人の意見であり、所属企業を代表するものではありません.