AI写的系统出现504怎么办?接口超时和数据库慢查询排查

打开列表没问题,导出年度报表等了很久却返回504。有人把Nginx超时改成十分钟,错误暂时消失,随后发现更多用户开始排队。

504表示网关没能及时获得所需上游响应。先找时间耗在哪个阶段,再决定优化查询、限制任务规模或调整超时。延长等待不会让数据库自动变快,也不能保证已经超时的写操作没有执行。

本文以Nginx、Spring Boot和MySQL为例,围绕"年度导出约30秒后失败"的场景,从耗时日志走到任务化改造与回归验收。所有耗时数字都是诊断示例,代码为局部片段,阈值应按业务目标和实际容量制定,不代表已经在你的系统完成压力测试。

目录

  1. 先区分502与504
  2. 在入口日志里分开记录耗时
  3. 慢SQL先看执行计划
  4. 超时配置不是任务总时长开关
  5. 大报表适合改成任务
  6. 确定性能验收的范围
  7. 案例拆解:报表导出为什么总在30秒左右失败
  8. 异步导出最小设计:不仅是加一个线程
  9. 用同一组样本验收,避免"优化后感觉快了"

一、先区分502与504

502强调上游响应无效;504强调等待超时。连接阶段超时也可能返回504,所以不能把504简化成"已经成功连上数据库但SQL慢"。

图1:502侧重无效上游响应,504侧重等待超时,具体超时阶段仍需通过代理日志确定。

记录Nginx错误日志是在连接、发送还是读取上游时超时。同时查看请求是否经过CDN、负载均衡和另一个网关,它们可能拥有更短的等待限制。

二、在入口日志里分开记录耗时

下面的日志格式定义放在http上下文,再由站点引用:

nginx 复制代码
log_format timing '$request_id $request_method $uri status=$status '
                  'upstream=$upstream_status rt=$request_time '
                  'connect=$upstream_connect_time '
                  'header=$upstream_header_time '
                  'response=$upstream_response_time';

它不记录查询参数、Cookie和Authorization。应用若要关联请求ID,需要由可信入口传递并接入应用日志。发生重试时,上游变量可能包含多个值,不能当作单个数字分析。

图2:分别观察连接、等待响应头和接收响应的耗时,才能区分连接慢与后台处理慢。

连接耗时高时先查上游可达性和连接队列;连接快但响应头迟迟不来时,查应用执行、依赖等待和线程排队。总响应耗时大但持续返回数据,又与完全没有数据返回的情况不同。

三、慢SQL先看执行计划

sql 复制代码
EXPLAIN
SELECT id, name, created_at
FROM project
WHERE company_id = 1001
  AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 50;

这是普通EXPLAIN,不执行结果查询;表与字段须按项目替换。先确认是否扫描过多行、排序方式和已有索引。不能只凭一个查询片段就决定新增索引,还要看数据分布、写入成本和其他SQL。

EXPLAIN ANALYZE会执行语句,不应对未知大查询直接在生产尝试。慢查询记录也要结合采样窗口和数据脱敏,避免把业务参数直接公开。

图3:SQL执行、连接池排队、锁等待和外部调用需要分开分析,接口慢不一定是SQL本身慢。

连接池等待、数据库锁等待和SQL执行时间要分开。有时SQL本身很快,但线程排队拿不到连接;扩大超时只会让队伍更长。

四、超时配置不是任务总时长开关

示例片段放在目标代理location中:

nginx 复制代码
proxy_connect_timeout 3s;
proxy_send_timeout 15s;
proxy_read_timeout 30s;

以上是说明用途的样例值。proxy_read_timeout限制相邻两次读取上游数据之间的等待,并不是整个请求的总时长。应用HTTP客户端、数据库查询和连接池也有各自的超时,应该结合整体响应预算设计。

若网关30秒返回504,后台任务却继续执行两分钟,客户端立刻重试可能造成重复处理。应核对取消传播、幂等机制与结果查询能力,而不是把错误页面当成事务回滚证据。

五、大报表适合改成任务

图4:长任务将提交、可靠受理、后台执行和结果查询分开,返回任务标识不代表任务已经完成。

较大的导出可以采用"提交任务---返回任务ID---查询进度---下载结果"的方式。接口可以返回202与任务标识,但必须有实际持久化任务和后续查询机制支撑。

任务至少记录提交人、参数摘要、状态、进度、失败原因和结果有效期。下载仍要验证用户权限;任务列表和结果链接不能仅凭ID访问。重复提交还需明确复用还是新建,避免异步队列继续堆积。

六、确定性能验收的范围

图5:用实际数据规模和目标并发验证优化效果,同时核对超时后的写入结果与重试保护。

检查项 合格证据
耗时阶段 确认连接、排队或执行瓶颈
真实规模 按目标数据量验证,非空表测试
并发 正常峰值下延迟与错误率可接受
超时后果 写操作结果可以核对
任务模式 状态、失败和下载权限完整

记录优化前后的样本规模、并发、P95延迟和错误率,才能判断变化是否有效。没有做容量测试时,就说明是配置审查与小样本验证,不把它描述成压力测试结论。

下面以导出为例,把入口超时、后台执行和用户取得结果三个时间点分开检查。

七、案例拆解:报表导出为什么总在30秒左右失败

设想一个教学场景:项目列表很快,年度导出却在约30秒后504,后台过了一会儿还打印"导出完成"。这意味着用户等待结束与后台任务结束可能不是同一时刻。

先记录失败发生在哪层入口。如果前方CDN先超时,单独调整源站Nginx并不能让浏览器多等一分钟。时间"恰好30秒"是查配置的线索,不是数据库慢的证明。

1. 让耗时日志真正用于该接口

前文的log_format仅声明格式,还需在实际处理路径中启用:

nginx 复制代码
# 合并至已有server;示例假设后台保留/api前缀
location /api/ {
    access_log /var/log/nginx/api_timing.log timing;
    proxy_set_header X-Request-ID $request_id;
    proxy_pass http://127.0.0.1:8080;
    proxy_connect_timeout 3s;
    proxy_read_timeout 30s;
}

这些数值只是教学边界,不是通用生产建议。应用日志需接入同一请求ID;否则入口有ID、后台没有,仍无法可靠关联并发请求。

示意记录:

text 复制代码
rid=demo-export status=504 connect=0.002 header=- response=30.004
error: upstream timed out (...) while reading response header from upstream

示意字段名可按前文格式增加rid,实际输出格式与平台版本有关。这里最关键的证据是"连接很快,但等待响应头超时",所以继续看应用排队、生成报表和依赖调用。

header=-不是"耗时为零"。它表示没有可用的该项时间,不能按0加入平均值。多个上游尝试也可能产生多组值,分析时必须保留尝试顺序。

2. 在应用里拆分导出耗时

至少区分以下几个阶段,而不是只记录方法总耗时:

阶段 要测量的内容 常见误判
获取数据库连接 等待连接的时间 误认为SQL执行慢
查询并取回数据 执行、网络和结果读取 只看SQL服务端执行时间
组装与写文件 内存处理、压缩、磁盘写入 忽略文件生成成本
上传结果 对象存储或远端调用 把外部等待算作数据库慢
返回响应 是否已提交响应头或断开 误以为后台完成等于用户收到

下面是Java 17中的局部计时片段,放入原有导出方法,已有对象和方法按工程替换:

java 复制代码
long started = System.nanoTime();
try {
    return exportService.export(query);
} finally {
    long elapsedMs =
            java.util.concurrent.TimeUnit.NANOSECONDS
                    .toMillis(System.nanoTime() - started);
    log.info("export finished requestId={} elapsedMs={}",
            requestId, elapsedMs);
}

它只测同步调用结束时间;如果export内部只是把任务交给线程池,测到的是提交耗时,不是实际任务耗时。细分阶段应在真正执行的位置计时,并避免记录报表明细和敏感查询参数。

3. 根据耗时选择修复,而不是统一调大超时

如果SQL扫描范围过大,结合前文EXPLAIN检查条件与已有索引;如果获取连接就等待很久,检查长事务和连接释放;如果数据库已经返回而生成文件耗时很长,应控制数据规模或改任务模式。

在隔离测试环境,可用代表性数据对比两个实现,但不要把空表或几十条数据的结果当成大报表性能结论。索引变化还会影响写入、存储与其他查询,需要一起评估。

八、异步导出最小设计:不仅是加一个线程

直接new Thread启动任务后返回"成功",会引入新的问题:进程重启任务丢失、异常不可见、重复提交无限排队,以及用户无法确认结果。

一个最小任务模型可以包含:

字段 用途
task_id 查询任务的稳定标识
tenant_id、created_by 数据隔离和归属
idempotency_key 同一提交的去重依据
request_hash 检查同一幂等键是否对应相同参数
status PENDING、RUNNING、SUCCEEDED、FAILED
result_key、expires_at 结果位置与保留期限
error_code 脱敏的失败分类
created_at、updated_at 排查与超时恢复

幂等键唯一约束的作用域应包含业务归属,例如租户和提交人;同一键传入不同参数应明确拒绝,不能悄悄返回另一份报表。参数本身还要有数量和时间范围限制。

接口可以这样约定:

text 复制代码
POST /api/export-tasks
  -> 202 Accepted
  -> Location: /api/export-tasks/{taskId}
  -> 返回任务ID和PENDING状态

GET /api/export-tasks/{taskId}
  -> 返回当前任务状态,校验租户和提交人权限

GET /api/export-tasks/{taskId}/file
  -> 任务成功且未过期才允许下载,仍然校验权限

202只表示已经受理,不能代替数据库中的任务记录。任务表加消息队列时,还要处理"任务落库成功但消息发送失败"的一致性问题,可以评估事务事件记录或可靠扫描投递机制。

后台工作者应通过原子认领避免重复执行,配合租约、超时回收或明确失败策略。对于可能重复执行的任务,结果保存也要有幂等规则。以上是设计边界,不是一张任务表就自动具备的能力。

九、用同一组样本验收,避免"优化后感觉快了"

不要只比较两次手动点击。固定数据范围、并发数和缓存条件,分别记录:

  • 普通查询的P95延迟和错误率;
  • 导出受理接口的响应时间;
  • 任务排队时间、实际执行时间和成功率;
  • 文件条数、权限、下载有效期与内容完整性;
  • 工作进程重启后任务如何恢复。

可以先把以下表格填实,再决定是否发布:

用例 检查结果
原来超时的数据范围 能完成;结果正确
多人同时提交 队列有界;普通业务没有被拖垮
重复点击提交 按设计复用或拒绝,不无限新增任务
工作进程中断 任务可恢复或明确失败,不永久RUNNING
越权查询任务ID 无法看到或下载他人结果
文件已过期 明确提示,不返回失效或错误文件

504修复后的目标不是"所有请求永远不超时",而是合理范围内及时完成,超限任务有明确处理路径,失败之后结果可确认。

参考资料

相关推荐
苏三说技术2 小时前
Kafka已正式接入AI
后端
IT_陈寒2 小时前
Redis的Set操作居然能把我的服务整挂了?
前端·人工智能·后端
momo061173 小时前
Redis新手入门 -- 学习笔记
redis·后端
井云AI3 小时前
vendor 资源包是什么:井云 DSH 客户端打包为什么离不开它
后端·智能体小程序
2601_962218613 小时前
万象生鲜系统订单全生命周期状态同步技术实现业务可视
大数据·数据库·人工智能·python·算法
重生之我是Java开发战士3 小时前
【Spring Cloud】Nacos注册中心
后端·spring·spring cloud
Wang's Blog3 小时前
Java框架快速入门:Spring Security+OAuth2之用户注册与唯一性校验实现
java·数据库·spring
程序员20073 小时前
AI 编程工具链碎片化?基于多端兼容的统一 Agent Rules 架构实践
后端
程序员20074 小时前
拒绝 AI 代码“暗度陈仓”:工程级 AI 编码边界与防护规则设计
后端