用户反馈问答很慢 排查思路

作者:old wang 发布时间: 2025-05-16 阅读量:14 评论数:0

SSE 问答和普通接口不一样,用户感受最明显的是两个时间:一个是 排队等待时间,另一个是 模型首包时间。如果用户 30 秒才看到第一个字,不一定是模型慢,也可能是前面排队太久、检索慢、Rerank 慢、Prompt 太长、线程池打满,或者 SSE 写出阻塞。

第一,看 Redis 等待队列


先看当前队列长度、用户请求的入队时间、等待时长、排队位置。如果队列很长,说明问题出在入口并发控制或下游处理能力不足。

如果用户“一直排队”,我第一步不会看模型,而是先看他有没有拿到执行资格。

我会看:

当前队列长度

用户 requestId 是否还在 ZSET 里

入队时间

等待时长

排队位置

队列里最老请求的时间

是否有大量过期 requestId 残留

如果发现队列很长,说明系统整体处理能力不足,可能是模型慢、线程池满、检索慢导致消费速度下降。

如果队列不长但用户一直排队,就要怀疑队列状态异常,比如队首请求已经断开但没清理,或者信号量 permit 被占满。

排查时,我会先查用户 requestId 的 rank,再看队列中排在前面的请求是否已经超时或取消。

优化上,可以定期清理过期队列项,给等待请求设置最大等待时间,前端展示排队位置和超时提示。

这里的取舍是:排队能保护系统,但不能无限排队。所以我会设置等待上限,超过就明确拒绝,而不是让用户一直等。

第二,看 Redisson 信号量占用


如果队列不长但请求一直排队,要看执行许可是不是被占满,以及有没有许可没有释放。比如用户关闭页面、SSE 异常、服务重启后,permit 没释放,就会导致后续请求一直拿不到执行资格。

如果 Redis 队列里一直有人排队,但执行中的请求数看起来不高,或者 permit 一直显示被占满,就要怀疑信号量没有释放。

重点看:

当前已占用 permit 数

剩余 permit 数

执行中 requestId 列表

requestId 对应 SSE 是否还活着

permit 是否设置过期时间

onCompletion / onTimeout / onError 是否释放

常见问题是:用户关闭浏览器、刷新页面、网络断开,SSE 连接已经没了,但服务端没有释放 permit。这样后续请求永远排队。

排查时,我会通过 requestId 查对应状态:WAITINGRUNNINGDONEFAILEDCANCELED。如果状态已经结束但 permit 还占用,就说明释放逻辑有问题。

优化上,要做主动释放和过期兜底:

SSE 正常完成:release permit

SSE 超时:release permit

SSE error:release permit

客户端断开:release permit

服务异常:靠可过期 permit 兜底

这里的取舍是:只靠过期时间会导致资源释放不及时,只靠主动释放又怕异常路径漏掉。所以两层都要做。

第三,看 线程池是否打满


看问答线程池、检索线程池、模型调用线程池、SSE 推送线程池的 activeCount、queueSize、rejectCount。如果线程池满了,就要看线程栈卡在哪里。

RAG 链路里可能有多个线程池:

问答主流程线程池

多通道检索线程池

模型调用线程池

Rerank 线程池

SSE 推送线程池

Trace 异步落库线程池

文档入库消费线程池

我会看每个线程池的:

activeCount 活跃线程数

poolSize 当前线程总数

queueSize 队列积压数

rejectCount 拒绝任务数

taskDuration 任务执行耗时

largestPoolSize 历史最大线程数

如果队列持续增长,说明任务提交速度超过处理速度。然后用 jstack 看线程到底卡在哪里:

卡在向量库查询

卡在数据库连接池

卡在 Redis

卡在模型 HTTP 调用

卡在 Rerank

卡在 SSE send

实际使用中容易出现的问题是,只看到线程池满就加线程。这样如果根因是模型服务慢,加线程只会让更多请求同时打模型,整体更慢。

优化上,要按任务隔离线程池。入库任务不能和问答任务抢资源,Trace 落库不能拖慢主链路,Rerank 要有短超时。

这里的取舍是:线程池是隔离资源的,不是无限放大并发的。所以我会先看线程卡点,再决定是扩线程、限流还是降级。

第四,看 检索耗时


RAG Trace 里看多通道检索耗时,是意图定向检索慢,还是全局向量检索慢,还是权限过滤、数据库查询、向量库查询慢。

我会在 Trace 里看:

问题改写耗时

意图识别耗时

权限范围计算耗时

意图定向检索耗时

全局向量检索耗时

多问拆分召回耗时

候选合并去重耗时

如果检索慢,常见原因有:

知识库范围太大

权限过滤条件复杂

TopK 太大

向量库索引不合理

向量库连接池不足

数据库连接池打满

多问拆分导致检索次数过多

排查时,我会先看慢的是哪个通道。如果只有全局向量检索慢,就看向量库;如果权限过滤慢,就看数据库索引和权限模型;如果多问拆分慢,就看子问题数量是否过多。

优化上,可以缩小知识库范围、预过滤权限、控制 TopK、优化向量索引、减少无效通道、给单通道设置超时。

这里的取舍是:检索范围越大,召回可能更全,但延迟和噪声也更高。所以不能所有问题都全局检索,要先按权限和知识库范围收缩。

第五,看 Rerank 耗时


如果候选 chunk 太多,Rerank 模型慢或超时,会拖慢首包。Rerank 应该有短超时,失败后允许跳过,不能无限等。

如果用户 30 秒才出首字,而检索很快、模型还没开始,那我会重点看 Rerank。

Rerank 慢的常见原因:

进入 Rerank 的候选 chunk 太多

Rerank 模型本身慢

Rerank 服务限流

批量输入过大

没有设置短超时

排查时,我会看:

Rerank 输入候选数量

Rerank 耗时

是否超时

是否触发降级

Rerank 前后排序

如果 Rerank 超过阈值,应该直接降级为原始检索排序,而不是一直等。

优化上,可以限制进入 Rerank 的候选数量,比如先每个通道 topK,再合并去重,再只取前 N 个做 Rerank。

这里的取舍是:Rerank 能提高准确率,但会增加延迟。所以我不会让 Rerank 无限等待,应该短超时、可跳过。

第六,看 模型首包耗时


首包慢是 30 秒才出字的高频原因。要看 Prompt token 长度、模型路由选择的是哪个模型、是否触发 fallback、模型健康状态、供应商是否限流。

如果 Trace 里显示检索和 Rerank 都很快,但模型调用后很久才有第一个 token,那问题大概率在模型层。

我会看:

选中的模型

Prompt token 长度

上下文 chunk 数量

模型首包耗时

模型总耗时

是否触发 fallback

模型健康状态

是否供应商限流

常见原因:

Prompt 太长

模型本身慢

模型服务排队

供应商限流

网络抖动

主模型健康状态异常

优化上,可以做:

压缩 Prompt

减少无关 chunk

控制最终 topN

首包超时切备用模型

模型健康检查

慢模型降权

按问题类型选择模型

这里的取舍是:强模型效果好但可能慢,快模型质量可能差。所以我不会所有问题都走最强模型,而是按问题复杂度和实时性要求做模型路由。

第七,看 SSE 写出是否阻塞或异常


如果模型已经返回 token,但前端收不到,要看 SSE send 是否阻塞、客户端是否断开、网络是否异常、是否出现 broken pipe、timeout。

如果模型日志显示 token 已经返回,但前端没有显示,就要看 SSE。

重点看:

SSE send 是否抛异常

客户端是否断开

是否 broken pipe

是否 timeout

是否网络代理缓冲

是否响应头正确

是否 emitter 没有 flush

如果 SSE 写出阻塞或异常,没有正确释放资源,还会影响后续请求排队。

排查时,我会看同一个 traceId 的事件序列:

WAITING

START

MESSAGE

ERROR

DONE

如果只有 START 没有 MESSAGE,要看模型首包。
如果有 MESSAGE 但前端没显示,要看网络和前端 EventSource。
如果 ERROR 后没有 complete,要看资源释放逻辑。

优化上,统一封装 SSE sender,所有异常路径都 completeWithError,并释放 permit、清理上下文、更新请求状态。

这里的取舍是:SSE 提升体验,但长连接状态管理更复杂。所以我不会把它当普通 HTTP 返回处理,而是必须管理断连、超时和资源释放。

第八,看 基础依赖资源


包括 Redis、向量库、模型服务、数据库连接池、RocketMQ、机器 CPU、内存、GC。如果数据库连接池打满,权限查询和会话查询都会慢;如果 Redis 慢,排队和限流状态也会异常;如果向量库慢,检索节点会变慢。

容易出现的问题是:只看接口总耗时,但不知道时间花在哪一步。RAG 链路长,如果没有 Trace,很容易误判。比如用户说“AI 慢”,但实际可能是排队 25 秒,模型首包只用了 2 秒;也可能是检索 1 秒、Rerank 20 秒、模型 5 秒。

优化上,可以从几个方向处理:

排队慢:调整并发阈值、缩短等待超时、优化 permit 释放、增加用户侧排队提示

线程池满:按任务隔离线程池,限制队列长度,排查线程卡点

检索慢:优化权限过滤、向量索引、TopK、知识库范围

Rerank 慢:限制候选数量,设置短超时,失败跳过

模型首包慢:压缩 Prompt、减少无关 chunk、模型路由、fallback、首包探测

SSE 异常:断连释放资源,统一 onError/onTimeout/onCompletion 清理

依赖慢:数据库连接池、Redis、向量库、模型服务分别做监控和告警

我会分别看:

Redis

队列 ZSET 操作是否慢

连接池是否满

Lua 执行是否耗时

Redis 是否有慢命令

向量库

检索耗时

TopK

过滤条件

collection / table 数据量

连接池

索引状态

模型服务

首包耗时

失败率

超时率

限流错误

当前模型健康状态

数据库

连接池 active / idle / wait

慢 SQL

权限查询耗时

会话查询耗时

Trace 落库耗时

实际使用中容易出现的问题是,数据库连接池打满后,很多节点都会慢,比如会话读取、权限过滤、Trace 写入都会受影响,看起来像整个系统都慢。

排查时,我会把依赖指标和 Trace 节点耗时对齐,看哪个依赖变慢和哪个节点变慢同时发生。

优化上,依赖层都要有超时、连接池监控、慢查询监控和隔离策略。

这里的取舍是:下游慢时不能盲目放大并发,否则会雪崩。所以我会优先保护下游,必要时拒绝或降级。

评论