ELBを介したSSEを行う場合の設定や気を付けるところ

SSE(Content-Type: text/event-stream)便利ですよね。フレームワークがサポートしていることも多いですし、その通りやれば結構サクッと動作します。

しかし、ローカルで試した場合にサクッと動作しても、ELBを介した場合に結構ハマったりしたので、その場合の解消法というか、ここ設定しとくと良いかもを書きます。

クライアント(アプリなど) ⇔ ELB ⇔ nginx(EC2インスタンス) ⇔ サーバアプリケーション

という構成が前提です。

結論

結論から言うと、下記の設定をしておくと私が試した限りはスムーズでした。

  • ELBのリスナーはHTTP/HTTPSではなく、TCP/SSLを使用
  • nginxのproxy_bufferingはoff

ELBのリスナーはHTTP/HTTPSではなく、TCP/SSLを使用

これがほとんどなのですが、ELBのリスナーはHTTP/HTTPSを使用すると、ELBが接続を保持したままになり、クライアントがHTTPコネクションを切断しても、サーバアプリケーション側に切断が伝わらない場合があるっぽいです。Redis pub/subなどに接続し、そっからデータを受け取るようなことをしている場合、Redisとの接続を切る必要があるかと思いますが、そういう場合致命的です。

この場合、ELBのリスナーにTCP/SSLを使用してあげたら、スパッと解消されました。ただし、HTTP/HTTPSは専用の処理を挟んでいたり(X-Forwarded-Forリクエストヘッダの付与など)、あとこいつのせいでSSEとの相性が悪いのだと思いますが、バックエンドとのコネクションを維持するキープアライブが働いています。それらが必要な場合はまた一考の余地ありとなります。

nginxのproxy_bufferingはoff

単純な話なのですが、nginxのproxy_bufferingが有効なままだと、サーバアプリケーションでイベントを書き込んでもnginxがバッファリングしてしまい、しばらくの間クライアントが受け取れません。SSEの場合、接続を維持するためにハートビート("data:"など意味を為さないイベント)を流すと思うので、結果それによって押し出されはしますが、SSEの性質上バッファリングしない方が正しいと思うので、切ってしまった方が良いかと思います。

location /path/to/sse {
  # ...

  proxy_buffering off;

  # ...
}

とでも書いておきましょう。併せてproxy_read_timeoutも確認しておいた方が良いかもしれません。ハートビートの間隔より短いと接続が切れてしまいます。nginxのデフォルトは60秒(ついでに言うとELBのアイドルタイムアウトも60秒)なので、あまり考える必要はないかもしれませんが……。

紆余曲折の話

私がいろいろ試した話をだらだら書くだけなので、あまり実のある話はありません。

事の発端はAkka StreamsのalsoToを使うとSourceが停止しないで書いたような、既読処理が停止しない問題を調べていたとき。alsoToの使用をやめてこれで問題解消やろ!と思ったが、そんなことはなくあれー?てなった。

ELBを介さず、直接EC2インスタンスにアクセスするとすんなり動作することから、どうやらELBがコネクション張ったままにしてるっぽいことまで分かった。これ無効にしたいんだけど〜と正直思ったが、どうやら無理っぽい。

じゃあSSEのハートビートを無効にし、ELBのアイドルタイムアウト任せに切断、クライアントで再接続と割り切るかと思ったが、それでもサーバアプリケーションの処理が停止しない。ならばサーバアプリケーションから終了を送ってしまえと、60秒で完了を送るようにしたが、これだとサーバアプリケーションの処理は停止しても、クライアントでエラーが発生し(Google Chromeだとnet::ERR_INCOMPLETE_CHUNKED_ENCODING)、再接続もしばらくの間失敗するというイケてない感じになった。

ちなみにSSEのハートビートを無効にしたときもいろいろあった。nginxのproxy_bufferingがonになっていてクライアントがイベントを受け取れなかったり、Akka Streamsが下流の停止を検出できず、動作が停止しなかったり。まあ結局ハートビートは有効にしたのでこの辺は関係ないのだけど。

で、チームにひたすら進捗投げてたんだけど、そこでHTTP/SSLリスナーはどう?とアドバイスをもらい、一発で解決したのであった。おしまい。