事情本身不复杂,线索都在日志里。流式接口本地直连能逐字出来,接入 CDN 之后前端只收到一次完整响应,首字时间变成 9 秒,抓包看到 Content-Encoding: gzip 且只有一个 TCP 包带完整 body,回源域名的压缩开关是默认打开的。

第一反应是「回源响应缺 X-Accel-Buffering: no,边缘节点按普通 HTML 处理」,验证下来不成立。

排查方向其实有限,先把可能性排一排。gzip 压缩流需要攒够一块才能压,小分片被攒成大块才下发。另一种情况是CDN 默认对 text/event-stream 也做压缩,忽略它是流式协议。

不容易想到的是,缓存键没有区分 Accept 头,流式与非流式响应被同一条规则处理。

先确认回源响应头里有没有 gzip 与缓冲相关标记

curl -sI -H 'Accept: text/event-stream' https://cdn.example.com/v1/chat/completions | grep -iE 'content-encoding|accel|transfer'

然后在应用侧对流式路由显式关闭压缩

sed -i 's/GZipMiddleware(minimum_size=500)/GZipMiddleware(minimum_size=500, compresslevel=1)/' app.py

给流式响应补上禁止缓冲的响应头

python -c "print({'X-Accel-Buffering':'no','Cache-Control':'no-cache'})"

刷新边缘缓存后重新观察首字时间

curl -s -o /dev/null -w '%{time_starttransfer}\n' -H 'Accept: text/event-stream' https://cdn.example.com/v1/chat/completions

改完先别撤,确认这几项。响应头里不再出现 content-encoding: gzip,首字时间回到 1 秒以内,开发者工具里能看到多条增量事件而不是一次完整响应。

这里还有个坑。压缩和流式天然冲突,边缘节点上按路径单独关压缩比全局调整安全,改完记得等缓存刷新生效。同一份配置在测试环境跑通,不代表线上没事,两边的默认值经常不一样。

作者 9823189