読み飛ばしてください
おはようございます、しなもんです。
Dockerって便利ですよね。大好きです。
Dockerを使うことで環境構築が楽になりますし、開発環境からの移行も簡単で助かっています。
ですがそんなDockerを長年使用していた結果、仮想環境内の依存先の状況を把握しておらず、見事にサービスが止まることになりました。
簡単な経緯
CinnamonWorksで運営しているDiscordBotで起きた事件です。
2023/11/16の深夜0時34分、Discordの運営より緊急メッセージが来ました。
意訳すると、
あなたのBotが短期間に1000回以上Discordに接続しました。
この動作は一般的にバグであるため、あなたのBotのトークンをリセットしました。
BotとDiscordの通信にはトークンが必要です。
それがリセットされたため、サービスが止まってしまいました。
DiscordのBotを知らない方にもシステムの概要が分かっていただけるよう説明すると、
- DiscordとBotは常にWebsocketで通信している
- もしイベントが発生した場合は、HTTP APIでやりとりする
という感じです。
詳しい説明は今回の本題ではないので省きますが、
DiscordAPIの全体図をサクッと理解したい方はこちらのQiita記事が参考になると思います。
メッセージの内容から察するに、Websocket通信に何らかのエラーが発生し、
短期間に再接続を繰り返した結果、Discordに出禁をくらった、という次第なわけです。
(追記・24h以内に1000回ログインがあった場合に起こる処置のようです。)
いままで問題なかったのに、、、?
解決した
幸いにもサービス停止自体には停止から5分後に気づくことができました。
とりあえずトークンを再取得しBotを再起動させますが、エラーが発生し再起動を繰り返す状態になってしまいました。
エラーの内容
Traceback (most recent call last):
File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 731, in _load_from_module_spec
spec.loader.exec_module(lib) # type: ignore
File "<frozen importlib._bootstrap_external>", line 883, in exec_module
File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
File "/usr/src/bot/cogs/genbot.py", line 11, in <module>
import main
File "/usr/src/bot/main.py", line 66, in <module>
bot.run(TOKEN)
File "/usr/local/lib/python3.10/site-packages/discord/client.py", line 715, in run
return future.result()
File "/usr/local/lib/python3.10/site-packages/discord/client.py", line 694, in runner
await self.start(*args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/discord/client.py", line 658, in start
await self.connect(reconnect=reconnect)
File "/usr/local/lib/python3.10/site-packages/discord/shard.py", line 466, in connect
raise item.error
File "/usr/local/lib/python3.10/site-packages/discord/shard.py", line 184, in worker
await self.ws.poll_event()
File "/usr/local/lib/python3.10/site-packages/discord/gateway.py", line 591, in poll_event
await self.received_message(msg.data)
File "/usr/local/lib/python3.10/site-packages/discord/gateway.py", line 541, in received_message
func(data)
File "/usr/local/lib/python3.10/site-packages/discord/state.py", line 1221, in parse_guild_create
guild = self._get_create_guild(data)
File "/usr/local/lib/python3.10/site-packages/discord/state.py", line 1185, in _get_create_guild
guild._from_data(data)
File "/usr/local/lib/python3.10/site-packages/discord/guild.py", line 504, in _from_data
self.stickers: Tuple[GuildSticker, ...] = tuple(
File "/usr/local/lib/python3.10/site-packages/discord/guild.py", line 505, in <lambda>
map(lambda d: state.store_sticker(self, d), guild.get("stickers", []))
File "/usr/local/lib/python3.10/site-packages/discord/state.py", line 370, in store_sticker
self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data)
File "/usr/local/lib/python3.10/site-packages/discord/sticker.py", line 277, in __init__
self._from_data(data)
File "/usr/local/lib/python3.10/site-packages/discord/sticker.py", line 420, in _from_data
super()._from_data(data)
File "/usr/local/lib/python3.10/site-packages/discord/sticker.py", line 284, in _from_data
self.url: str = f"{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}"
File "/usr/local/lib/python3.10/site-packages/discord/enums.py", line 564, in file_extension
return lookup[self]
KeyError: <StickerFormatType.unknown_4: 4>
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/src/bot/main.py", line 52, in <module>
bot.load_extensions(
File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 974, in load_extensions
loaded = self.load_extension(ext_path, package=package, recursive=recursive, store=store)
File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 867, in load_extension
self._load_from_module_spec(spec, name)
File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 734, in _load_from_module_spec
raise errors.ExtensionFailed(key, e) from e
discord.errors.ExtensionFailed: Extension 'cogs.genbot' raised an error: KeyError: <StickerFormatType.unknown_4: 4>
Task was destroyed but it is pending!
task: <Task pending name='pycord: on_ready' coro=<Client._run_event() done, defined at /usr/local/lib/python3.10/site-packages/discord/client.py:374> wait_for=<Future pending cb=[Task.task_wakeup()]>>
(ファイルパスや関数名とかが含まれてますがこのBotはオープンソースなので気にしません)
要約すると、Discordサーバーから受信したデータの中に未知のフォーマットが含まれていて、プログラムが処理できない状態になっているようです。
Discordとの通信にはPycordというラッパーライブラリを使用しています。
そのライブラリがバグを起こしていると想定し、requirements.txtで指定しているPycordのバージョンを変更しました。
- py-cord==2.1.3
+ py-cord==2.4.1
推奨される関数が変更されるなどの仕様変更が入りましたが、エラーはさっぱり消え去りました。
本題(教訓)
依存ライブラリのバージョンは定期的に確認しよう。本当に。
1. ライブラリ・依存関係の最新化を怠らない
使用するライブラリや依存関係を定期的に更新することで、
最新の機能やセキュリティ対策を享受できます。
(当たり前と思っていてもできてなかった)
特に、Discordのような活発な開発が行われているサービスの場合、仕様変更やバグ修正が頻繁に行われます。最新情報を把握し、迅速に対応することが大事です。
pipを使用している場合は、pip freeze
コマンドなどで定期的に依存関係を確認すると良いようです。
2. 仮想環境内の状況を把握する
開発環境だけでなく、本番環境も含めて仮想環境内の状況を定期的に確認し、問題がないか確認する習慣を身につけるべきでした。
最近は以下の点を確認するようにしています。
- 使用しているライブラリのバージョン
- 設定ファイルの内容
- ログファイルにエラーメッセージがないか
3. 監視を付ける
Uptime Kumaというモニタリングツールを導入しました。
これを物理的・地理的に本番環境と異なる場所に配置し、システムを監視しています。
これがかなり便利で簡単でおしゃれなので、今度別記事で紹介できたらと思います。
書きました。↓
4. 情報収集をする
Discord 公式ブログやドキュメント、開発者コミュニティなどを気にかけるようにしました。
すべて英語なので正直めんどくさい
ただ、この対策によって救われた事件がありました。
これは無事に対策して乗り越えられたので良かったです。
最後に
教訓がどっさり生まれました。役に立てて行こうと思います。
Discordが毎年のように破壊的変更してくるのが悪い気もしてきた
では私はこれからAPIの全資料をじっくり読み込んでくるのでこれで失礼します。
それでは。