背景
ロングポーリングの欠点として、ファイルディスクリプタ(やそれに伴いメモリ)の枯渇がよく上がる。
それを避けるためにWebSocketの利用が上がるわけだが、WebSocketはなぜFDが枯渇しないのだろうか?と考え調べたのでここにまとめておく。
(ServerSideEventはここでは触れない。)
今回の結論
WebSocketでもFDは消費する。
1TCPコネクション=1FD消費。
ただし、WebSocketはHTTP PollingのようにリクエストごとにTCP接続を作成・破棄せず、1つのTCP接続を長時間維持して通信を行う。
HTTP PollingではリクエストごとにTCP接続を作成して保持する。そのためクライアント数に比例してソケット・リクエストオブジェクト・バッファなどのメモリ使用量が増加する。
大量のクライアントが接続するとサーバーが保持するリクエスト数も増え、最終的にメモリ不足に陥る可能性がある。
またWebSocketサーバーは通常、epollなどのI/O多重化機構を利用したイベントループにより多数のソケットを効率的に監視することで大量接続をスケーラブルに処理できる。
ファイルディスクリプタとは
ファイルディスクリプタ(以下FD)とはOSが管理している「開いているファイルやソケットなどの入出力対象」を指し示すための整数値の識別子
入出力の管理番号のため、ファイルだけではなく標準出力や外部とのTCP通信もその対象となる。一つのファイル・HTTP通信を行うと一つのFDが占有され、ファイルを閉じる・HTTP通信が終了すると解放される。
また、FDの上限数はプロセス単位やOS単位でそれぞれ決まってくる。
普段FDに触れる例えとして、よくみる以下のようなコマンドをあげる。
以下のコマンドは内部的には標準出力のファイルディスクリプタをファイル出力に付け替えるということをしている。
本来はhelloという情報は標準出力に向けられるが、コマンドではそれをファイルに向けて流している。言い換えるとFDはデータの向き先を管理する仕組みなので、向き先を変えるとファイル出力にできるということ。
# >はファイルディスクリプタの行き先の変更を意味する
echo "hello" > output.txt
ファイルディスクリプタの上限
先ほどFDについてプロセス単位に開いている入出力を管理するための識別子と書いたが、この識別子の個数には上限が存在する。
試しにWindowsのサイトでC言語のプロセス単位のファイルディスクリプタの上限を見てみると、デフォルト設定では512であり、設定できる最大値は8192とある。
Ubuntuではシステム単位でファイルディスクリプタの設定ができるようで、以下の記載がある。
ロングポーリングの欠点
ロングポーリングの欠点は、HTTPレスポンスを送らず(TCP接続をcloseせず)に待機し続けることにある。
待機している間は一つのFDを利用したままにするため、多数のクライアントが一つのサーバに同時にアクセスすると同時に利用できるFDの上限数に達してしまう(=FDが枯渇)
WebSocketではなぜ「FD枯渇が問題になりにくい」のか
先に結論だが、FDの枯渇を防いでいるのはepoll(Linux)やそれに準ずるOSの仕組みであって、WebSocketではない。
WebSocket(双方向通信プロトコル)とそれを効率的に扱うOS側の仕組みが組み合わさってスケーラブルな双方向通信を実現している。
前提として、WebSocketはTCPを使う点は同じものの、HTTPとは全く別のプロトコルであることを認識する必要がある。
WebSocketは通信開始時にHTTPを用いるが、HTTP Upgradeという同一TCP接続上で利用するプロトコルを切り替える仕組みにより、ハンドシェイク完了後はHTTPではなくWebSocketプロトコルで通信を行う。
また、WebSocket単体は双方向通信を可能にするものであり、複数のWebSocket通信をどうやってスケーラブルに捌くか?はOS側でコントロールする必要がある。(あくまでプロトコルなのでそれをどう扱うか?は別問題ということ。)
多数のクライアントがWebSocket接続を貼り続ける場合、クライアント数分のTCP接続が維持され、FDも同数消費される。
しかし epoll 等のI/O多重化機構を用いることで、それら多数のFDを少数スレッドで効率的に監視し、イベントが発生したFDのみを非同期的に処理できるため、スレッドやCPUが枯渇しにくい。
epoll等を使ったI/O多重化の仕組みをイベントループと呼ぶ。
WebSocketでの双方向通信実現の仕組み
HTTPはステートレスなのでセッションを利用して擬似的にステートフルにしている。
詳細は以下の記事で触れている。
これに対しWebSocketではTCPソケットを保持することで双方向通信を実現している。
TCPソケットには以下のような情報が保持される。
- protocol
- server IP
- server port
- client IP
- client port
これらの情報や TCP の内部状態(シーケンス番号、送受信バッファ等)はOSによってTCPソケットとして保持され、そのソケットはファイルディスクリプタ(FD)として管理される。
WebSocketサーバでは、これら複数のFDをepollなどのイベント通知機構に登録しI/Oが可能になったFDのみがOSから通知されることで、少数のスレッドで多数の接続を非同期的に処理している。
