KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
07 · 生产坑与可观测:为什么本地好好的线上会卡住 — keel 龙骨
这一章回答:SSE 的失败模式几乎都是"看起来正常但没有数据流动",怎么定位是哪一层的问题。
这一章回答:SSE 的失败模式几乎都是"看起来正常但没有数据流动",怎么定位是哪一层的问题。
前面几章已经分散讲过部分坑。这一章做一次归类与排查路径:当你收到"流式输出卡住了"的工单,按什么顺序排除。
一、四类高频坑
坑 1:缓冲(最高频)
现象:连接成功(200),但长时间收不到任何数据;服务端日志显示在推送。
可能位置:应用框架攒批、nginx proxy_buffering、gzip 压缩、CDN/网关智能加速。
排查:
# 绕过 nginx 直连应用,看是否实时
curl -N --max-time 10 http://127.0.0.1:8000/runs/1/stream | head
# 经过 nginx 再试一次,对比 TTFB
curl -N -o /dev/null -w 'TTFB %{time_starttransfer}s\n' https://your.domain/runs/1/stream
直连实时、经过 nginx 不实时 → 就是 nginx 缓冲/压缩问题(第 03 章配置)。
坑 2:超时被掐断
现象:固定时间点断开(60s、4min、30min 这种整数),客户端自动重连,看起来像"偶尔卡一下"。
可能位置:nginx proxy_read_timeout(默认 60s)、负载均衡空闲超时、CDN 边缘超时、应用 keep-alive 超时。
排查:记录断开的时间间隔是否规律;对比各层超时配置与心跳间隔。
规律:外层超时 > 内层超时 > 心跳间隔 × 2
坑 3:连接数限制
现象:页面其它请求变慢甚至卡死;部分用户的流打不开。
可能位置:浏览器同域 6 连接(HTTP/1.1)、nginx worker_connections、应用 fd 上限、负载均衡连接数配额。
排查:
ulimit -n # 应用进程 fd 上限
ss -s # 当前连接总数
nginx -T | grep worker_connections
浏览器侧:DevTools Network 看是否有请求处于 "Pending" 排队状态。
坑 4:移动端与网络切换
现象:手机息屏/切后台后流断;Wi-Fi 切 4G 后流断;回到前台后内容缺失。
原因:移动系统会挂起后台网络;IP 变化导致 TCP 连接失效。
应对:
- 客户端监听
visibilitychange,页面隐藏时主动关闭 SSE(省资源),回到前台时重连并走全量拉取(因为可能已经过了保留期); - 依赖
Last-Event-ID续播,但要准备好"续不上就重建"的回退; - 心跳间隔在移动端要更短(系统挂起判定通常 30s 左右)。
二、还有几个次高频的
| 坑 | 现象 | 解法 |
|---|---|---|
| gzip 攒批 | 事件成批到达,实时性变差 | 对 text/event-stream 关闭压缩 |
| 响应被缓存 | 多个用户收到同样内容,或收到旧内容 | Cache-Control: no-store + proxy_cache off |
Connection: keep-alive 头被代理吃掉 |
连接被提前关闭 | proxy_set_header Connection '' + proxy_http_version 1.1 |
| HTTP/2 下的多路复用 | 单条流阻塞影响其它请求(取决于实现) | 确认代理的 HTTP/2 行为;必要时该路径用 HTTP/1.1 |
| 生成器未消费导致 finally 不执行 | 断开后资源不释放 | 事件源加超时(如 BLOCK 5000)让出控制权 |
| 异常吞掉 | 流静默结束,无 error 事件 | 生成器外层 try/except,出错也要发 error 事件 |
三、排查路径(拿到工单后按这个顺序)
① 客户端能连通吗?
curl -N 直连应用 → 有数据? → 有:问题在中间层;无:问题在应用
② 是「没发」还是「没到」?
服务端日志有没有在推 → 在推:传输层问题(缓冲/压缩/超时)
没推:应用层问题(生成器卡住、事件源空)
③ 断开有规律吗?
固定间隔 → 超时配置;随机 → 网络/客户端
④ 是个别用户还是全部?
个别 → 客户端环境(代理、移动端、浏览器)
全部 → 服务端或网关配置
⑤ 最近有发布吗?
长连接问题 80% 与配置变更/发布相关(超时、缓冲、实例数)
第 ② 步是最关键的分水岭。它决定了你去查应用还是查网络。所以服务端必须有"已推送事件数"的日志/指标——没有这个,你永远分不清是"没发"还是"没到"。
四、指标与日志设计
必须有的四个指标(对应第 06 章):
sse_connections_active 当前连接数
sse_events_sent_total 已发送事件数(按 event 类型分)
sse_reconnects_total 重连次数
sse_first_byte_seconds TTFB 直方图
必须有的三条日志:
连接建立:run_id、user、last_event_id、来源 IP
异常断开:run_id、持续时长、已发事件数、断开原因(客户端/超时/背压/异常)
流结束: run_id、总事件数、结束原因(done/cancelled/error)
"已发事件数"是排查的灵魂:断开日志里带上它,你就能判断是"推了一半断了"还是"一条都没推"。
五、压测:长连接和短请求不一样
常规 HTTP 压测工具(wrk/ab)默认按"请求-响应"计数,不适合长连接。要注意:
| 误区 | 正确做法 |
|---|---|
| 用 QPS 衡量 | 用并发连接数 + 每条连接的事件速率衡量 |
| 客户端跑一会儿就结束 | 需要保持连接并持续统计到达事件 |
| 只看成功率 | 还要看 TTFB、事件到达间隔、重连次数 |
| 压测客户端连接数不够 | 确认客户端 fd 与端口范围足够(ulimit -n、临时端口) |
# 简易长连接压测脚本要点
async def one_client(i):
t0 = time.time(); received = 0; ttfb = None
async with session.get(url) as resp:
async for line in resp.content:
if ttfb is None: ttfb = time.time() - t0
received += 1
if time.time() - t0 > DURATION: break
return ttfb, received
# 统计:并发 N 个客户端,聚合 TTFB 分位、每连接事件数、断连次数
关键验收:并发爬升时,TTFB 不应随连接数增长(增长说明缓冲或排队),事件到达间隔应保持稳定。
动手:可观察结果
| 产出 | 判断标准 |
|---|---|
| 一份排查 Runbook | 五步路径,每步给出判定命令与结论分支 |
| 缓冲复现与修复 | 打开/关闭 proxy_buffering 的 TTFB 对比(如 8s → 60ms) |
| 超时复现 | proxy_read_timeout=60 下第 61 秒断开的证据(日志时间戳) |
| 四项指标仪表盘 | 连接数、事件数、重连次数、TTFB 四个指标可观测 |
| 一次长连接压测 | 并发 100/500/1000 三档的 TTFB P50/P99 与每连接事件数 |
| 移动端验证 | 息屏 3 分钟后的表现与恢复路径(续播或全量重建) |
完成标志:收到"流式卡住"的工单,你能在 15 分钟内判断出是"没发"还是"没到",并定位到具体是哪一层配置。
故障注入
| 注入方式 | 观察 |
|---|---|
proxy_buffering on(默认) |
TTFB 变化、curl -N 是否卡住 |
开启 gzip on 且不限类型 |
事件是否成批到达 |
proxy_read_timeout 60 |
第 61 秒是否断开(日志时间戳证据) |
打开 proxy_cache |
是否收到旧内容/他人内容 |
| HTTP/1.1 开 7 条连接 | 第 7 条是否排队(浏览器 Network 面板) |
| 手机息屏 3 分钟 | 是否断开;回前台后续播还是重建 |
| 生成器里吞掉异常 | 流是否静默结束(无 error 事件) |
| 用 wrk 压 SSE | 指标是否失真(验证工具选型) |
自测题
- "连接成功但没数据"的四个可能位置是什么?怎么逐个排除?
- 排查时最关键的分水岭是哪一步?为什么服务端必须记录"已发事件数"?
- 移动端有哪些特有的失败模式?客户端应该怎么应对?
- 长连接压测与常规 HTTP 压测的三个区别是什么?
- 压测时哪个指标最能暴露"被缓冲"?为什么?