这篇文章上次修改于 899 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

1 现象

$ wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
Running 10s test @ http://192.168.0.30:8080/
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    43.60ms    6.41ms  56.58ms   97.06%
    Req/Sec     1.15k   120.29     1.92k    88.50%
  Latency Distribution
     50%   44.02ms
     75%   44.33ms
     90%   47.62ms
     99%   48.88ms
  22853 requests in 10.01s, 18.55MB read
Requests/sec:   2283.31
Transfer/sec:      1.85MB

在并发请求下,平均延迟为 43ms,延迟较大。

2 排查

使用 tcpdump 抓包:

$ tcpdump -nn tcp port 8080 -w nginx.pcap

用 Wireshark 打开 nginx.pcap。由于网络包数量较多,可以过滤一下,单击右键并选择 “Follow” -> “TCP Stream”。为了更加直观,继续点击菜单栏里的 Statics -> Flow Graph,选中 “Limit to display filter” 并设置 Flow type 为 “TCP Flows”:

第二次 HTTP 请求较慢,特别是客户端在收到服务器第一个分组后,40ms 后才发出了 ACK 响应(图中蓝色行)。

40ms 是 TCP 延迟确认(Delayed ACK)的最小超时时间。这是 TCP 针对 ACK 的优化机制,不用每次请求都发送一个 ACK,而是等一会(比如 40ms),看有没有“顺风车”,如果恰好有其它包需要发送,则捎带着 ACK 一起发送过去,否则就超时后发送。

因为 40ms 延迟发生在了客户端 wrk,所以有可能是客户端开启了延迟确认。

执行 man tcp,会发现只有 TCP 套接字专门设置了 TCP_QUICK 才会开启快速确认模式:

TCP_QUICKACK (since Linux 2.4.4) 
Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are sent 
immediately, rather than delayed if needed in accordance to normal TCP operation. This flag is not
permanent, it only enables a switch to or from quickack mode. Subsequent operation of the TCP protocol
will once again enter/leave quickack mode depending on internal protocol processing and factors such
as delayed ack timeouts occurring and data transfer. This option should not be used in code intended
to be portable.

利用 strace 查看 wrk 为套接字设置了哪些选项:

$ strace -f wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
...
setsockopt(52, SOL_TCP, TCP_NODELAY, [1], 4) = 0
...

可以看到并没有设置 TCP_QUICKACK 选项。但是服务端不应该受客户端的影响,回到 Wireshark 重新观察:

1173 是之前看到的 ACK 延迟包,697 包和 1175 包共同组成一个 HTTP 响应(ACK 都是 85)。第二个分组 1175 没有和前一个分组 697 一起发送,而是等 697 的 ACK 后才发送了 1175,这跟延迟确认比较像,只是不是 ACK,而是数据包。

这里可以想到 Nagle 算法,通过合并小包来提高带宽利用率。Nagle 算法规定,一个 TCP 连接上最多只有一个未被确认的未完成分组;在收到这个分组的 ACK 前不发送别的分组。这些小分组会被组合起来,在收到 ACK 后一起发送出去。

Nagle 算法跟客户端的默认延迟确认机制结合起来,就会导致延迟较大。

man tcp,会看到只有设置了 TCP_NODELAY 后才会禁用 Nagle 算法:

TCP_NODELAY
              If set, disable the Nagle algorithm.  This means that segments are always sent as soon as possible, even
              if there is only a small amount of data.  When not set, data is buffered until  there  is  a  sufficient
              amount  to  send out, thereby avoiding the frequent sending of small packets, which results in poor uti‐
              lization of the network.  This option is overridden by TCP_CORK; however, setting this option forces  an
              explicit flush of pending output, even if TCP_CORK is currently set.

查看 nginx 配置:

$ cat /etc/nginx/nginx.conf | grep tcp_nodelay
    tcp_nodelay    off;

将其设置为 on 应该就可以解决了。

重新测试后发现延迟已缩短到了 9ms。

参考

倪朋飞. Linux 性能优化实战.