Django + daphne + channels構成のWebsocketサーバーで502エラー
概要
Django + Dapahne + Channlels 構成の Websokcet サーバーにて、502エラーが発生する事態に遭遇しました。
原因や解決策を、調べた範囲で書いていきます。
結論
サーバー側に、実質的な Websocket 接続上限が存在していた。
上限を超えてWebsocket接続を試みた結果、接続時に502エラーが発生した。
Websocketの上限は、マシンの CPU コア数に依存する模様。
また、環境変数で上限を設定することもできる。
構成
遭遇した問題
ある日、WebsocketクライアントからWebsocket接続を試みたところ、エラーが発生するようになった。
このエラーを観察したところ、以下の特徴があった。
- 常に失敗するのではなく、失敗したり成功したりする
- 一度失敗すると、連続で失敗する傾向にある
- しばらく時間を置くと、成功することがある
ログを確認したところ、以下を確認。
- Websocketクライアント(≒フロントエンド)側にて、
502
ステータスコードが記録されていること - Websocketサーバー(≒バックエンド)側にて、
1006
ステータスコードが記録されていること
原因(仮説)
ASGI(≒Websocketサーバー)側で、スレッドの上限を管理している模様。
以下、 https://docs.djangoproject.com/ja/5.0/_modules/asgiref/sync/ より該当箇所を引用。
# If they've set ASGI_THREADS, update the default asyncio executor for now
if "ASGI_THREADS" in os.environ:
# We use get_event_loop here - not get_running_loop - as this will
# be run at import time, and we want to update the main thread's loop.
loop = asyncio.get_event_loop()
loop.set_default_executor(
ThreadPoolExecutor(max_workers=int(os.environ["ASGI_THREADS"]))
)
また、 github の issue に、ASGI_THREADS
環境変数でスレッド数制限を試みた旨の記載が確認できた。
マシンスペック不足で起こりがちな エラーの特徴 も相まって、「このスレッド1つが、1つのWebsocket接続を処理しているのでは?」「スレッドが足りなくなるほど接続を試みた結果、接続に失敗したのでは?」と推測。
以上より、このスレッドが、Websocket 接続に使用されているのでは?という仮説を立てた。
検証(エラーの再現)
「ASGI_THREADS
の値だけ、Websocket接続用のスレッドが作られる」と仮定するなら、
ASGI_THREADS
環境変数を1に設定すると、2つ以上の Websocket 接続時にエラーが発生すると思われる。
ASGI_THREADS=1
に設定し、2つ目の Websocket 接続を試みたところ、以下を確認。
- クライアント側では、
hang up
のメッセージと共に切断 - サーバー側では、ステータスコード
1006
を返しつつ切断
仮説通りの結果となった。
解決策
幾つかの案が考えられる。
No | 案 | メリット | デメリット |
---|---|---|---|
1 | スケールアップ(マシンのCPUを増設)する | 手っ取り早い | 手動運用(接続数が更に増えた場合、手動で更なるスケールアップ)が必要 |
2 | スケールアウト(Websocket接続数に応じてインスタンスを増設)する | 自動運用可能 | Websocket接続数にてスケールアウトする仕組みの開発が必要 |
3 |
ASGI_THREADS の値を設定する |
Websocket接続数をコントロールできる。上記案との併用も考えられる。 | (調べた限り)適正値不明 |
案1は、暫定対応といった、応急処置に近い。
案2は、恒久対応に近い。
ASGI_THREADS
に大きな値を割り当てれば解決しそうな気はするが、どのような挙動になるかはわからない。
その他
ASGI_THREADS
未設定時のスレッド数は不明。
issue を見ると、 「CPU * 5 かもしれないが、確かではない」といった記述がみられる。
Discussion