ストリームを殺すのは間にいるものだ
一言でいうと
ストリーミングが死ぬ場所は、たいていアプリケーションではなく、間にあるものです。 プロキシのバッファ、読み取りタイムアウト、そしてブラウザの同時接続数の上限です。
なぜ必要なのか
ローカルではトークンが1文字ずつ流れるのに、デプロイすると、答えがすべて終わったあとに 一度にまとめて出てきます。アプリケーションのコードは1行も違いません。ログを見ても、 サーバーは時間どおりに送ったと書かれています。
途中にプロキシがあるからです。nginxのproxy_bufferingは、
ドキュメント上のデフォルトがon
で、有効になっているとレスポンスをバッファにためてから出力します。ストリーミングは「少しずつ頻繁に」が
すべてなのに、バッファリングは「まとめて一度に」が目的なので、まさに正反対です。
2つ目によく出会うのが、タイムアウトです。同じドキュメントでproxy_read_timeoutの
デフォルト値は60sで、説明は明確です。プロキシされたサーバーがその時間内に何も
送らなければ、接続を閉じます。 ユーザーが質問をしない夜間の時間帯に、通知
ストリームが1分ごとに切れる理由が、これです。
どう動くのか
バッファリングを無効にする2つの場所
設定ファイルでproxy_buffering offとして無効にすることもできますが、そうするとその経路のすべての
レスポンスがバッファリングを失います。同じドキュメントが、より狭い方法を書いています。
レスポンスヘッダーで無効にする方法です。
X-Accel-Buffering: no
yesまたはnoをこのヘッダーで返すと、そのレスポンスに対してだけバッファリングが有効になったり
無効になったりします。ストリーミング用のエンドポイントだけにこのヘッダーを付ければ、残りの経路は
バッファリングの利点をそのまま維持できます。アプリケーションが自分の事情をいちばんよく知っている場所で
決められるようにする設計なので、設定ファイルを変更する権限がないときにも使えます。
注意すべきなのは、これがnginxにだけ向けた指示だという点です。手前にCDNやほかの プロキシがさらにあるなら、そちらにも同じことを別に伝える必要があります。1層だけ解除して 「直した」と済ませると、別の層でそのまま詰まります。
ハートビートはマナーではなく必須
読み取りタイムアウトを避ける方法は、静かな区間をなくすことです。仕様が定めている コメント行が、まさにその用途に合っています。コロンで始まる行は無視されるので、クライアント 側には何の影響もなく、バイトだけが流れます。
: keep-alive
間隔は、プロキシが待ってくれる時間よりも十分に短くする必要があります。デフォルトが60秒のところで 55秒間隔を使うと、ジッターが1回入っただけで切れます。実務では、その半分以下にします。
ハートビートには、もう1つの使い道があります。クライアント側で「いつから何も 来ていないか」を測れるようになります。イベントがもともと少ないストリームでは、静かなのが 正常なのか死んでいるのか区別する方法がないのですが、ハートビートがあればその判断ができます。
ブラウザの同時接続数の上限
HTTP/1.1では、ブラウザは1つのオリジン(origin)に開く接続数を制限します。MDNは、 「かつては2から3の間だったデフォルト値が、今では一般に6個」と書いています (HTTP/1.x接続管理)。 SSE接続は終わらないレスポンスなので、その枠を占有し続けます。タブを6つ開くと 6つの枠がすべて埋まり、7つ目のタブはもちろん、そのオリジンに向かう普通のAPIリクエストまで 順番待ちになります。画面が止まったように見えるのに、サーバーのログにはリクエストが届いてすらいないため、 原因を探すのが特に難しくなります。
解決方法は2つあります。1つはHTTP/2に上げることです。1本のTCP接続の上で、ストリームを 複数多重化するので(RFC 9113)、 接続1本の問題に置き換わります。もう1つは、タブごとに接続しないことです。共有 ワーカー(SharedWorker)が 接続を1つ握り、 BroadcastChannel で同じオリジンの他のタブに分配します。
現場での姿
「ローカルでは動くのに、ステージングでだけ一度にまとめて出てくる」という報告は、ほぼ常に バッファリングです。確認する方法は、アプリケーションのログではなく、最初のバイトまでにかかった 時間です。ストリーミング経路と、一度にまとめて返す経路の最初のバイトの時刻を並べて測れば、 一発で分かれます。合計時間が同じなのに最初のバイトだけが違うなら、途中でためているのです。
夜にだけ切れる通知ストリームは、ハートビートがないか、間隔がタイムアウトに近すぎる 場合です。再現が難しそうに見えますが、静かな時間帯にだけ起きるという事実そのものが、すでに 「何かを送っていないから」という手がかりです。
同時接続数の上限は、QAではほとんど見つかりません。テストはタブ1つで行うからです。 報告はいつも「タブを複数開いておくと遅くなる」という形で届き、その文を パフォーマンスの問題として読んだ瞬間に、何日も消えてしまいます。
次のクイズで確認すること
バッファリングをレスポンスヘッダーで無効にする場所、ハートビートの間隔を何に合わせて決めるのか、同時接続数の 上限がどんな症状として現れるのかを、6問で確認します。