node http keep-alive demo

http keep-alive 相关资料非常多,如果深挖,那可能就长篇大论了,不合适普及,这只是一篇新手入门引导,
主要讲解 node 下 http 请求的坑,以及 keep-alive 的简单使用,后续才会详细剖析原理。

起因

我司使用 node 做中间层开发,所以 api 都是 node 代理转发的,虽然目前 qps 不是特别高,都能满足需求,但压测总归是有的。
不压不知道,一压,emmmmm。。。

搭建个纯净的 node 请求测试环境

先不说业务,我们来搭建个最简单的测试环境,看看压测工具能跑到多少 QPS。
然后我们写个 api 代理模块看看能跑到多少。

使用 node 官方例子 https://nodejs.org/en/about/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

然后我们用 wrk 来压测,因为简单方便。

  • 参数 -c 并发数,就想象成同时请求后台 api 的数量,一个页面往往会调用3-5个后台接口。
  • 参数 -d 持续时间,这就是压测本质啊,持续越久,效果越真实,因为很多时候后面会挂掉的。

看看 10 并发,持续 10 秒的情况。

1
2
3
4
5
6
7
8
9
$ wrk -c 10 -d 10 http://localhost:3000
Running 10s test @ http://localhost:3000
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 523.02us 129.44us 4.04ms 92.33%
Req/Sec 9.59k 550.11 10.55k 82.67%
192745 requests in 10.10s, 25.37MB read
Requests/sec: 19083.81
Transfer/sec: 2.51MB

可以看出 Req/Sec 的 Avg 值,就是平均 QPS 为 9.59k,因为默认开了2线程,所以总 QPS 为 19k。

好了,测试环境和测试结果已经有了直观的展现。
下面搭建 node 接口代理然后重新压测。

node 接口代理

我们由于业务需求,没用 http-proxy 做直接的代理转发,而是基于 got 模块,自定义的接口模型。

我们先不考虑业务开销,直接代理转发看看结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const http = require('http');
const got = require('got');

const hostname = '127.0.0.1';
const port = 8000;

const server = http.createServer((req, res) => {
got.get('http://localhost:3000/').then(({ body }) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(body);
}).catch((err) => console.log(err));
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

代码跟刚才一样,只是多了一层 got 代理接口。
这里没用 got stream 直接 pipe 给 res,因为我们的代理接口中会做其他业务操作。

现在来压测下这个服务的情况吧。

1
2
3
4
5
6
7
8
9
$ wrk -c 10 -d 10 http://localhost:8000
Running 10s test @ http://localhost:8000
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 13.63ms 20.29ms 193.78ms 94.45%
Req/Sec 522.58 126.90 660.00 89.23%
10237 requests in 10.03s, 1.35MB read
Requests/sec: 1020.52
Transfer/sec: 137.53KB

有点慌,什么情况,怎么会相差这么多。

我们在 3000 服务中加入 console.log(req.headers); 看看 headers 字段。

1
2
3
4
5
6
const server = http.createServer((req, res) => {
console.log(req.headers); // 输出 headers
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});

然后访问 8000 服务,看到控制台输出:

1
2
3
4
{ 'user-agent': 'got/9.3.2 (https://github.com/sindresorhus/got)',
'accept-encoding': 'gzip, deflate',
host: 'localhost:3000',
connection: 'close' }

其中 connection 所以每次请求都会重新建立 tcp 连接,浪费了不少性能。

接下来我们要打开 keep-alive 提升代理性能。

开启 keep-alive

安装 agentkeepalive 模块,这是 fengmk2 大佬封装的模块,我们直接使用看看效果先。

1
$ yarn add agentkeepalive

然后在 8000 服务中开启代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require('http');
const got = require('got');
const Agent = require('agentkeepalive');

const agent = new Agent();

const hostname = '127.0.0.1';
const port = 8000;

const server = http.createServer((req, res) => {
got.get('http://localhost:3000/', { agent }).then(({ body }) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(body);
}).catch((err) => console.log(err));
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

重新启动,访问 8000 服务,查看 3000 服务控制台。

1
2
3
4
{ 'user-agent': 'got/9.3.2 (https://github.com/sindresorhus/got)',
'accept-encoding': 'gzip, deflate',
host: 'localhost:3000',
connection: 'keep-alive' }

看到已经开启了 keep-alive,我们把 3000 服务中的 log 先关掉,否则影响结果。

1
2
3
4
5
6
7
8
9
$ wrk -c 10 -d 10 http://localhost:8000
Running 10s test @ http://localhost:8000
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.32ms 1.84ms 19.30ms 89.87%
Req/Sec 0.96k 69.30 1.06k 78.50%
19141 requests in 10.02s, 2.52MB read
Requests/sec: 1910.43
Transfer/sec: 257.46KB

可以看到性能提升了 1 倍,将近 1k 了,但跟原服务 9.59k 还是相差太多。

目前我们只能开多进程来提升整体 QPS 了。
比如开 9 进程,每个进程都能吃到近 1k,差不多可以吃满源服务。
但如果你开了 10 进程甚至更多,那瓶颈就会在源服务上,就需要给源服务加 cpu 或加服务器。

小结

这里还有个会影响结果的因素,而且影响会比较大。
因为是我本地测试,两个服务都跑本地,要严谨的话,应该跑在服务器上,或相同配置的虚拟机中。
不过本文意图是体验 keep-alive 和怎么用 node 吃满接口服务。
所以就不做那么严谨的测试了。

还要吐槽一点,node http 请求,性能真是低下啊,或者是我不知道怎么正确的使用。
因为 autocannon 压测工具,也是 node 写的,但他能吃满,我们直接写请求,只能达到 1/10。

我自己写了个基于 net 的 http 1.1 请求 demo 测试,只提升了100左右,并没有特别大的改进。
因为水平有限,没写基于 net + keep-alive 的 http 1.1 测试,所以不能下结论。

后续我会慢慢研究 keep-alive 然后记录分享的。