一次 API 延迟排查:从 2 分钟到 314ms,我做了这 5 步
我自己做了一个知识库,问答上线时好好的,8 秒出答案。有一天突然变成 2-5 分钟,页面直接卡死,Ctrl+C 都按不动。 用户说"之前很快"。我花了两天排查,最后把一次问答压回 314ms。 全程 5 步,每一步都在缩小嫌疑范围。
1. 症状:8 秒变 2 分钟
Vue 版前端上线后,一次问答 2-5 分钟。旧 Jinja2 版同样慢。页面完全卡死,终端 Ctrl+C 无效------说明卡的是后端请求,不是前端渲染。
2. 第一步:排除 Vue CDN
Vue 通过 CDN 加载,浏览器要阻塞等脚本下载。国内访问 unpkg.com 极慢,怀疑它。
换了 npmmirror 国内镜像,还是慢。算一笔账:脚本加载再慢,几十秒也该超时;卡死 5 分钟,问题一定在后端。
这一步的收尾是顺手把 Vue 下载到本地 static/,彻底消除 CDN 依赖------不是主因,但值得做。
3. 第二步:模型名是诱因,不是主因
之前排查 DeepSeek 400 错误时,发现模型名从 deepseek-chat 变成了 deepseek-v4-pro。改成 v4-pro 后 API 能用了,但没注意 v4-pro 慢得多。
换成 deepseek-v4-flash,API 耗时缩到 5-16s(原来约 28s)。有效果,但解释不了 2 分钟------就算 API 要 16s,中间还有 1 分多钟不知道去哪了。
4. 第三步:加计时日志,锁死 Redis
在 ask() 里插入打印语句,输出每步耗时(当时的调试输出,修复后已删除):
ini
[计时] Redis查缓存: 10.3s ← 疑点!
[计时] ChromaDB检索: 11.1s ← 实际上只有 0.8s(含等待)
[计时] 调DeepSeek前: 32.7s ← 中间 21 秒去哪了?
[计时] DeepSeek返回: 45.1s
5 行 print 比 1 小时瞎猜管用。ChromaDB 检索实际只要 0.8s,但"Redis 查缓存"一项就吞了 10-21 秒,ChromaDB 和 DeepSeek 之间还莫名多了 20 秒------那又是另一处 Redis 调用。
5. 第四步:临时禁用 Redis,实锤
把所有 Redis 调用替换成返回 None 的空函数,彻底跳过。
结果:ChromaDB 0.6s、DeepSeek 16s、总耗时 16s。Redis 就是罪魁祸首。
6. 第五步:修 Redis 挂起 + 启动 Redis
根因有两层:
层一:socket 超时不可靠。 代码里设了 socket_connect_timeout,但 ping() 不尊重这个参数。Redis 容器没启动时,一次连接挂起 10-21 秒才报错。
层二:一个请求摸了 4 次 Redis。 我顺着 ask() 数了一遍源码(rag_engine.py):查缓存、读会话、存会话、存缓存,四处调用点。Redis 挂起时,每一处都等满超时。
修复方案:加一个全局熔断标记------连不上就锁 60 秒,期间直接跳过 Redis。以下是 redis_cache.py 的真实源码(逐字):
python
# 全局状态:避免每次请求都重试连 Redis
_redis_client = None
_redis_unavailable_until = 0.0 # 时间戳------在此之前不重试
def _get_client():
"""懒加载 Redis 连接------连不上 60 秒内不再重试"""
global _redis_client, _redis_unavailable_until
now = _time.time()
if now < _redis_unavailable_until:
return None # 刚才连不上,跳过
if _redis_client is not None:
try:
_redis_client.ping()
return _redis_client
except Exception:
_redis_client = None
try:
import redis
host = os.getenv("REDIS_HOST", "localhost")
port = int(os.getenv("REDIS_PORT", "6379"))
db = int(os.getenv("REDIS_DB", "0"))
password = os.getenv("REDIS_PASSWORD", None)
_redis_client = redis.Redis(
host=host, port=port, db=db, password=password,
socket_connect_timeout=0.5, socket_timeout=0.5,
)
_redis_client.ping()
_redis_unavailable_until = 0.0 # 连上了,重置标记
logger.info(f"Redis 已连接: {host}:{port}")
return _redis_client
except Exception:
logger.info("Redis 未连接------60s 内不再重试")
_redis_unavailable_until = now + 60 # 60 秒内不走连接逻辑
return None
Redis 本身也没在跑,顺手 docker start redis-test 启动。修好后:同问题第二次命中缓存 → 314ms。
7. 时间线对比
| 阶段 | 请求耗时 | 根因 |
|---|---|---|
| 原始状态(旧 deepseek-chat) | ~8s | 正常 |
| 模型名过期 + 改 v4-pro | 2-5 分钟 | Redis 挂起 10-21s/次 × 多处调用 + API 28s |
| 换 v4-flash | 1.9 分钟 | Redis 挂起仍未解决,API 降到 16s |
| 临时禁用 Redis | 16s | 只剩 DeepSeek API 时间 |
| Redis 修好 + 启动 | 5s(首次)/ 314ms(缓存命中) | 正常 |
8. 教训
- 外部依赖挂了不能拖垮主流程。 Redis 是缓存,应该"可用则加速,不可用则跳过"。连不上重试 21 秒,完全违背了加缓存的目的。
- socket_connect_timeout 不可靠。 Python redis 库的 timeout 参数不能保证所有操作都在超时内完成。应用层防御比库参数可靠------连不上就锁 N 秒。
- 每一个外部依赖都是延迟源。 一个函数里调了 4 次 Redis,每次都可能挂起。缓存、日志、监控这些"辅助功能"都能在关键时刻拖慢主流程。
- 计时日志是最快的排查工具。 每步打印耗时,一眼看出瓶颈。
- 关掉它,排查就变简单了。 怀疑 Redis 就把返回值设成 None,立刻确认。修好再打开。
9. 问题速查
| 问题 | 答案 |
|---|---|
| 为什么 socket 超时不可靠 | 部分 redis-py 操作(如 ping)不严格受 socket 超时约束,挂起仍可能超过 |
| 缓存挂了怎么保证不拖垮主流程 | 熔断:连不上锁 N 秒,期间直接跳过缓存 |
| 排查性能问题第一步做什么 | 加每步计时日志,用数据定位,不靠猜 |
一句话总结
一次问答从 8 秒变成 2 分钟,最后是缓存拖垮了主流程。外部依赖都要当"可能挂"来写代码------可用则加速,不可用则跳过,熔断 60 秒,主流程永远不让缓存牵着走。
下一篇写我的知识库为什么搜得准:英文 Embedding 换成中文模型后,检索精度差在哪,V3 增量索引怎么做的。