再说TCP神奇的40ms

版权声明:本文由安斌原创文章,转载请注明出处: 
文章原文连接:https://www.qcloud.com/community/article/186html

来源:腾云阁 https://www.qcloud.com/communitynode

 

TCP是一个复杂的协议,每一个机制在带来优点的同时也会引入其余的问题。 Nagel算法和delay ack机制是减小发送端和接收端包量的两个机制, 能够有效减小网络包量,避免拥塞。可是,在特定场景下, Nagel算法要求网络中只有一个未确认的包, 而delay ack机制须要等待更多的数据包, 再发送ACK回包, 致使发送和接收端等待对方发送数据, 形成死锁, 只有当delay ack超时后才能解开死锁,进而致使应用侧对外的延时高。 其余文字已经介绍了相关的机制, 已经有一些文章介绍这种时延的场景。本文结合具体的tcpdump包,分析触发delay ack的场景,相关的内核参数, 以及规避的方案。linux

背景

给redis加了一个proxy层, 压测的时候发现, 对写入命令,数据长度大于2k后, 性能降低很是明显, 只有直连redis-server的1/10. 而get请求影响并非那么明显。
git

分析

观察系统的负载和网络包量状况, 都比较低, 网络包量也比较小, proxy内部的耗时也比较短。 无赖只能祭出tcpdump神奇, 果真有妖邪。github

22号tcp请求包, 42ms后服务端才返回了ack。 初步怀疑是网络层的延时致使了耗时增长。Google和km上找资料, 大概的解释是这样: 因为客户端打开了Nagel算法, 服务端未关闭延迟ack, 会致使延迟ack超时后,再发送ack,引发超时。redis

原理

Nagel算法,转自维基百科算法

if there is new data to send

  if the window size >= MSS and available data is >= MSS

    send complete MSS segment now

  else

    if there is unconfirmed data still in the pipe

      enqueue data in the buffer until an acknowledge is received

    else

      send data immediately

    end if

  end if

end if

简单讲, Nagel算法的规则是:缓存

  1. 若是发送内容大于1个MSS, 当即发送;
  2. 若是以前没有包未被确认, 当即发送;
  3. 若是以前有包未被确认, 缓存发送内容;
  4. 若是收到ack, 当即发送缓存的内容。

延迟ACK的源码以下:net/ipv4/tcp_input.c

基本原理是:网络

  1. 若是收到的数据内容大于一个MSS, 发送ACK;
  2. 若是收到了接收窗口觉得的数据, 发送ACK;
  3. 若是处于quick mode, 发送ACK;
  4. 若是收到乱序的数据, 发送ACK;
  5. 其余, 延迟发送ACK

其余都比较明确, quick mode是怎么判断的呢? 继续往下看代码:
socket

影响quick mode的一个因素是 ping pong的状态。 Pingpong是一个状态值, 用来标识当前tcp交互的状态, 以预测是不是W-R-W-R-W-R这种交互式的通信模式, 若是处于, 能够用延迟ack, 利用Read的回包, 将Write的回包, 捎带给发送方。

如上图所示, 默认pingpong = 0, 表示非交互式的, 服务端收到数据后, 当即返回ACK, 当服务端有数据响应时,服务端将pingpong = 1, 之后的交互中, 服务端不会当即返回ack,而是等待有数据或者ACK超时后响应。

问题

按照前面的的原理分析,应该每次都有ACK延迟的,为何咱们测试小于2K的数据时, 性能并无受到影响呢?
继续分析tcpdump包:

按照Nagel算法和延迟ACK机制, 上面的交互以下图所示, 因为每次发生的数据都包含了完整的请求, 服务端处理完成后, 向客户端返回命令响应时, 将请求的ACK捎带给客户端,节约一次网络包。

再分析2K的场景:

以下表所示, 第22个包发送的数据小于MSS, 同时,pingpong = 1, 被认为是交互模式, 期待经过捎带ACK的方式来减小网络的包量。 可是, 服务端收到的数据,并非一个完整的包,不能产生一次应答。服务端只能在等待40ms超时后,发送ACK响应包。
同时,从客户端来看,若是在发送一个包, 也能够打破已收数据 > MSS的限制。 可是,客户端受Nagel算法的限制, 一次只能有一个包未被确认,其余的数据只能被缓存起来, 等待发送。

触发场景

一次tcp请求的数据, 不能在服务端产生一次响应,或者小于一个MSS

规避方案

只有同时客户端打开Nagel算法, 服务端打开tcp_delay_ack才会致使前面的死锁状态。 解决方案能够从TCP的两端来入手。

服务端:

  1. 关闭tcp_delay_ack, 这样, 每一个tcp请求包都会有一个ack及时响应, 不会出现延迟的状况。 操做方式:
    echo 1 > /proc/sys/net/ipv4/tcp_no_delay_ack
    可是, 每一个tcp请求都返回一个ack包, 致使网络包量的增长,关闭tcp延迟确认后, 网络包量大概增长了80%,在高峰期影响仍是比较明显。
  2. 设置TCP_QUICKACK属性。 可是须要每次recv后再设置一次。 对应咱们的场景不太适合,须要修改服务端redis源码。

客户端:

  1. 关闭nagel算法,即设置socket tcp_no_delay属性。
    static void _set_tcp_nodelay(int fd) {
    int enable = 1;
    setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof(enable));
    }
  2. 避免屡次写, 再读取的场景, 合并成一个大包的写;避免一次请求分红多个包发送, 最开始发送的包小于一个MSS,对咱们的场景, 把第22号包的1424个字节缓存起来, 大于一个MSS的时候,再发送出去, 服务端当即返回响应, 客户端继续发送后续的数据, 完成交互,避免时延。

参考资料:
http://jerrypeng.me/2013/08/mythical-40ms-delay-and-tcp-nodelay/
http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201231214038740/
http://blog.chinaunix.net/uid-28387257-id-3658980.html
https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_input.c

相关文章
相关标签/搜索