模拟 Nagle 算法的Delayed Ack

最近做了keepalive优化,在线上应用使用了agentkeepalive, 从监控系统观察到,我们预期的 TIME_WAIT 过多的问题解决了; 但是,应用的响应时间RT在非正常的情况下,很平均地涨了40ms,由原来的4-5ms,涨到44-46ms之间。

摸索导致RT增加了40ms的原因

由于是对Agent增加了KeepAlive的支持,很自然地会去看 net 模块的文档,通过搜索keepAlive, 肯定会定位到socket.setKeepAlive()方法。

但是根据socket.setKeepAlive()方法的文档描述,和我们的问题没多大关联。

在屏幕中的socket.setNoDelay()吸引了我的视线,然后我看到Nagle algorithm, 然后想起之前看过的网络编程中Nagle算法和Delayed ACK的测试

文中描述的一段:

可以看到,每次请求到应答的时间间隔都在40ms,除了第一次。linux的delayed ack是40ms,而不是原来以为的200ms。第一次立即ACK,似乎跟linux的quickack mode有关,这里我不是特别清楚,有比较清楚的同学请指教。

40ms,和我们的问题现象一致!

重现 Nagle's Delayed ACK

按照网络编程中Nagle算法和Delayed ACK的测试 文中说到的 write-write-read 模式, 使用 http.request() + agentkeepalive@v0.1.0 模拟重现本文遇到的问题。

模拟程序分为 客户端(Client) 和 服务端(Server) 两个角色。

重现过程:Client 发送支持 Keepalive 的HTTP POST请求,然后等待 Server 响应数据。

服务端(Server)

nagle_delayed_ack_server.js

var http = require('http');
var microtime = require('microtime');

http.createServer(function (req, res) {
  var size = 0;
  var start = microtime.now();
  req.on('data', function (data) {
    size += data.length;
  });
  req.on('end', function () {
    var use = microtime.now() - start;
    res.end(JSON.stringify({
      port: req.connection.remotePort,
      size: size,
      headers: req.headers,
      method: req.method,
      url: req.url,
      use: use,
    }));
    console.log('[%sμs] %s:%s %s %s',
      use, req.connection.remoteAddress, req.connection.remotePort, req.method, req.url);
  });
}).listen(1984);

客户端(Client)

nagle_delayed_ack_client.js

// npm install agentkeepalive@0.1.0: should reappear the delay problem
// npm install agentkeepalive@0.1.1+: should slove the delay problem
var Agent = require('agentkeepalive');
var http = require('http');
var microtime = require('microtime');

var agent = new Agent();

function request(callback) {
  var options = {
    host: 'localhost',
    port: 1984,
    path: '/fengmk2',
    method: 'POST',
    agent: agent
  };
  var start = microtime.now();
  var req = http.request(options, function (res) {
    res.on('end', function () {
      var use = microtime.now() - start;
      console.log('[%sμs] %s', use, res.statusCode);
      callback();
    });
  });
  req.write('foo');
  process.nextTick(function () {
    req.end('bar');
  });
}

function next() {
  setTimeout(function () {
    request(next);
  }, 1000);
}

next();

分别运行 Server 和 Client,输出如下:

重新 Delayed Ack 的场景

可以看到,第一次请求不会等待Ack,但是从第二次请求开始,rt都增加了40ms,就是我们遇到的问题。

Server

$ node nagle_delayed_ack_server.js

[402μs] 127.0.0.1:6666 POST /fengmk2
[38971μs] 127.0.0.1:6666 POST /fengmk2
[39685μs] 127.0.0.1:6666 POST /fengmk2
[40662μs] 127.0.0.1:6666 POST /fengmk2
[40519μs] 127.0.0.1:6666 POST /fengmk2
[39600μs] 127.0.0.1:6666 POST /fengmk2
[39290μs] 127.0.0.1:6666 POST /fengmk2
[39101μs] 127.0.0.1:6666 POST /fengmk2
[39659μs] 127.0.0.1:6666 POST /fengmk2
[39685μs] 127.0.0.1:6666 POST /fengmk2

Client

$ node nagle_delayed_ack_client.js

[9659μs] 200
[40702μs] 200
[40306μs] 200
[41571μs] 200
[41388μs] 200
[40879μs] 200
[39812μs] 200
[40277μs] 200
[40328μs] 200
[40317μs] 200

解决问题

通常,找原因都是非常漫长的,而解决问题通常都是一瞬间的。

只需要在agentkeepalive中增加一行代码:

s.setNoDelay(true);

问题就解决了。

Comments

Fork me on GitHub