<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Proxy_buffering on Ambertu</title><link>https://ambertu.com/tags/proxy_buffering/</link><description>Recent content in Proxy_buffering on Ambertu</description><generator>Hugo</generator><language>zh-CN</language><copyright>2026 Ambertu · 粤ICP备18071259号</copyright><lastBuildDate>Thu, 03 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://ambertu.com/tags/proxy_buffering/index.xml" rel="self" type="application/rss+xml"/><item><title>反代把大模型 API 的 SSE 流式响应缓冲了：五个缓冲点与三层排查法</title><link>https://ambertu.com/posts/llm-api-sse-proxy-buffering/</link><pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate><guid>https://ambertu.com/posts/llm-api-sse-proxy-buffering/</guid><description>大模型 API 中转后流式输出变成一次性返回，首字节延迟等于整个生成时长？问题几乎不在应用。本文列出请求路径上真实存在的五个缓冲点，给出一套从应用往外逐层二分的排查方法。</description><content:encoded><![CDATA[<p>自建大模型 API 中转后最常见的故障是：<strong>后端明明是流式的，经过反向代理就变成一次性返回</strong>——客户端等十几秒没有任何输出，然后整段文字一次吐完。首字节延迟几乎等于整个生成时长，流式的意义全部丧失。</p>
<p>这个问题九成不在应用代码里，而在 <strong>nginx 的 <code>proxy_buffering</code> 默认是开启的</strong>。但真正麻烦的地方是：请求路径上会缓冲的地方不止 nginx 一个，实际有五个，而且<strong>其中一个是你用来排查的 curl 自己</strong>。如果不知道这一点，你会在错误的层上改半天配置。</p>
<h2 id="先分清-sse-和-websocket">先分清 SSE 和 WebSocket</h2>
<p>这是配置写错的根源。大模型 API 的流式输出用的是 <strong>SSE（Server-Sent Events）</strong>：一个普通的 HTTP 响应，<code>Content-Type: text/event-stream</code>，靠 chunked 编码持续往下写。<strong>它不涉及协议升级。</strong></p>
<p>而网上大量&quot;流式接口反代配置&quot;教程给的是 WebSocket 的写法：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="c1"># ❌ 这是 WebSocket 的配置，SSE 不需要，而且硬编码有害
</span></span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Upgrade</span> <span class="nv">$http_upgrade</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Connection</span> <span class="s">&#34;upgrade&#34;</span><span class="p">;</span>
</span></span></code></pre></div><p>问题出在第二行<strong>硬编码</strong>了 <code>&quot;upgrade&quot;</code>：它会给经过这个 location 的<strong>所有</strong>请求都带上 <code>Connection: upgrade</code>，包括根本没有 <code>Upgrade</code> 头的普通请求和 SSE 请求。<code>Connection</code> 是逐跳（hop-by-hop）首部，被这么滥用会破坏到上游的 keepalive，某些上游实现还会因此行为异常。</p>
<p>正确的写法是用 <code>map</code> 按客户端是否真的请求升级来取值：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="c1"># http 块内
</span></span></span><span class="line"><span class="cl"><span class="k">map</span> <span class="nv">$http_upgrade</span> <span class="nv">$connection_upgrade</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">default</span> <span class="s">upgrade</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">&#39;&#39;</span>      <span class="s">close</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># location 内
</span></span></span><span class="line"><span class="cl"><span class="k">proxy_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Upgrade</span>    <span class="nv">$http_upgrade</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Connection</span> <span class="nv">$connection_upgrade</span><span class="p">;</span>
</span></span></code></pre></div><p>如果这个 location 只服务 SSE、完全没有 WebSocket，那更简单——两行都不要写，改成让 nginx 复用到上游的连接：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">proxy_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Connection</span> <span class="s">&#34;&#34;</span><span class="p">;</span>
</span></span></code></pre></div><h2 id="请求路径上真实存在的五个缓冲点">请求路径上真实存在的五个缓冲点</h2>
<figure>
<svg viewBox="0 0 800 240" role="img" aria-label="从应用到客户端的请求路径上有五个缓冲点：应用是否调用 flush、nginx 的 proxy_buffering、nginx 的 gzip 压缩、CDN 层缓冲、以及客户端 curl 自身的输出缓冲" style="width:100%;height:auto;color:currentColor">
  <defs>
    <marker id="a1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor"/>
    </marker>
  </defs>
  <text x="0" y="14" font-size="11" font-family="monospace" fill="currentColor" opacity="0.6">token 的流动方向 →</text>
  <g font-family="monospace" font-size="11.5" fill="currentColor">
    <rect x="0" y="42" width="118" height="42" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="59" y="68" text-anchor="middle">应用</text>
    <line x1="118" y1="63" x2="198" y2="63" stroke="currentColor" stroke-width="1.2" marker-end="url(#a1)"/>
    <rect x="200" y="42" width="118" height="42" fill="currentColor" fill-opacity="0.09" stroke="currentColor" stroke-width="1.3"/>
    <text x="259" y="68" text-anchor="middle">nginx</text>
    <line x1="318" y1="63" x2="398" y2="63" stroke="currentColor" stroke-width="1.2" marker-end="url(#a1)"/>
    <rect x="400" y="42" width="118" height="42" fill="none" stroke="currentColor" stroke-width="1.3" stroke-dasharray="4 3"/>
    <text x="459" y="62" text-anchor="middle">CDN</text>
    <text x="459" y="77" text-anchor="middle" font-size="9" opacity="0.6">若有</text>
    <line x1="518" y1="63" x2="598" y2="63" stroke="currentColor" stroke-width="1.2" marker-end="url(#a1)"/>
    <rect x="600" y="42" width="118" height="42" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="659" y="68" text-anchor="middle">客户端</text>
  </g>
  <g font-family="monospace" font-size="9.5" fill="currentColor">
    <text x="0" y="112" opacity="0.55">默认</text>
    <text x="0" y="130">看代码</text>
    <text x="0" y="158" opacity="0.55">开关</text>
    <text x="0" y="176">flusher.Flush()</text>
    <text x="200" y="112" opacity="0.55">默认</text>
    <text x="200" y="130" font-weight="bold">缓冲 ⚠ 主因</text>
    <text x="200" y="158" opacity="0.55">开关</text>
    <text x="200" y="176">proxy_buffering off</text>
    <text x="200" y="194">gzip off ← 第二个</text>
    <text x="400" y="112" opacity="0.55">默认</text>
    <text x="400" y="130">多数缓冲 ⚠</text>
    <text x="400" y="158" opacity="0.55">开关</text>
    <text x="400" y="176">控制台流式配置</text>
    <text x="600" y="112" opacity="0.55">默认</text>
    <text x="600" y="130">curl 自己缓冲 ⚠</text>
    <text x="600" y="158" opacity="0.55">开关</text>
    <text x="600" y="176">curl -N</text>
  </g>
  <line x1="0" y1="212" x2="780" y2="212" stroke="currentColor" stroke-width="1" opacity="0.2"/>
  <text x="0" y="232" font-size="10" font-family="monospace" fill="currentColor" opacity="0.6">共 5 个缓冲点：应用 flush · proxy_buffering · gzip · CDN · 客户端</text>
</svg>
<figcaption>nginx 那一格是灰底的，因为它同时贡献了两个缓冲点：<code>proxy_buffering</code> 和 <code>gzip</code>。<strong>最容易误导排查的是最右边那格</strong>——不加 <code>-N</code> 的 curl 自己会缓冲输出，你会以为是服务端问题。</figcaption>
</figure>
<p>第五个缓冲点最坑人，所以单独强调：<strong><code>curl</code> 默认会缓冲输出</strong>。用不带 <code>-N</code> 的 curl 去验证流式，看到的永远是&quot;一次性返回&quot;，无论服务端配得对不对。排查流式问题的第一条纪律是所有 curl 都加 <code>-N</code>（等价于 <code>--no-buffer</code>）。</p>
<h2 id="症状长什么样">症状长什么样</h2>
<figure>
<svg viewBox="0 0 720 180" role="img" aria-label="示意图：缓冲开启时客户端长时间无输出然后一次性收到全部内容；缓冲关闭时 token 从一开始就持续到达" style="width:100%;height:auto;color:currentColor">
  <text x="0" y="14" font-size="11" font-family="monospace" fill="currentColor" opacity="0.6">客户端收到数据的时间轴（示意）</text>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <text x="0" y="48">缓冲开启</text>
    <line x1="130" y1="52" x2="660" y2="52" stroke="currentColor" stroke-width="1" opacity="0.25"/>
    <rect x="560" y="38" width="100" height="18" fill="currentColor" opacity="0.75"/>
    <text x="130" y="76" opacity="0.6">↑ 全程无输出 ——————————————————→ 一次吐完</text>
    <text x="0" y="118">缓冲关闭</text>
    <line x1="130" y1="122" x2="660" y2="122" stroke="currentColor" stroke-width="1" opacity="0.25"/>
    <rect x="140" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="162" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="184" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="206" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="228" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="250" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="272" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="294" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="316" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="338" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="360" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <rect x="382" y="108" width="14" height="18" fill="currentColor" opacity="0.75"/>
    <text x="140" y="146" opacity="0.6">↑ 首字节很快，token 持续到达</text>
  </g>
  <text x="0" y="174" font-size="10" font-family="monospace" fill="currentColor" opacity="0.6">本图为示意，非实测数据</text>
</svg>
<figcaption>判据不是"感觉慢"，而是一个可量化的信号：<strong>首字节时间约等于总时长</strong>。下一节给出怎么量。</figcaption>
</figure>
<p>用 curl 的计时字段可以把这件事量化，不用凭感觉：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -N -o /dev/null -s <span class="se">\
</span></span></span><span class="line"><span class="cl">  -w <span class="s2">&#34;首字节 %{time_starttransfer}s / 总计 %{time_total}s\n&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$KEY</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{&#34;model&#34;:&#34;gpt-4o-mini&#34;,&#34;stream&#34;:true,&#34;messages&#34;:[{&#34;role&#34;:&#34;user&#34;,&#34;content&#34;:&#34;数到二十&#34;}]}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://your-domain/v1/chat/completions
</span></span></code></pre></div><ul>
<li><code>首字节 ≈ 总计</code> → 被完整缓冲了</li>
<li><code>首字节 ≪ 总计</code> → 流式正常</li>
</ul>
<p>另一个免费信号在响应头里：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -N -D - -o /dev/null -s ... <span class="p">|</span> grep -iE <span class="s1">&#39;content-length|transfer-encoding|content-type&#39;</span>
</span></span></code></pre></div><p><strong>出现 <code>Content-Length</code> 就说明响应被完整缓冲过</strong>——只有知道全部长度才能写出这个头。正常流式应该是 <code>Transfer-Encoding: chunked</code>（HTTP/1.1）或 HTTP/2 的分帧，加上 <code>Content-Type: text/event-stream</code>。</p>
<h2 id="三层二分排查法">三层二分排查法</h2>
<p>不要直接去猜哪一层，按下面这棵树走，两三条命令就能定位。核心思路是<strong>从最里层往外逐层加一跳</strong>，第一次出问题的那跳就是元凶。</p>
<figure>
<svg viewBox="0 0 780 350" role="img" aria-label="排查决策树：先用 curl -N 直连应用确认后端本身流式，再经 nginx，再经 CDN，每一步不流式就定位到该层" style="width:100%;height:auto;color:currentColor">
  <defs>
    <marker id="a2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor"/>
    </marker>
  </defs>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <rect x="180" y="14" width="270" height="38" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="315" y="31" text-anchor="middle">① curl -N 直连 127.0.0.1:3000</text>
    <text x="315" y="45" text-anchor="middle" opacity="0.6">绕过全部反代</text>
    <line x1="200" y1="52" x2="120" y2="88" stroke="currentColor" stroke-width="1.2" marker-end="url(#a2)"/>
    <text x="140" y="74" text-anchor="end" opacity="0.75">不流式</text>
    <rect x="10" y="90" width="200" height="38" fill="currentColor" fill-opacity="0.09" stroke="currentColor" stroke-width="1.3"/>
    <text x="110" y="107" text-anchor="middle">应用没有 flush</text>
    <text x="110" y="121" text-anchor="middle" opacity="0.6">改代码，见下节</text>
    <line x1="360" y1="52" x2="430" y2="88" stroke="currentColor" stroke-width="1.2" marker-end="url(#a2)"/>
    <text x="404" y="74" opacity="0.75">流式 ✓</text>
    <rect x="330" y="90" width="270" height="38" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="465" y="107" text-anchor="middle">② curl -N 经 nginx（内网 IP）</text>
    <text x="465" y="121" text-anchor="middle" opacity="0.6">加上反代这一跳</text>
    <line x1="350" y1="128" x2="230" y2="164" stroke="currentColor" stroke-width="1.2" marker-end="url(#a2)"/>
    <text x="252" y="150" text-anchor="end" opacity="0.75">不流式</text>
    <rect x="60" y="166" width="230" height="52" fill="currentColor" fill-opacity="0.09" stroke="currentColor" stroke-width="1.3"/>
    <text x="175" y="183" text-anchor="middle">nginx 缓冲</text>
    <text x="175" y="197" text-anchor="middle" opacity="0.6">proxy_buffering / gzip</text>
    <text x="175" y="211" text-anchor="middle" opacity="0.6">最常见的一档</text>
    <line x1="520" y1="128" x2="570" y2="164" stroke="currentColor" stroke-width="1.2" marker-end="url(#a2)"/>
    <text x="548" y="150" opacity="0.75">流式 ✓</text>
    <rect x="450" y="166" width="270" height="38" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="585" y="183" text-anchor="middle">③ curl -N 经公网域名</text>
    <text x="585" y="197" text-anchor="middle" opacity="0.6">加上 CDN 这一跳</text>
    <line x1="480" y1="204" x2="400" y2="242" stroke="currentColor" stroke-width="1.2" marker-end="url(#a2)"/>
    <text x="420" y="228" text-anchor="end" opacity="0.75">不流式</text>
    <rect x="250" y="244" width="200" height="38" fill="currentColor" fill-opacity="0.09" stroke="currentColor" stroke-width="1.3"/>
    <text x="350" y="261" text-anchor="middle">CDN 在缓冲</text>
    <text x="350" y="275" text-anchor="middle" opacity="0.6">去控制台配流式</text>
    <line x1="650" y1="204" x2="680" y2="242" stroke="currentColor" stroke-width="1.2" marker-end="url(#a2)"/>
    <text x="670" y="228" opacity="0.75">流式 ✓</text>
    <rect x="530" y="244" width="240" height="52" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="650" y="261" text-anchor="middle">服务端没问题</text>
    <text x="650" y="275" text-anchor="middle" opacity="0.6">查客户端：漏了 -N，</text>
    <text x="650" y="289" text-anchor="middle" opacity="0.6">或 SDK 未开 stream</text>
  </g>
  <text x="0" y="330" font-size="10" font-family="monospace" fill="currentColor" opacity="0.6">每一步都必须带 -N，否则测的是 curl 自己的缓冲</text>
</svg>
<figcaption>这套方法的价值在于<strong>每次只增加一跳</strong>，所以定位是确定的，不需要靠猜。灰底的三个是终点结论。</figcaption>
</figure>
<h2 id="nginx-侧的完整配置">nginx 侧的完整配置</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">location</span> <span class="s">/v1/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:3000/v1/</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 关掉响应缓冲 —— 解决问题的主开关
</span></span></span><span class="line"><span class="cl">    <span class="kn">proxy_buffering</span>    <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">proxy_request_buffering</span> <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">proxy_cache</span>        <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># gzip 压缩也会攒数据，流式 location 里必须关
</span></span></span><span class="line"><span class="cl">    <span class="kn">gzip</span>               <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># chunked 需要 HTTP/1.1；清空 Connection 让 nginx 复用上游连接
</span></span></span><span class="line"><span class="cl">    <span class="kn">proxy_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">proxy_set_header</span>   <span class="s">Connection</span> <span class="s">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 长生成会超过默认的 60s，放宽读超时
</span></span></span><span class="line"><span class="cl">    <span class="kn">proxy_read_timeout</span> <span class="s">600s</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">proxy_send_timeout</span> <span class="s">600s</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">proxy_set_header</span>   <span class="s">Host</span>      <span class="nv">$host</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">proxy_set_header</span>   <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>几个容易漏的点：</p>
<ul>
<li><strong><code>gzip off</code> 不是可选项。</strong> nginx 的 gzip 模块要攒够数据才能压缩，开着它等于重新引入缓冲。很多人只关了 <code>proxy_buffering</code> 就以为完事，结果症状减轻但没消失，就是 gzip 在作祟。</li>
<li><strong><code>proxy_read_timeout</code> 默认 60 秒。</strong> 长上下文生成很容易超，超了会收到截断的响应或 504，且现象酷似&quot;流式坏了&quot;。</li>
<li><strong><code>proxy_request_buffering off</code></strong> 管的是请求方向，上传大 prompt 时有用，和响应流式无关但顺手一起配。</li>
</ul>
<h3 id="不想改-nginx-配置用响应头">不想改 nginx 配置？用响应头</h3>
<p>如果你没有 nginx 的配置权限，或者不想为一个接口改全局配置，<strong>应用可以自己让 nginx 关掉缓冲</strong>——返回这个响应头：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">X-Accel-Buffering: no
</span></span></code></pre></div><p>nginx 识别这个头并对该响应禁用缓冲。这是 nginx 特有的机制，非常实用：<strong>开关从运维侧移到了应用侧</strong>，随代码走，不会因为换机器忘配而复发。</p>
<h2 id="caddy-侧">Caddy 侧</h2>
<p>Caddy 的 <code>reverse_proxy</code> 对 <code>text/event-stream</code> 会自动刷新，多数情况下开箱即用。如果确实需要强制：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddyfile" data-lang="caddyfile"><span class="line"><span class="cl"><span class="gh">your-domain.com</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">reverse_proxy</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">3000</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">flush_interval</span> <span class="s">-1</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>flush_interval -1</code> 表示每次写入立即刷新。另外 Caddy 默认会对文本类型启用压缩，如果显式配了 <code>encode gzip</code>，记得把流式路径排除掉，理由和 nginx 的 <code>gzip off</code> 一样。</p>
<h2 id="应用侧反代配对了也可能不流式">应用侧：反代配对了也可能不流式</h2>
<p>如果排查树的第一步就不通过，那是应用自己没有 flush。反代关掉缓冲只是&quot;不主动攒&quot;，它没法把应用攒着不发的数据变出来。Go 里的标准写法：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">stream</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">flusher</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">w</span><span class="p">.(</span><span class="nx">http</span><span class="p">.</span><span class="nx">Flusher</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;streaming unsupported&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusInternalServerError</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;text/event-stream&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Cache-Control&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;no-cache&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;X-Accel-Buffering&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;no&#34;</span><span class="p">)</span><span class="w"> </span><span class="c1">// 顺手把 nginx 也关掉</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">chunk</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">chunks</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;data: %s\n\n&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">chunk</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">flusher</span><span class="p">.</span><span class="nf">Flush</span><span class="p">()</span><span class="w"> </span><span class="c1">// 少了这行，前面全白配</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprint</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;data: [DONE]\n\n&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">flusher</span><span class="p">.</span><span class="nf">Flush</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>注意 <code>w.Header().Set</code> 必须在第一次 <code>Write</code> 之前调用，否则不生效——这是另一个安静失败的经典来源。</p>
<h2 id="相关阅读">相关阅读</h2>
<ul>
<li><a href="/posts/new-api-binary-vs-docker/">new-api 部署选型：2G 小内存机器上裸二进制比 Docker 省多少</a> —— 反代前面那一层，部署形态怎么选</li>
<li><a href="/posts/api-gateway-real-ip-ratelimit/">API 中转站的限流为什么不能信 X-Forwarded-For</a> —— 本文配置里那行 <code>X-Real-IP</code> 为什么不能省</li>
</ul>
]]></content:encoded></item></channel></rss>