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 连接失效。

应对:

二、还有几个次高频的

坑 现象 解法
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 指标是否失真(验证工具选型)

自测题

  1. "连接成功但没数据"的四个可能位置是什么?怎么逐个排除?
  2. 排查时最关键的分水岭是哪一步?为什么服务端必须记录"已发事件数"?
  3. 移动端有哪些特有的失败模式?客户端应该怎么应对?
  4. 长连接压测与常规 HTTP 压测的三个区别是什么?
  5. 压测时哪个指标最能暴露"被缓冲"?为什么?

进入 keel 阅读