Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一次 Node.js http 连接无法复用的问题排查

一次 Node.js http 连接无法复用的问题排查

原创
作者头像
码农架构
修改于 2020-10-23 06:25:25
修改于 2020-10-23 06:25:25
2K00
代码可运行
举报
文章被收录于专栏:码农架构码农架构
运行总次数:0
代码可运行

首发公众号:码农架构

一次压测中阿里云 SLB 的并发连接数被打满了,导致服务之间的 HTTP 调用延迟很大。当时 SLB 的并发连接数情况如下图所示。

登录容器终端查看,发现某个前端 Node.js 服务中的单个容器的 ESTABLISH 状态的连接数达到 2 万多个,几十个容器直接把连接数占满了。

通过 tcpdump 抓包发现了如下的情况:

  • http 连接请求头都有带上 Connection: Keep-Alive
  • 连接全部都没有复用,一个连接三次握手完,隔了 65s 才会被 nginx 超时发送 fin 包断开

因此连接既有 Keep-Alive,不会在 http 请求处理完以后关闭,又没有被复用,因此压测请求一上来,连接蹭蹭蹭的往上涨,马上就达到了 SLB 的瓶颈。

一开始我们以为是 Node.js 的 http.Agent 的参数设置有错误,Node.js 通过http.Agent 来管理可复用的连接,创建 http.Agent 实例的方法如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var agent = new http.Agent();

请求头 Connection: Keep-Alive 就是在 http.Agent 中指定的,如下所示。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var agent = new http.Agent({keepAlive: true})

我们来写一个最简单的例子,代码如下所示。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let http = require('http');

const agent = new http.Agent({keepAlive: true,});

function sendHttp() {
    http.get({
        hostname: 'ya.test.me',
        port: 80,
        path: '/',
        agent: agent
    }, (res) => {
        console.log('STATUS: ' + res.statusCode);
        res.on('data', chunk => {
            console.log('BODY: ' + chunk);
        });
    });
}

sendHttp();

setTimeout(function () {
    console.log("start sleep");
    sendHttp();
}, 10 * 1000);

setTimeout(function () {
}, 100000);

执行上面的 Node.js 代码,得到的抓包结果如下

可用看到这次实验中的包,间隔 10s 的两次 HTTP 请求复用了 TCP 连接,这个连接在空闲 65s 左右以后被 Nginx 断开。

排查到这里,貌似思路就断了。Node.js 明明有复用连接的能力,为什么这里没有生效。只能去阅读 Node.js 的 Agent 的源码,发现它在底层维护了requests、freeSockets 等数据结构,如下所示。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function Agent(options) {
  this.requests = {};
  this.sockets = {};
  /**
  *  freeSockets 是一个 map,结构如下:
  *  test.ye.me:80 -> [socket1, socket2, socket3...]
  *  xxx.com:8080 -> [socket1, socket2, socket3...]
  **/
  this.freeSockets = {};
  this.keepAliveMsecs = this.options.keepAliveMsecs || 1000;
  this.keepAlive = this.options.keepAlive || false;
  // 允许对单个 host:port 最大的连接数
  this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
  // 允许对单个 host:port 最大的空闲连接数
  this.maxFreeSockets = this.options.maxFreeSockets || 256;
}

当连接空闲时,会回调 free 事件,核心代码注释如下所示。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 this.on('free', (socket, options) => {
    // name 是域名和端口的拼接字符串 test.ya.me:80:
    var name = this.getName(options);
        // 得到当前的域名端口对应的 freeSockets 数组
        var freeSockets = this.freeSockets[name];
        var freeLen = freeSockets ? freeSockets.length : 0;
        var count = freeLen;
        // 如果当前空闲连接大于配置的 maxSockets 或 maxFreeSockets 值,直接关闭当前这个 socket
        if (count > this.maxSockets || freeLen >= this.maxFreeSockets) {
          socket.destroy(); // 关闭 socket
        } else if (this.keepSocketAlive(socket)) {
          如果是 Keep-Alive 的 socket,将它加入到 freeSockets 数组中
          freeSockets = freeSockets || [];
          this.freeSockets[name] = freeSockets;
          socket[async_id_symbol] = -1;
          socket._httpMessage = null;
          this.removeSocket(socket, options);
          freeSockets.push(socket);
        } else {
          前面都不满足要它何用,关掉
          // Implementation doesn't want to keep socket alive
          socket.destroy();
        }
    }
  });
}

通过调试,发现线上服务的代码,确实有回调到 free 事件,但是没有调用过 reuseSocket 方法。后来前端大佬去看代码发现了一点蛛丝马迹,每次请求时,都新建了一个 http.Agent 对象,这样就相当于每次 http 调用都新建了一个连接池,每次 HTTP 请求完以后这个连接池的空闲连接数都是 1。下面来进行实验,代码如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let express = require("express");
let app = express();
let http = require('http');

app.get("/", function (req, res) {
    http.get({
        hostname: 'ya.test.me',
        port: 80,
        path: '/',
        agent: new http.Agent({keepAlive: true,})
    }, (result) => {
        console.log('STATUS: ' + result.statusCode);
        result.on('data', chunk => {
            console.log('BODY: ' + chunk);
        });
        res.send("hello");
    });
});
app.listen(3000);

启动这个 Node.js 服务,同时开始抓取 80 端口的包,使用 ab 工具(其它能批量发起 http 调用工具也行)调用这个 node 服务,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ab -n 5000 -c 10  'http://10.211.55.10:3000/'

短短时间内 ESTABLISH 的连接就达到了几千个,这些连接全部没有复用。与线上的现象一样。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
netstat -lnpa | grep :80 | grep -v 8080  | awk '{print $6}' | sort | uniq -c | sort -rn

   2038 ESTABLISHED
      1 LISTEN

跟踪其中一个包,它的包交互过程如下。

这个连接保持了 65s 才被 Nginx 超时断开,既占了连接,又没有复用,比短连接危害更大。

接下来我们把 http.Agent 对象改为了全局公用,重新来一遍实验,使用 netstat 的结果如下所示。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
netstat -lnpa | grep :80 | grep -v 8080  | awk '{print $6}' | sort | uniq -c | sort -rn

     10 ESTABLISHED
      1 LISTEN

可以看到全程最多只创建了 10 个连接,相比于之前的 2000 多个,降了几个数量级。wireshark 跟踪一个包的结果如下。

可以看到连接终于被复用起来了。

小结

这个问题本来比较简单,只是因为对封装过很多层以后的 Node.js 不太熟悉,导致排查花了一些时间。这个问题在 Java 中很早也犯过错,使用 OkHttp 发起连接时,如果 OkHttpClient 实例没有被单例,每次调用都 new 一个的话,那就是一个灾难。因为 OkHttpClient 内部也是维护了一个连接池。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
  final ConnectionPool connectionPool;
}

教训告诉我们,连接池使用需谨慎,连接池需要共享。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Node.js v19,它来了!详解 6 大特性
通译自:6 Major Features of Node.js 19. Details of Node.js 19 new features… | by Jennifer Fu | Oct, 2022 | Better Programming
掘金安东尼
2022/11/30
8750
Node.js v19,它来了!详解 6 大特性
走进Node.js 之 HTTP实现分析
作者:正龙(沪江Web前端开发工程师) 本文为原创文章,转载请注明作者及出处 上文“走进Node.js启动过程”中我们算是成功入门了。既然Node.js的强项是处理网络请求,那我们就来分析一个HT
iKcamp
2018/01/04
2.1K0
走进Node.js 之 HTTP实现分析
《深入浅出Node.js》-网络编程
Node 中提供了 net,dgram,http,https 四个模块,分别用来处理 TCP,UDP,HTTP,HTTPS,适用于客户端和服务器。
李振
2021/11/26
7300
nodejs源码分析之http Agent
Agent对TCP连接进行了池化管理。简单的情况下,客户端发送一个HTTP请求之前,首先建立一个TCP连接,收到响应后会立刻关闭TCP连接。但是我们知道TCP的三次握手是比较耗时的。所以如果我们能复用TCP连接,在一个TCP连接上发送多个HTTP请求和接收多个HTTP响应,那么在性能上面就会得到很大的提升。Agent的作用就是复用TCP连接。不过Agent的模式是在一个TCP连接上串行地发送请求和接收响应,不支持HTTP PipeLine模式。下面我们看一下Agent模块的具体实现。看它是如何实现TCP连接复用的。
theanarkh
2021/04/22
1K0
nodejs源码分析之http Agent
深入学习 Node.js Http
Expect 是一个请求消息头,包含一个期望条件,表示服务器只有在满足此期望条件的情况下才能妥善地处理请求。规范中只规定了一个期望条件,即 Expect: 100-continue,对此服务器可以做出如下回应:
阿宝哥
2019/11/05
9640
Node核心模块篇:HTTP
HTTP协议是世界上广泛使用的应用层通信协议,而通过Node的核心模块HTTP,我们可以方便快速的构建自己的HTTP服务器和客户端,并在两者之间进行通信传递数据。
凌虚
2020/07/18
6300
Node核心模块篇:HTTP
Node理论笔记:网络编程
TCP全名为传输控制协议,在OSI(由七层组成:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)中属于传输层协议。HTTP、SMTP、IMAP协议都是基于TCP构建的。
Ashen
2020/06/01
1.3K0
Node.js 安全最佳实践
最近 Node.js 团队在官方文档上公布了一份最新的安全实践,解读了一些 Node.js 服务下一些常见的攻击场景以及预防手段,我们一起来看看吧!
ConardLi
2023/01/09
2.4K0
Node.js 安全最佳实践
使用http维持socket长连接
用户1141560
2017/12/22
1.5K0
Http 持久连接与 HttpClient 连接池
HTTP协议是无状态的协议,即每一次请求都是互相独立的。因此它的最初实现是,每一个http请求都会打开一个tcp socket连接,当交互完毕后会关闭这个连接。
用户1257393
2018/07/30
2.1K0
Http 持久连接与 HttpClient 连接池
Node.js 在有赞的实践分享
有赞最早的一个比较完整的 Node 项目是公司内部的一个管理系统,这个系统是用 Node 全栈开发的,主要包括一个给 HR 用的员工管理系统和给小伙伴用的 APP。就像大多数公司一样,我们第一个 Node 项目也是直接用 Koa,然后整合一些开源的中间件,这样就快速的把项目搭建起来了。
五月君
2019/08/29
1.2K0
Node.js 在有赞的实践分享
基于Unix Socket的可靠Node.js HTTP代理实现(支持WebSocket协议)
实现代理服务,最常见的便是代理服务器代理相应的协议体请求源站,并将响应从源站转发给客户端。而在本文的场景中,代理服务及源服务采用相同技术栈(Node.js),源服务是由代理服务fork出的业务服务(如下图),代理服务不仅负责请求反向代理及转发规则设定,同时也负责业务服务伸缩扩容、日志输出与相关资源监控报警。下文称源服务为业务服务。
欲休
2020/03/12
1.7K0
《Node.js》核心技术教程(笔记)
模块化是一种设计思想,利用模块化可以把一个非常复杂的系统结构细化到具体的功能点,每个功能点看作一个模块,然后通过某种规则把这些小的模块组合到一起,构成模块化系统。
爱学习的程序媛
2022/04/07
1.8K0
《Node.js》核心技术教程(笔记)
详解HTTP 与TCP中Keep-Alive机制的区别
keepalive已经不是什么新鲜的概念了,HTTP协议中有keep-alive的概念,TCP协议中也有keep-alive的概念。二者的作用是不同的。本文将详细的介绍http中的keep-alive,介绍tomcat在server端是如何对keep-alive进行处理,以及jdk对http协议中keep-alive的支持。同时会详细介绍tcp中的keepalive机制以及应用层的心跳。
田守枝
2019/05/21
4.2K0
详解HTTP 与TCP中Keep-Alive机制的区别
Go http client 连接池不复用的问题
当 http client 返回值为不为空,只读取 response header,但不读 body 内容就执行 response.Body.Close(),那么连接会被主动关闭,得不到复用。
梦醒人间
2021/01/04
3.7K0
使用Node.js理解和测量Http时序
举个例子:如果你的DNS查询比你期望的时间更长,这个问题可能是因为你的DNS供应商或者DNS缓存引起的。
疯狂的技术宅
2019/03/27
1.2K0
使用Node.js理解和测量Http时序
HTTP 与 TCP 的 KeepAlive 是一个东西吗?
KeepAlive 已经不是什么新鲜的概念了,HTTP 协议中有 KeepAlive 的概念,TCP 协议中也有 KeepAlive 的概念。二者的作用是不同的。本文将详细的介绍 HTTP 中的 KeepAlive,介绍 Tomcat 在 Server 端是如何对 KeepAlive 进行处理,以及 JDK 对 HTTP 协议中 KeepAlive 的支持。同时会详细介绍 TCP 中的 KeepAlive 机制以及应用层的心跳。
涤生
2019/07/10
1.6K0
使用 C# 开发 node.js 插件
项目需求 最近在开发一个 electron 程序,其中有用到和硬件通讯部分;硬件厂商给的是 .dll 链接库做通讯桥接, 第一版本使用 C 写的 Node.js 扩展 😁;由于有异步任务的关系,实现使用了 N-API 提供的多线程做异步任务调度, 虽然功能实现了,但是也有些值得思考的点。 纯 C 编程效率低,木有 trycatch 的语言调试难度也大 (磕磕绊绊的) 编写好的 .node 扩展文件,放在 electron 主进程中运行会有一定的隐患稍有差错会导致软件闪退 (后来用子进程隔离运行) 基于
conanma
2022/01/05
2.1K0
Android网络编程(八)源码解析OkHttp中篇[复用连接池]
1.引子 在了解OkHttp的复用连接池之前,我们首先要了解几个概念。 TCP三次握手 通常我们进行HTTP连接网络的时候我们会进行TCP的三次握手,然后传输数据,然后再释放连接。 TCP三次握手的过
用户1269200
2018/02/01
1.3K0
Android网络编程(八)源码解析OkHttp中篇[复用连接池]
为什么要用 Node.js
这是一个移动端工程师涉足前端和后端开发的学习笔记,如有错误或理解不到位的地方,万望指正。 Node.js 是什么 传统意义上的 JavaScript 运行在浏览器上,这是因为浏览器内核实际上分为两个部分:渲染引擎和 JavaScript 引擎。前者负责渲染 HTML + CSS,后者则负责运行 JavaScript。Chrome 使用的 JavaScript 引擎是 V8,它的速度非常快。 Node.js 是一个运行在服务端的框架,它的底层就使用了 V8 引擎。我们知道 Apache + PHP 以及 J
前朝楚水
2018/04/03
2.3K0
相关推荐
Node.js v19,它来了!详解 6 大特性
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验