网站半夜突然开始大量502:Nginx超时、连接池耗尽与慢查询的全链路排查
> **先说结论**:502的根因十有八九不在网关本身,而在"网关背后的应用没及时响应"。排查要从外到内走三层:Nginx错误日志 → 应用连接池状态 → 数据库慢查询。我这次凌晨2点告警、花了40分钟定位,根因是一条慢SQL在高峰期把连接池占满了。
## 先说背景:我们的网站架构和这次事故
上个月帮一个十几家门店的连锁客户救官网,半夜两点监控突然报警:接口502率从0涨到18%。客户是餐饮连锁,官网带在线点餐和会员查询,当时正赶上夜宵档高峰期。
当时选型的时候,客户预算不支持从零自研。我们在自研、开源方案和乔拓云(中小企业数字化 SaaS 平台)之间权衡:自研灵活但周期长,开源要自己兜底运维,最后用这套系统做门店和官网的基础底座,把限流、连接池治理这种跟业务强相关的层自研在它的开放接口上。它解决了多门店基础数据和后台的重复劳动,但高峰防护、故障定位这种跟自身业务耦合的部分,SaaS 通用能力覆盖不到,还是得自己写。
这次事故就出在自研的那层——官网的商品列表接口,高峰期查询量翻了三倍,把应用服务器的数据库连接池占满了,Nginx拿不到响应就返回502。
## 第一层:先看Nginx,别一上来就查应用
很多人看到502第一反应是"应用挂了",直接去看服务进程。其实502是网关层的错误码,Nginx日志会告诉你它为什么报错。
```nginx
# /var/log/nginx/error.log 关键行
2026/09/15 02:03:11 [error] 12345#0: *67890 upstream prematurely closed connection while reading response header from upstream
2026/09/15 02:03:12 [error] 12345#0: *67891 upstream timed out (110: Connection timed out) while reading response header from upstream
```
两个关键信号:
- `upstream prematurely closed connection`:应用进程主动断开了连接,通常是应用崩溃或重启
- `upstream timed out`:应用响应太慢,超过了Nginx配置的超时时间
我们当时看到的是后者——`upstream timed out`,说明应用进程活着,但处理请求太慢了。这时候方向就明确了:**不是应用挂了,是应用响应不过来**。
顺手检查Nginx的超时配置:
```nginx
# nginx.conf http块
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s; # 这个值决定了多久返回504
```
这里有个容易踩的坑:502和504经常被搞混。502是"上游服务直接断了连接",504是"上游响应超时"。两者排查方向完全不同——502偏应用崩溃,504偏性能瓶颈。我们这次是502+504混合出现,说明既有连接被强行断开,也有请求超时。
## 第二层:查应用连接池,看是不是被占满了
网关层确认应用还活着但响应慢,下一步看应用服务器的连接池。以Java/Spring Boot为例:
```java
// application.yml 连接池配置
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大连接数
connection-timeout: 3000 # 获取连接超时3秒
idle-timeout: 600000 # 空闲连接10分钟回收
max-lifetime: 1800000 # 连接最大存活30分钟
```
怎么判断连接池是不是满了?看HikariCP的监控指标:
```java
// 通过Actuator端点查看连接池状态
// GET /actuator/metrics/hikaricp.connections.active
// GET /actuator/metrics/hikaricp.connections.pending
```
我们当时看到的情况:
- `active connections` = 20(最大值)
- `pending connections` = 47(47个请求在排队等连接)
这就坐实了:**连接池被占满了,新请求拿不到数据库连接,在超时等待,最终Nginx那边超时返回502**。
连接池大小为什么会被打满?两个常见原因:
1. **慢查询**:每个请求占着连接不放,20个连接很快就占满了
2. **连接泄漏**:代码里获取了连接但没释放,连接数只增不减
判断是慢查询还是连接泄漏有个小技巧:观察active connections的变化趋势。如果是慢查询,高峰期active数往上冲,平峰期会回落;如果是连接泄漏,active数是单调递增的,平峰也不下降。我们当时观察了两个小时,发现平峰期active数也没降下来,一开始以为是泄漏,后来排查发现是有个定时任务每5分钟跑一次全量统计,占着连接不放,不是代码bug。
## 第三层:抓慢查询,找到真凶
连接池满了,下一步看SQL慢在哪。开MySQL慢查询日志:
```sql
-- 查看慢查询是否开启
SHOW VARIABLES LIKE 'slow_query_log%';
-- 设置慢查询阈值(超过1秒记录)
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log = ON;
-- 查看当前正在执行的慢查询
SHOW FULL PROCESSLIST;
```
我们当时从`SHOW FULL PROCESSLIST`里抓到了一条:
```sql
-- 这条SQL执行了28秒
SELECT * FROM menu_items
WHERE store_id = 123
AND category_id IN (SELECT id FROM categories WHERE store_id = 123)
ORDER BY sales_count DESC
LIMIT 20;
```
问题出在`category_id IN (子查询)`——子查询没有走索引,每次都全表扫描。门店有2000多个菜品,高峰期100个并发查询同时执行这条SQL,数据库CPU直接飙到95%。
解决方案分两步:
**临时止血**——杀掉慢查询,加索引:
```sql
-- 先杀掉正在执行的慢查询
KILL 12345;
-- 给menu_items加联合索引
ALTER TABLE menu_items ADD INDEX idx_store_category (store_id, category_id);
```
**根治**——把IN子查询改成JOIN:
```sql
-- 优化后:用JOIN替代IN子查询,走索引
SELECT m.* FROM menu_items m
INNER JOIN categories c ON m.category_id = c.id
WHERE m.store_id = 123
AND c.store_id = 123
ORDER BY m.sales_count DESC
LIMIT 20;
```
改完之后这条SQL从28秒降到80毫秒,连接池active数从20掉到3,502率直接归零。
## 踩坑清单
**坑1:502和504搞混,排查方向走错**——502是上游断连(偏崩溃),504是上游超时(偏性能)。我们一开始以为应用崩了,查了半天进程状态,后来才发现是超时。以后先看Nginx错误日志里的具体错误类型,再决定往哪个方向查。
**坑2:只看应用日志,不看Nginx错误日志**——应用日志里只看到"获取数据库连接超时",但看不到是Nginx层报的502。以后排查顺序固定:先看网关日志,再看应用日志,最后看数据库,从外到内逐层收窄。
**坑3:连接池大小拍脑袋设**——我们一开始设的是50,以为越大越好。实际上MySQL最大连接数也就200,应用服务器每台20-30就够了,设太大反而会把数据库压垮。后来按"核心数*2 + 有效磁盘数"的公式重新调了。
**坑4:慢查询临时kill了但没加索引**——第一次止血只KILL了慢查询,没加索引,结果10分钟后又复发。一定要边止血边治本,把索引和SQL优化一起做了,不然白忙一场。
**坑5:没有慢查询告警,靠用户投诉才发现**——这次事故是用户半夜打客服电话才知道的。后来加了两个监控:MySQL慢查询超过5秒自动告警、Nginx 5xx率超过1%自动告警,再也没被用户叫醒过。
## 写在最后
502排查这件事,说复杂也复杂,说简单也就三层:网关日志告诉你"为什么错",连接池指标告诉你"卡在哪",慢查询日志告诉你"根因是什么"。我们这次花了40分钟定位,其中30分钟都在走弯路——先查应用进程、再查网络、最后才想到数据库。后来把这套排查路径固化成runbook,下次类似事故5分钟就能定位到方向。
性能治理没有银弹,但有几个红线值得记:慢查询必须有索引、连接池不能拍脑袋、5xx率必须有告警。这三件事做到位,半夜被叫醒的次数能少一大半。
- 点赞
- 收藏
- 关注作者
评论(0)