华为云ELB偶发502排查
华为云 ELB 偶发 502 排查:问题不在后端,在"连接被谁先关掉"
502 率只有 0.05%,而且高峰期完全正常、低峰期反而集中出现。后端应用日志里翻不到对应请求,代码、CPU、内存、慢查询全部干净。最后查出来是 ELB 与后端之间的长连接超时配置错配,一个参数改完,502 归零。
一、现象:一个反常识的分布
线上接口通过 ELB 暴露,监控里出现 502,特征是:
| 观察项 | 现象 |
|---|---|
| 502 率 | 0.05% 左右,绝对量不大但持续存在 |
| 时间分布 | 集中在低峰期(凌晨 2~6 点),高峰期几乎为 0 |
| 后端应用日志 | 找不到对应的请求记录 |
| 后端资源 | CPU、内存、GC、连接池全部正常 |
| 下游依赖 | 无超时、无慢查询 |
第三条是决定性线索:后端应用根本没收到这个请求。所以问题不在业务代码,而在"请求到达后端之前"的那一段——也就是 ELB 到后端之间的连接。
二、先把 502 和 504 分清楚
这两个错误码经常被混在一起排查,方向完全不同:
| 错误码 | 含义 | 排查方向 |
|---|---|---|
| 502 Bad Gateway | 网关从后端拿到了无效响应,或连接异常 | 连接层:RST、连接被关、响应格式非法 |
| 504 Gateway Timeout | 网关等后端超时 | 应用层:慢查询、线程池打满、死锁 |
本次是 502,且后端无日志,指向"连接层异常"——ELB 把请求发到了一条已经被关闭的连接上,写进去立刻收到 RST,于是判定上游异常,返回 502。
三、机制:keepalive 复用与"谁先关闭"
ELB 与后端之间不是每次请求都新建连接,而是维护一个长连接池,请求来了从池里取一条复用。这套机制在绝大多数时候都很好,但它有一个隐含前提:
连接池里的连接,必须是活的。
问题就出在"谁先判定这条连接已经没用了"上。连接的两端各自维护自己的空闲超时:
- ELB 侧:监听器的空闲超时(比如 300 秒),超过就关闭连接
- 后端侧:keepalive 超时(比如 60 秒),超过就关闭连接
如果后端先关(60 秒 < 300 秒),会发生这样的竞争:
T+60s 后端认为连接空闲超时,主动发 FIN 关闭连接
↓ 但 ELB 的空闲计时器还没到(它以为还有 240 秒)
T+61s ELB 收到请求,从池里取出这条连接,写入请求数据
↓ 后端早已关闭,回 RST
T+61s ELB 判定上游连接异常 → 返回 502
关键在于"关闭的感知是单向的":后端关闭连接时,ELB 在收到 FIN 之前仍然认为这条连接可用,于是出现了一个竞争窗口。窗口里的请求,全部变成 502。
为什么低峰期更严重
这就是整件事最反常识的地方,也是验证这个假设的最好证据:
- 高峰期:连接一直在传数据,几乎不会触发空闲超时,池里的连接始终是活的 → 502 极少
- 低峰期:请求稀疏,连接频繁进入空闲状态,大量连接恰好落在"后端已关闭、ELB 未感知"的窗口里 → 502 集中爆发
“流量越少错误越多”,基本可以把连接复用类问题直接锁定。
四、验证:三个动作确认假设
1. 对齐两边的超时配置
把 ELB 监听器的空闲超时,和后端服务的 keepalive 超时并排写出来对比:
| 配置项 | 值 | 关系 |
|---|---|---|
| ELB 监听器空闲超时 | 300 s | 较长 |
后端 Nginx keepalive_timeout |
60 s | 较短 → 后端先关 → 危险 |
只要后端超时小于 ELB 超时,就一定存在这个竞争窗口。
2. 在后端抓包,看 FIN 和请求的时序
# 在后端服务器抓 ELB 过来的流量
tcpdump -i any -nn -s0 'tcp port 8080' -w /tmp/elb.pcap
在 Wireshark 里过滤 tcp.flags.fin == 1 || tcp.flags.reset == 1,能看到:
- 后端发出
FIN, ACK(空闲超时主动关闭) - 几十毫秒后,ELB 在这条连接上发送 HTTP 请求
- 后端回
RST
这个时序一旦出现,假设就成立。
3. 看后端连接数是否周期性归零
后端 ESTABLISHED 连接数如果呈锯齿状、周期性掉到接近 0,说明连接被批量关闭了——这是 keepalive 超时在起作用。正常情况下,连接数应该稳定在一个水平线上小幅波动。
五、修复:让 ELB 先关,后端后关
原则只有一条:
关闭动作必须由连接池的持有者(ELB)发起。
因为 ELB 关闭自己池里的连接时,会同步把它从池中移除,不会再把请求发到死连接上。反过来,后端擅自关闭,ELB 就成了"不知情的一方"。
落地方式:
| 做法 | 说明 |
|---|---|
| 后端 keepalive 超时 > ELB 空闲超时 | 例如 ELB 300 s,后端设 900 s,留足余量 |
| 或调小 ELB 空闲超时 | 让它明显短于后端超时,同样成立 |
| 后端限制单连接最大请求数 | 如 Nginx keepalive_requests 1000,主动轮换,避免长连接老化 |
后端 Nginx 侧示例:
upstream app_backend {
server 192.168.1.10:8080;
keepalive 64; # 连接池大小
keepalive_requests 1000; # 单连接最多处理 1000 个请求后轮换
keepalive_timeout 900s; # 必须大于 ELB 空闲超时
}
同时把 proxy_http_version 1.1 和 proxy_set_header Connection "" 配上,否则 Nginx 对上游默认用 HTTP/1.0,keepalive 根本不生效:
proxy_http_version 1.1;
proxy_set_header Connection "";
这一条经常被漏掉:配了 keepalive 却没改 HTTP 版本,等于没配,连接照样每次新建。
六、其他会引发 502 的场景(分类清单)
连接复用只是 502 的一个来源。按出现频率排一下,方便下次快速定位:
| 场景 | 特征 | 处理 |
|---|---|---|
| 超时错配(本文) | 低峰期集中、后端无日志 | 后端 keepalive > ELB 空闲超时 |
| 滚动发布未优雅摘流 | 发布时间点集中出现 | 优雅停机 + 健康检查摘流延迟 |
| 后端连接队列溢出 | 高并发时突发,伴随 SYN 重传 |
调大 backlog、扩实例 |
| 健康检查配置错误 | 只有部分实例持续 502 | 核对健康检查路径与端口 |
| 响应头过大 / 非法响应 | 特定接口必现 | 检查 header 体积、响应格式 |
| 后端 TLS 配置不一致 | 改了证书后开始出现 | 对齐协议版本与证书链 |
排查顺序建议:先看 502 是否与流量相关(低峰多还是高峰多),这一步就能把范围砍掉一大半。
七、效果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 502 率 | 0.05% | 0 |
| 低峰期 502 次数 | 每小时数十次 | 0 |
| 后端 ESTABLISHED 连接数 | 锯齿状,周期性归零 | 平稳 |
| 改动内容 | — | 后端 keepalive 60s → 900s,补 proxy_http_version 1.1 |
代码零改动,业务零感知。
八、小结
四句话:
- 502 看连接,504 看超时,先把错误码分清楚,排查方向完全不同。
- "流量越少错误越多"是连接复用类问题的强信号,空闲超时在低峰期才暴露。
- 关闭动作必须由连接池持有者发起——后端 keepalive 超时要大于 ELB 空闲超时,方向别搞反。
- 配了
keepalive别忘了proxy_http_version 1.1,否则连接池形同虚设。
如果只记一句:偶发 502 先去对齐两边的超时参数,比翻应用日志快得多。
- 点赞
- 收藏
- 关注作者
评论(0)