5

开发环境

前端:Vue 2.0
后台:Node Express
浏览器:Chrome
部署系统:Linux

问题现象

在现有项目的基础之上增加了两个页面,但是在使用的过程中发现,当连续操作几次之后页面会变得奇慢无比,查看接口调用发现接口请求长时间处于pending状态,但是等1-2分钟左右接口还是会返回应答结果。如下图所示:

图片描述

原因分析

通过反复复现该问题(在各个页面之间不同切换,触发请求),发现了一个规律,就是每次在第7次页面切换的时候,所有接口都会被阻塞并在1分多钟之后才返回。

图片描述

看看这1分多钟究竟花在了哪里?

图片描述

从上图可以看到,整个接口请求的大部分时间都花在了Stalled阶段。现在的问题是Stalled是啥意思?下面是一段比较浅显的解释:

Time the request spent waiting before it could be sent. This time is inclusive of any time spent in proxy negotiation.Additionally, this time will include when the browser is waiting for an already established connection to become available for re-use, obeying Chrome’s maximum six TCP connection per origin rule.

从上面的解释看,可能有两个原因:

  1. TCP连接出问题了,一直无法建链成功;
  2. TCP连接是OK的,但是一直被占用无法使用。

首先看第一个问题,TCP连接是否正常?

$ lsof -i:8700
COMMAND   PID     USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
node    26315 mumingv   12u  IPv4 449215145     0t64  TCP *:8700 (LISTEN)
node    26315 mumingv   16u  IPv4 449217569     0t64  TCP localhost:8700->172.24.186.14:54064 (ESTABLISHED)
node    26315 mumingv   17u  IPv4 449217570     0t64  TCP localhost:8700->172.24.186.14:54065 (ESTABLISHED)
node    26315 mumingv   18u  IPv4 449217580     0t64  TCP localhost:8700->172.24.186.14:54066 (ESTABLISHED)
node    26315 mumingv   19u  IPv4 449217581     0t64  TCP localhost:8700->172.24.186.14:54067 (ESTABLISHED)
node    26315 mumingv   20u  IPv4 449226874     0t64  TCP localhost:8700->172.24.186.14:54574 (ESTABLISHED)
node    26315 mumingv   21u  IPv4 449217583     0t64  TCP localhost:8700->172.24.186.14:54069 (ESTABLISHED)

上图中,8700是网站的服务端口号,172.24.184.14是Chrome浏览器所在Mac的IP。在复现问题的过程中一直执行lsof -i:8700持续进行观察发现,当在第7次页面切换的时候,这里的TCP连接数量不再增加,维持在6个左右且状态都是ESTABLISHED(已建立)。所以可以基本排除TCP连接的问题。

当然,也可以通过netstat命令查询TCP连接状态。

$ netstat -tunpa | grep 8700
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 0.0.0.0:8700                0.0.0.0:*                   LISTEN      27765/node          
tcp        0      0 10.95.199.140:8700          172.24.186.14:65413         ESTABLISHED 27765/node          
tcp        0      0 10.95.199.140:8700          172.24.186.14:65412         ESTABLISHED 27765/node          
tcp        0      0 10.95.199.140:8700          172.24.186.14:65421         ESTABLISHED 27765/node          
tcp        0      0 10.95.199.140:8700          172.24.186.14:65420         ESTABLISHED 27765/node          
tcp        0      0 10.95.199.140:8700          172.24.186.14:65419         ESTABLISHED 27765/node          
tcp        0      0 10.95.199.140:8700          172.24.186.14:65422         ESTABLISHED 27765/node          

再来看第二个问题,TCP连接被谁占用了不释放?

看看是不是有其他请求占用了这些TCP连接,查看所有请求,果不其然:

图片描述

原来每次在页面切换的时候,浏览器都会默认发送一个请求获取一次网页图标,这个不是前端业务逻辑主动调用的XHR请求,但对于后端来说也是一次GET请求。

实际上,如果没有要求显示特定网页图标的话,后端随便返回一个信息就好了,不用非得准备一个网页图标。浏览器拿不到图标的话会显示一个默认图标。

问题找到了,看看为啥后端为啥没有返回图标并加以解决就好了。具体到这个项目,是在node express的app.js入口文件中没有注册相应的处理逻辑。

// 接口路由
loadRouter(app, '/project-name', path.join(__dirname, 'app/controllers'));

// 静态页面
app.use('/project-name', express.static(path.join(__dirname, "webroot", "project-name")));

// favicon.ico和其他不支持的请求
app.get("*", function(req, res) {
    if (req.path === "/favicon.ico") {
        return;  // !!!这里不能直接return,需要返回具体的内容,否则会阻塞express框架返回应答消息!!!
    }   
    throw new PathError();
});

知道问题后,修改就很简单了。

app.get("*", function(req, res) {
    if (req.path === "/favicon.ico") {
        res.json({'status':0, msg:''});  // 这里随便返回个内容就行,不影响浏览器使用默认图标进行展示
    }   
    throw new PathError();
});

至此,问题解决。

FAQ

Q:为什么浏览器和服务端之间最多只能创建6个TCP连接?

TCP连接资源数量有限,如果不限制数量的话,所有TCP全部被占用的话系统就“无法提供服务”了。一般浏览器的并发TCP连接数量都在5、6个左右,对于Chrome来说是6个。至于为什么是这么多,这是各浏览器自行设置的,没有标准。具体解释参考:官方文档

Q:后续如何排查这类接口问题?

一般按照如下几步进行排查即可:

  1. 浏览器端看XHR请求,判断XHR请求本身是否有异常;
  2. 浏览器端看ALL请求,判断非XHR请求是否有异常;
  3. 服务器端查看服务本身是否正常;
  4. 服务器端查看服务建立的TCP连接是否正常;
  5. 抓包查看TCP交互和业务请求交互报文是否有异常。

参考资料

  1. Network Issues Guide
  2. Understanding Resource Timing
  3. chrome的timeline中stalled问题解析

mumingv
24 声望1 粉丝

持之以恒,方得始终