华为云 ELB 弹性负载均衡后端健康检查抖动引发间歇性 502 的一次完整排查
上周三下午,客服转来一条反馈:我们一个内部订单系统的接口在下午三点到四点之间会随机返回 502,量不大,大概每分钟三到五次,监控面板上的错误率曲线却是"毛刺状"的——高的时候能到 8%,但一秒后立刻归零,APM 里的 p99 延迟完全正常。
这种"偶发、随机、自愈"的故障是最难查的,因为它几乎不留下稳定复现的路径。第一反应当然是去看应用日志,但翻了半小时,业务代码里没有任何异常堆栈,Tomcat 的 access log 里 502 那一行的 upstream 状态是空值。第二反应是看网络,结果 VPC 侧的流量图一切正常,弹性带宽、水连通性、丢包率全是绿的。最后我们才把视线移到负载均衡上,而真凶就藏在最不起眼的一个配置项里。
一、先排除三个最容易被误判的东西
故障排查里最费时间的从来不是定位,而是排除。我这次在 ELB 上先后排除了三个方向:
第一是健康检查本身"假绿"。当时的 ELB 监听器配了 HTTP 健康检查,检查路径是 `/health`,间隔 30 秒、超时 5 秒、不返回 3xx 视为不健康。看上去标准。但我们把后端 ECS 上的 `/health` 单独拉出来压测,发现这个接口在正常情况下响应时间 P99 只有 8 毫秒,问题是——它是同步执行的,而且会去查一次数据库。下午三点正好是我们另一个批处理任务在跑,数据库连接池被打满,`/health` 的响应时间被拖到了 4 秒以上,偶尔直接超时。而 ELB 的健康检查超时正好配的是 5 秒,也就是说,**只要那次数据库抖动超过 5 秒,ELB 就会把这个后端标记为不健康**。
第二是后端服务器数量。我们一开始怀疑是单点,就加了两台后端。加完之后故障确实减轻了,但没有消失——这反而是个好信号,说明问题确实和后端的"健康状态判定"有关,而不是流量本身。
第三是长连接复用。我们的服务启用了 Keep-Alive,超时 90 秒,而 ELB 侧的后端 idle timeout 是 60 秒。这也是一个经典错配,但排查后发现它的表现是"固定的 60 秒断开一次",而不是随机 502,所以排除。
二、把健康检查机制拆开看
讲清楚原理才能定位。ELB 的 HTTP 健康检查是一个**独立的定时轮询**,它的每一次请求都会独立完成一次完整的三次握手和一次 HTTP 往返。这意味着两件事:
第一,它的耗时等于网络 RTT 加上后端处理时间,而后端处理时间完全取决于你那个健康检查接口有多"重"。用数据库做实时检查,等于把数据库的性能问题直接引入了负载均衡的判定逻辑里——数据库一抖,健康检查就误判,然后 ELB 把一台**其实还能服务的**后端摘掉,于是 502 产生。
第二,它有"**连续失败阈值**"这个概念。默认一次失败就摘除、连续成功两次才加回。这意味着一次网络抖动就能造成一次摘除,而恢复要等两个周期。阈值越小,灵敏但越容易误伤;阈值越大,稳但摘除滞后。这个值需要在灵敏度和稳定性之间取平衡。
我们后端一共有 6 台,ELB 的后端组里当时实际可用的是 4 台,另外 2 台因为健康检查反复失败被摘掉。这 6 台在同一个可用区、同一个规格、同一套部署脚本,唯一的区别是它们各自连的数据库分片不同——那两台正好连到了当时有慢查询的分片上。
三、真正的根因
链路是這樣的:批处理任务把一个数据库分片的 CPU 打到 90% 以上 → 该分片上的两台 ECS 的 `/health` 接口因同步查库而响应超过 5 秒 → ELB 健康检查判定失败并把这 2 台摘出后端组 → 剩余 4 台要承接全部流量,其中 2 台因为和摘掉的那两台同属一个可用区,实际跨可用区流量比例失衡 → 在流量重新分布的那几十秒里,部分请求落到正在重建连接的后端上,返回 502。
所以 502 不是 ELB 的 bug,也不是应用的 bug,而是**一条从数据库慢查询出发、经过健康检查、最终在负载均衡上放大成用户可见故障的传导链**。真正让我们踩坑的,是健康检查接口的实现方式和健康检查阈值的配置,这两个地方都写着"合理"的值,组合起来却是一个放大器。
四、我们做的四件事
第一,把健康检查接口从"查数据库"改成"查内存缓存"。我们用了一个进程内的静态布尔变量,健康检查只读这个变量,而后台线程每 5 秒异步去探一次数据库,探到异常就把变量置为 false。健康检查从此永远是 1 毫秒级返回,而且它反映的仍然是真实可用性。
第二,把健康检查阈值从"失败 1 次摘除"改成"失败 3 次摘除、成功 2 次加回",间隔从 30 秒缩短到 10 秒。数学上 3 次 × 10 秒 = 30 秒,和原来的 1 次 × 30 秒 = 30 秒在检测时延上是等价的,但抗抖动能力提升了 3 倍。这是我认为性价比最高的一个改动,纯配置,零成本。
第三,把健康检查的超时从 5 秒改成 2 秒。既然接口已经做到 1 毫秒,2 秒的超时给足余量的同时,还能在后端真正卡死时更快地把它摘掉,避免请求堆积。
第四,加了一条兜底的监控:单独对 ELB 的后端健康检查失败次数做告警,阈值是"连续 2 个周期失败"。这样后端组可用数量下降时,我们能先于业务方发现问题,而不是等他们报障。
五、沉淀下来的检查清单
这次之后我们给所有用 ELB 的服务定了一份检查清单,现在我一般会直接照着过一遍:
- 健康检查接口必须无依赖:不查数据库、不查下游、不读远程配置、不做重计算;
- - 健康检查接口要能区分"进程活着"和"能接流量"这两种状态,必要时做双探针;
- - 失败阈值不要用默认值,至少改成连续 2 到 3 次;
- - 超时时间要明显大于健康检查接口的 P99 响应时间,但明显小于后端服务的最大容忍延迟;
- - 后端 Keep-Alive 超时与 ELB 的 idle timeout 要对齐,且方向要一致(后端略短于 ELB);
- - 跨可用区的后端分布要保证摘除一台之后剩余容量仍然够用,不能只靠"多开几台";
- - 后端组可用数量要有独立告警。
六、一点体会
这次排查让我改变了一个观念:很多"云上的基础设施故障",根因都在自己的应用代码里,只是被云产品的抽象层放大了。ELB 只是一个忠实的执行者,它按我给的规则去判定后端生死——**规则本身不合理的时候,云的可观测性再强,也只能让你更快地看到问题,而不是更快地解决问题**。
所以现在我给团队做 code review 的时候,看到有人写健康检查接口去查数据库,我不会再评论代码风格了,我会直接问一句:这个接口的超时和阈值,配对过吗?
- 点赞
- 收藏
- 关注作者
评论(0)