解决 node 服务偶发 502 问题

2022年03月22日

问题描述

内部公共服务提供一个 ping 接口,客户端会每隔 5 秒请求一次这个 ping 接口,但是看客户端 http 日志时,发现会出现偶发 502 http 状态码。

尝试解决的过程

首先 502 Bad Gateway 是网关层(nginx)抛出的错误,可推断网关尝试访问 ping 接口时碰到了一些问题。

由于权限一些其他问题,我现在看不到网关层的日志,所以我需要猜测尝试修改,直至找到问题。

尝试优化接口响应速度

存在多个客户端调用这个 ping 接口,并发请求量挺大,看接口响应速度发现会出现速度慢的情况。那就尝试优化这个接口逻辑,先 http 响应,将业务逻辑后置。修改之后,发现问题继续存在。

思考:每隔 5 秒是不是存在什么问题?

这个就需要一些 node 经验了,通过看 node http server api 文档,发现 server.keepAliveTimeout 这个选项默认值就是 5 秒。此选项就涉及到 keep-alive 消息头部,表示 http 持久连接,5 秒内有第二个 http 请求的话,便会复用同一个底层 TCP 连接。

那这个值刚好是 5 秒,客户端又刚好每隔 5 秒 ping 一下,是不是这里有问题呢?

会不会出现此临界状况呢:当 node 服务端 5 秒到时,它关闭了底层 TCP 连接,但就在同时,客户端通过此底层 TCP 连接发起了第二次接口请求。推论下来,这次 http 请求的结构应该就是失败的。

验证 5 秒的临界状况

如何验证呢?这里我使用碰到相同问题的原作者的代码,循环进入上述的临界状况,发现确实最终失败率较高,客户端会出现 ECONNRESET 错误情况。

 1import _ from 'lodash';
 2import restify from 'restify';
 3import got from 'got';
 4import HttpAgent from 'agentkeepalive';
 5
 6const server = restify.createServer();
 7
 8server.get('/stuff', async (req, res, next) => {
 9  res.send('some stuff');
10});
11
12server.listen(8080, startSendingRequests);
13
14const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
16async function startSendingRequests() {
17  let successes = 0;
18  let failures = {};
19
20  const client = got.extend({
21    retry: 0,
22    agent: { http: new HttpAgent() },
23  });
24
25  for (const i of _.times(30)) {
26    await wait(4999);
27
28    await client
29      .get('http://localhost:8080/stuff')
30      .then((r) => {
31        successes++;
32      })
33      .catch((e) => {
34        failures[e.message] = (failures[e.message] || 0) + 1;
35      });
36  }
37
38  console.log({ successes, failures });
39}

解决方案

问题基本明确,解决方案有三个:

一、修改服务端 server.keepAliveTimeout 参数

最终我也是选了此方式,修改 server.keepAliveTimeout 为一个较大的值且最好不容易碰到临界点例如 333 秒。通过避免临界状态来解决此问题。

最终上线后,再看日志偶发 502 情况消失。

二、客户端重试

如果客户端也是 nodejs 写的,http 客户端可在碰到错误时,判断 req. reusedSocket 且错误码为 ECONNRESET 的情况下,选择重试。代码可参考

 1const http = require('http');
 2const agent = new http.Agent({ keepAlive: true });
 3
 4function retriableRequest() {
 5  const req = http
 6    .get('http://localhost:3000', { agent }, (res) => {
 7      // ...
 8    })
 9    .on('error', (err) => {
10      // Check if retry is needed
11      if (req.reusedSocket && err.code === 'ECONNRESET') {
12        retriableRequest();
13      }
14    });
15}
16
17retriableRequest();

三、客户端修改 keepalive 值

使用量较大的 agentkeepalive 库也曾碰到过此问题,它将 freeSocketTimeout 参数的默认值修改为了 4 秒,以达到避免临界状态的效果。

The default server-side timeout is 5000 milliseconds, to avoid ECONNRESET exceptions, we set the default value to 4000 milliseconds.

结论

相关链接