ELBを介したSSEを行う場合の設定や気を付けるところ
SSE(Content-Type: text/event-stream)便利ですよね。フレームワークがサポートしていることも多いですし、その通りやれば結構サクッと動作します。
しかし、ローカルで試した場合にサクッと動作しても、ELBを介した場合に結構ハマったりしたので、その場合の解消法というか、ここ設定しとくと良いかもを書きます。
クライアント(アプリなど) ⇔ ELB ⇔ nginx(EC2インスタンス) ⇔ サーバアプリケーション
という構成が前提です。
結論
結論から言うと、下記の設定をしておくと私が試した限りはスムーズでした。
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リスナーはどう?とアドバイスをもらい、一発で解決したのであった。おしまい。