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 调用挪出事务

三个核心教训

  1. Nginx 默认配置是为普通 Web 接口设计的,反代 AI/大模型接口时必须显式配置超时。但 Nginx 往往只是放大器,不是根因——改完 Nginx 还要继续追问:后端为什么会慢到超时?
  2. Spring Boot 多数据源配置中,initializeDataSourceBuilder().build() 不读 hikari.* 子配置——这是一个写了等于没写的经典陷阱,用 actuator 验证一下就能发现
  3. @Transactional + 大模型调用 = 定时炸弹——事务里放一个 60 秒的 LLM 调用,10 个连接几秒钟就能锁光

第一次查到 Nginx 就收手,是"治好了症状";追到连接池才是"治好了病"。线上排查最怕的不是查不到,是查到一个看起来合理的原因就不再往下挖了。


如果你也在用 Nginx 反代 AI 服务 + Spring Boot 多数据源,建议现在就去查两件事:Nginx 的超时配置,以及 HikariCP 的 maximumPoolSize 是不是真的生效了。毕竟,连配置文件都会骗你。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐