AI 服务 502 雪崩排查:从 Nginx 超时到连接池耗尽,查了两次才找到真凶
AI 中台明明活得好好的,但调用方全是 502。第一次查到 Nginx,改了,以为结束了。三天后又炸了,才发现真凶不是 Nginx——是连接池。
一、问题现象
某天下午,告警群里开始密集刷屏:
【P1】BizException: 请求 AI 中台异常
【Error】: 三方调用异常: 请求 AI 中台异常,
异常原因(BadGateway: status 502 reading AiEndpoint#chatComplete)
【时间】: 2026-04-05 14:42:00
关键词:502 Bad Gateway。
第一反应:AI 中台挂了?赶紧上去看——日志正常在回复,服务存活没问题。
奇怪的是:同一秒钟请求就 502 了,AI 中台却没有这条请求的日志。请求压根没到后端?
二、第一幕:Nginx,你干的好事
2.1 应用日志
Consumer 的错误日志很明确,Feign 调用 AI 中台返回 502:
com.cloud.exception.ServiceException: 请求AI中台异常,
异常原因(BadGateway: status 502 reading AiEndpoint#chatComplete)
at AiServiceImpl.callAiCentral(AiServiceImpl.java:1255)
at AbstractChatTaskExecutor.processAiReply(AbstractChatTaskExecutor.java:221)
at AbstractChatTaskExecutor.execute(AbstractChatTaskExecutor.java:185)
但 AI 中台侧完全没有对应 requestId 的日志。请求在哪里被拦截了?
Consumer → AI 中台的调用链中间会经过 Nginx 反向代理,先看 Nginx。
2.2 Nginx Access Log
统计 502 响应的接口分布:
grep '"502"' ai-service_access.log \
| grep -o '"request_uri": "[^"]*"' \
| sort | uniq -c | sort -rn
6800 "request_uri": "/v3/api/agent/completions"
5200 "request_uri": "/v2/api/agent/completions"
780 "request_uri": "/v5/api/agent/completions"
650 "request_uri": "/v1/api/agent/round/completions"
370 "request_uri": "/v1/api/vision/summary-pic"
...
超过 1.3 万条 502,集中在 completions 接口(AI 聊天补全)。
2.3 Nginx Error Log
真正的线索在 error 日志里:
# 线索一:upstream 超时
2026/04/05 01:13:50 [error] *8750563865 upstream timed out
(110: Connection timed out) while reading response header from upstream,
upstream: "http://10.0.2.51:8878/v1/api/chat/dify"
# 线索二:所有后端都被踢了
2026/04/05 01:13:50 [error] *8750523839 no live upstreams
while connecting to upstream,
upstream: "http://ai_backend/v3/api/agent/completions"
两条日志发生在同一秒,画面逐渐清晰了。
2.4 Nginx 配置
upstream ai_backend {
server 10.0.2.29:8878 weight=10;
server 10.0.2.51:8878 weight=10;
keepalive 32;
}
location / {
proxy_pass http://ai_backend;
}
干干净净,什么超时、重试、失败策略都没配——全部走 Nginx 默认值。
2.5 原因分析:三个默认值联手作案
故障链路:
Consumer ──Feign──▶ Nginx ──proxy_pass──▶ AI 中台 (2 节点)
│
│ ① AI 接口等大模型返回,耗时 > 60s
│ ② Nginx 判定超时,返回 502
│ ③ 默认 max_fails=1,一次超时就踢掉该节点
│ ④ 两个节点各超时一次,同时被踢
│ ⑤ no live upstreams,后续所有请求立即 502
▼
502 雪崩
三个默认值的致命组合:
| 配置项 | 默认值 | 坑在哪 |
|---|---|---|
proxy_read_timeout |
60s | AI completions 接口等大模型返回,经常超过 60 秒,触发超时 |
max_fails |
1 | 超时 1 次就把节点标记为不可用,太激进 |
fail_timeout |
10s | 被踢后 10 秒才恢复探测,期间该节点不接收任何请求 |
max_fails=1 + 只有 2 个后端节点 = 一个节点超时一次就踢,两个节点各超时一次就全踢。
Nginx 的"保护机制"反而成了"自杀机制"——它以为自己在帮你隔离故障节点,实际上把所有健康的节点都隔离了。
AI 中台:我明明还活着啊!
Nginx:不,你已经"死"了(确信)。
2.6 修复 Nginx
upstream 配置:放宽失败策略
upstream ai_backend {
server 10.0.2.29:8878 weight=10 max_fails=3 fail_timeout=30s;
server 10.0.2.51:8878 weight=10 max_fails=3 fail_timeout=30s;
keepalive 32;
}
location 配置:加超时和重试策略
location / {
proxy_pass http://ai_backend;
# AI 接口耗时较长(等大模型返回),超时放宽到 180 秒
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 180s;
# 仅在连接失败或超时时切换到下一个后端
# 不对 502 重试(AI 接口非幂等,重试可能导致重复回复)
proxy_next_upstream error timeout;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
改动对照表:
| 配置项 | 改前(默认值) | 改后 | 为什么这么改 |
|---|---|---|---|
max_fails |
1 | 3 | 3 次失败才踢,避免偶发超时就踢掉健康节点 |
fail_timeout |
10s | 30s | 给后端更多恢复时间 |
proxy_read_timeout |
60s | 180s | AI 接口等大模型返回,60 秒远远不够 |
proxy_next_upstream |
含 error/timeout/502 等 | error timeout | 502 不重试,避免 AI 接口重复调用 |
proxy_next_upstream_tries |
无限制 | 2 | 最多尝试 2 个后端,快速失败 |
2.7 验证
# 1. 检查配置语法
nginx -t
# 2. 平滑重载(不中断现有连接)
nginx -s reload
# 3. 观察 error 日志是否还有 no live upstreams
tail -f /etc/nginx/logs/ai-service_error.ssl.log | grep "no live"
# 4. 统计 502 数量是否下降
watch -n 10 'grep "502" /etc/nginx/logs/ai-service_access.ssl.log | wc -l'
修改上线后,no live upstreams 错误消失,502 数量从日均 1.3w+ 降至个位数。
以为故事到这里就结束了?
三、第二幕:三天后,又炸了
3.1 502 又来了
改完 Nginx 后安稳了两天。第三天早上,告警群再次刷屏:
【P1】BizException: 请求 AI 中台异常,
异常原因(BadGateway: status 502 reading AiEndpoint#chatComplete)
【时间】: 07:43:36
一模一样的告警,一模一样的 502。
但这次不一样——排查 Nginx error 日志,没有 no live upstreams,说明 Nginx 改完之后确实没再踢节点。那 502 从哪来的?
紧接着,同事反馈了另一条报错:
[CRITICAL] HikariPool-1 - Connection is not available,
request timed out after 30000ms.
连接池? 这个方向之前完全没查过。
3.2 排查接口响应
查看告警时间段(07:40 ~ 07:47)的接口响应情况,发现大量接口响应时间飙升到 30 秒以上,多个 completions 接口几乎同时变慢。
进一步看连接池监控,07:40 到 07:47 这 7 分钟内,HikariCP 活跃连接数一直维持在满值,没有任何空闲连接。
新请求拿不到连接 → 等 30 秒超时 → AI 中台返回异常 → Nginx 转发 502 给调用方。
3.3 查看数据源配置
# application-prod.yaml
spring:
datasource:
url: jdbc:mysql://172.16.0.10:3306/ai_central?...
username: aiUser
password: ...
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 64 # 期望 64 个连接
biz:
datasource:
url: jdbc:mysql://172.16.0.10:3306/biz_db?...
username: bizUser
password: ...
driver-class-name: com.mysql.cj.jdbc.Driver
# ❌ 没有配 hikari
看起来主库配置了 max=64,但 MySQL 慢日志里找不到这两台 AI 中台的 IP,说明 SQL 本身不慢,问题不在 MySQL 侧。
3.4 真凶浮出水面:DataSourceConfig
@Bean
@Primary
public DataSource primaryDataSource() {
return primaryDataSourceProperties()
.initializeDataSourceBuilder()
.build();
}
关键发现:initializeDataSourceBuilder().build() 只读 url/username/password/driver-class-name 这几个顶层属性,完全不读 hikari.* 子配置。
也就是说——yaml 里写的 hikari.maximum-pool-size: 64 是个装饰品,从来没生效过。实际连接池大小走的是 HikariCP 默认值:
| 数据源 | 期望 | 实际 |
|---|---|---|
| spring.datasource (ai_central) | 64 | 10(HikariCP 默认) |
| biz.datasource (biz_db) | 未配置 | 10(HikariCP 默认) |
AI 中台一台机器只有 20 个数据库连接,承载并发能力极低。
3.5 雪上加霜:事务里调大模型
连接池小还不是最致命的。更致命的是——事务内调用了大模型接口。
业务数据源用了 JPA + @Transactional,而 JPA 事务的特性是:事务开始就拿连接,直到提交/回滚才释放。
如果某个 @Transactional 标注的方法在事务内调用了 LLM 接口(GPT/Claude/Gemini,30~60 秒才返回),数据库连接会被一直占用:
正常:事务开始 → 拿 conn → 查 DB(10ms) → 提交 → 还 conn ✅ 占用 10ms
异常:事务开始 → 拿 conn → 查 DB(10ms) → 调 LLM(60s) → 提交 → 还 conn ❌ 占用 60s+
10 个连接,每个被占 60 秒,高峰期几十个并发一起来——连接池瞬间被锁死。
3.6 完整故障链路
现在把两次排查串起来,真正的故障链路是这样的:
高峰期 AI 自动回复并发上来
↓
@Transactional 方法内调用大模型(60s+)
↓
数据库连接被长期占用,10 个连接快速锁死
↓
HikariPool: Connection is not available, request timed out after 30000ms
↓
AI 中台响应变慢 / 直接超时
↓
Nginx proxy_read_timeout 超时(之前是 60s,改后 180s)
↓
max_fails 达到阈值,节点被踢(改前 1 次就踢,改后需 3 次)
↓
no live upstreams → 502 雪崩
第一次改 Nginx 为什么"好了两天"? 因为把超时从 60s 放宽到 180s、max_fails 从 1 提高到 3,给连接池争取了喘息空间。但连接池只有 10 个的根因没变,等并发量再上来一点,180s 也扛不住了。
Nginx 配置不是根因,只是放大器——它把连接池耗尽导致的慢响应,放大成了全局雪崩。
MySQL 慢日志看不到 AI 中台 IP 完美印证了这一点:SQL 本身根本不慢,是应用端连接池被锁死了。
四、解决方案
4.1 第一层:Nginx 配置(已在第一幕修复)
放宽超时、提高容错、控制重试策略——详见 2.6 节。这是防御层,防止后端偶发超时引发雪崩。
4.2 第二层:修复连接池配置(根治)
修改 PrimaryDataSourceConfig.java,让 hikari 子配置生效:
import com.zaxxer.hikari.HikariDataSource;
@Bean
@Primary
@ConfigurationProperties("spring.datasource.hikari") // ← 关键:再绑定一次 hikari 子配置
public DataSource primaryDataSource() {
return primaryDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class) // ← 显式指定 HikariDataSource
.build();
}
同样修复业务数据源,并补齐 yaml 中的 hikari 配置:
spring:
datasource:
url: jdbc:mysql://172.16.0.10:3306/ai_central?...
hikari:
maximum-pool-size: 64
minimum-idle: 10
connection-timeout: 10000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
biz:
datasource:
url: jdbc:mysql://172.16.0.10:3306/biz_db?...
hikari:
maximum-pool-size: 32
minimum-idle: 5
connection-timeout: 10000
idle-timeout: 600000
max-lifetime: 1800000
4.3 第三层:把 AI 调用挪出事务(业务层根治)
排查所有 @Transactional 标注的方法,确认事务内没有调用 LLM 接口:
// ❌ 反模式:事务内调用 LLM
@Transactional
public void processChat(String userId) {
User user = userRepo.findById(userId); // 拿连接
String response = llmClient.chat(...); // 等 60 秒,连接被占用
chatRecordRepo.save(...); // 还连接
}
// ✅ 正确:拆开事务边界
public void processChat(String userId) {
User user = userService.findUser(userId); // 短事务,10ms 还连接
String response = llmClient.chat(...); // 无事务,不占连接
chatRecordService.saveRecord(...); // 短事务
}
4.4 修复优先级
| 优先级 | 动作 | 收益 |
|---|---|---|
| P0 | 修 DataSourceConfig,让 hikari 配置生效 | max 10 → 64,立刻能扛 6 倍并发 |
| P0 | 排查 AI 调用是否在事务内,拆开事务边界 | 根治连接占用问题 |
| P1 | 业务数据源加独立 hikari 配置 | 隔离两个数据源压力 |
| P1 | 加 HikariCP 监控(leak detection + Prometheus 指标上报) | 提前预警 |
| P2 | Nginx 配置(已完成) | 防御层,避免后端抖动放大为雪崩 |
五、举一反三
5.1 Nginx 反代慢接口要显式配置超时
不止 AI 接口,以下场景都需要注意 Nginx 超时和失败策略:
| 场景 | 特点 | 建议 proxy_read_timeout |
|---|---|---|
| AI 大模型接口 | 等模型生成,耗时 30s~180s | 180s ~ 300s |
| 文件上传/下载 | 大文件传输耗时 | 根据文件大小调整 |
| 报表导出 | 查询+渲染耗时 | 120s ~ 300s |
| WebSocket 长连接 | 需要长时间保持 | 3600s 或更长 |
| SSE 流式响应 | AI 流式输出 | 300s+,配合 proxy_buffering off |
通用原则:Nginx 反代的后端如果是慢接口(响应时间 > 30s),必须显式配置超时参数,不要依赖默认值。
5.2 Spring Boot 多数据源的 HikariCP 配置陷阱
DataSourceProperties.initializeDataSourceBuilder().build() 是 Spring Boot 多数据源配置中最常见的写法,但它不会自动读取 hikari.* 子配置。
验证方法——通过 actuator 确认实际池大小:
curl http://localhost:8080/actuator/metrics/hikaricp.connections.max
# 如果返回 value: 10 而不是你配的值,就说明配置没生效
或者看应用启动日志:
grep -i "HikariConfig\|maximumPoolSize" app.log | head -5
5.3 事务边界的黄金法则
@Transactional 里不要放慢操作——这条规则不只适用于 AI 接口:
| 不该放进事务的操作 | 原因 |
|---|---|
| 调用大模型(LLM) | 30~120s,连接被锁死 |
| 调用三方 HTTP 接口 | 网络不确定性,可能超时 |
| 发送邮件/短信 | IO 操作,可能阻塞 |
| 文件上传/下载 | 耗时不可控 |
原则:事务只包裹数据库操作,IO 操作放在事务外面。
六、总结
| 阶段 | 关键发现 |
|---|---|
| 第一幕(表层) | Nginx proxy_read_timeout=60s 对 AI 接口太短 + max_fails=1 导致节点被误踢 → 502 雪崩 |
| 修复 Nginx | 放宽超时 180s + max_fails=3 + 控制重试策略 → 好了两天 |
| 第二幕(根因) | HikariCP 连接池配置没生效(期望 64 实际 10)+ 事务内调 LLM 导致连接被长期占用 → 连接池耗尽 |
| 真正修复 | 修 DataSourceConfig 让配置生效 + 拆事务边界 + AI 调用挪出事务 |
三个核心教训:
- Nginx 默认配置是为普通 Web 接口设计的,反代 AI/大模型接口时必须显式配置超时。但 Nginx 往往只是放大器,不是根因——改完 Nginx 还要继续追问:后端为什么会慢到超时?
- Spring Boot 多数据源配置中,
initializeDataSourceBuilder().build()不读 hikari.* 子配置——这是一个写了等于没写的经典陷阱,用 actuator 验证一下就能发现 @Transactional+ 大模型调用 = 定时炸弹——事务里放一个 60 秒的 LLM 调用,10 个连接几秒钟就能锁光
第一次查到 Nginx 就收手,是"治好了症状";追到连接池才是"治好了病"。线上排查最怕的不是查不到,是查到一个看起来合理的原因就不再往下挖了。
如果你也在用 Nginx 反代 AI 服务 + Spring Boot 多数据源,建议现在就去查两件事:Nginx 的超时配置,以及 HikariCP 的 maximumPoolSize 是不是真的生效了。毕竟,连配置文件都会骗你。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)