大規模なBotのShardingなどのベストプラクティス

# はじめに
私が個人で作成しているBotのZeTNONが、お陰様で2000サーバーに導入されました。
しかしサーバー数が多くなるにつれて処理が追いつかなくなったり、Gateway(要するにWebsocket通信)のレートリミットが散見されるようになりました。
しかしながらShardingなどの技術はGoogleで検索してもほぼ解説記事がなく、あったとしても古い情報がほどんどです。
そのため、今回は大規模Botを運営する上でのベストプラクティスについて解説します。

まずこの記事では`discord.py`のみについて取り扱います。その他の言語の皆さんごめんなさい。
`discord.py`をインストールしていない場合は、以下のコマンドでインストールしてください。
```cmd:cmd
$ pip install -U discord.py
```

# Shardingについて
DiscordのShardingは、公式API Docsでは以下のように説明されています。

```
As apps grow and are added to an increasing number of guilds,
some developers may find it necessary to divide portions of their app's operations across multiple processes.

As such, the Gateway implements a method of user-controlled guild sharding which allows apps to split events across a number of Gateway connections.
Guild sharding is entirely controlled by an app, and requires no state-sharing between separate connections to operate.

While all apps can enable sharding, it's not necessary for apps in a smaller number of guilds.
```

```text:和訳
アプリが成長し、ギルドに追加されるアプリの数が増えるにつれて、一部の開発者は、アプリの操作の一部を複数のプロセスに分割する必要があると感じるかもしれません。
そのため、ゲートウェイは、アプリが多数のゲートウェイ接続にわたってイベントを分割できるようにする、ユーザー制御のギルド シャーディングの方法を実装します。
ギルド シャーディングはアプリによって完全に制御され、動作するために個別の接続間で状態を共有する必要はありません。
すべてのアプリでシャーディングを有効にできますが、少数のギルドのアプリでは必要ありません。
```

Discord Gatewayとは、リアルタイムでDiscordのサーバーと自分のアプリを通信させるために、WebSocketを使って行われるものです。
これにはイベントハンドラがあります。`on_message`や`on_guild_join`などですね。

一つのGateway接続だけだと、Botの導入数が増えていくにしたがって送受信が追い付かないことがあります。
Shardingとは、そのようなGateway接続を複数開いておき、一つのGatewayに接続が集中しないようにする技術です。
さらに、複数のサーバーに別々のshard_idを割り当てることで、Botシステム全体の負荷分散にもなります。

# Shardingの基準
Shardingを有効にする基準ですが、少なくとも1000サーバーに導入されている場合、Shardingを有効にすることを推奨しています。Sharding一つで最大2500のサーバーを処理することができるため、2500サーバー以上導入されているBotは必ずShardingをしなければいけません。
1000サーバー未満の場合、逆にShardingが余計にシステムリソースを貪食し、システム全体のスループットが低下する恐れがあります。
また、後述する`AutoShardedClient`(または`AutoShardedBot`)は、discord.pyが自動的に1000サーバーにつきShardを一つ作ってくれます。
そのため、基本的には`サーバー数÷1000`を切り上げたものが、Botに必要なシャード数です。

# Shardingのやり方

Shardingの有効化は簡単です。
いままで使っていたメインのファイル
```py:main.py
# 中略

client = discord.Client(intents=discord.Intents.all())

# 中略
```
となっているものを、
```py:main.py

client = discord.AutoShardedClient(intents=discord.Intents.all())

```
とすることで、自動的にShardを計算し、適切な数だけ割り当ててくれます。
また、command.extの拡張機能を使っている`commands.Bot`の場合、
```py:main.py
bot = commands.Bot()
```
となっているのを、
```py:main.py
bot = commands.AutoShardedBot(intents=discord.Intents.all())
```
とすれば大丈夫です。

# メンバーのキャッシュやサーバーデータのキャッシュについて
現在、Discordにはintentsというものがあります。
これは[別の記事](https://qiita.com/Tettu0530_Qiita/items/709044b2fb71a8bf1faa)で詳しく解説しているので、今回は割愛させていただきます。
このIntentsのmembersを有効化すると、Botが起動した際に自動でメンバーのキャッシュ(BotがいちいちAPIを呼び出さなくてもいいように内部データにすること)が始まります。
しかしこのキャッシュの作業は、非常に時間がかかるため(2000サーバーで20～30分程度)、そのままにしておくと起動がものすごく遅くなります。
さらに、大量のサーバーに追加されている場合、一度に大量にサーバーをリクエストするため、容易にWebSocketのRate limit(レートリミット)に引っかかります。公式でのrate limitの閾値は`120req/60s`、ようするに平均`2req/s`ということです。

これは、botインスタンスをつくるときに以下のような引数を設定することで解決します。
```py:main.py
# AutoShardedClientも同じ引数です
bot = AutoShardedBot(chunk_guilds_at_startup=False, member_cache_flags=discord.MemberCacheFlags.none())
```

これで、起動時に自動で全サーバーのメンバーをキャッシュをしなくなります。やったね。
しかしこのままでは、`bot.get_user()`は一切使えません。なぜならこのようなメソッドはすべてキャッシュから取っているためです。
そのため、ユーザーを取得するなら`bot.get_user()`ではなく、`Guild.chunk()`でメンバーリストを取得するか、`bot.get_user()`を使う直前にキャッシュに入れておくというのが一般的です。
なお、`Guild.chunk()`はWebSocketを呼び出すため、60秒間に120リクエストというレート制限があることにご留意願います。
`await bot.fetch_user()`を使うのも一つの手ですが、こちらはそのままDiscord HTTP APIを呼び出すので最適であるかどうかはなんとも言えません。

# さいごに
このような大規模なBotに関するベストプラクティスが解説されている記事は本当に少なすぎるので、執筆しました。更新情報がありましたら、都度更新していきます。

毎度のことながら、フォロー・いいねよろしくお願いします！