Nagle和Delayed ACK优化算法合用导致的死锁问题

  前面说了TIME_WAIT的问题,这里再讨论网络开发中另外一个常见的奇怪现象——Nagle算法和Delayed ACK相互作用产生“死锁”导致网络性能下降的问题。这个问题算是网络服务器开发中较容易遇同时又十分严重的问题,会严重影响服务的响应时间和吞吐量,之前在网上看一些博客文章,发现连续好几篇都是描述这类问题的文章,Google两个关键字也会发现相关文章和解决方法大把大把的,他们描述问题的现象基本表现为规律性的几十到几百毫秒的确认延时。
  其实,这两种机制原本的初衷都是为了优化TCP传输效率、减少网络中低有效负载的小包数量的,减少网络拥塞提高传输效率的。这两种算法是由两个独立的团队在几乎相近的时间,分别从发送端和接收端的角度提出的优化机制,但是如果两者同时使用常常就会出现上面提到的问题。

一、算法介绍

1. Nagle算法

  Nagle算法的初衷是从发送端解决网络传输小数据包问题的,目的就是为了解决像Telnet这类应用程序性能而进行的优化。因为一个TCP数据包的传输至少需要固定的40字节头部信息(20字节TCP + 20字节IP),如果数据包实际负载都比较小的话,那么传输的效率就非常低,但是如果将这些小包的负载都尽量集中起来,封装到一个TCP数据包中进行传输,那么传输效率势必将会大大提高。此处我们再次强调,TCP传输的是一个字节流,本身不存在所谓的离散形式的数据包的概念,协议可以任意组合、拆分每次调用实际传输的数据长度。
  在Nagle算法中参数MSS(maximum segment size,IPv4默认值是576-20-20 = 536)扮演者重要的作用,其算法流程也简洁明了:
nagle
  概括地说来,其流程表述为:(a)不考虑窗口流量控制的限制,一旦累积的数据达到MSS就立即执行传输;(b)否则如果当前有未ACK的数据,就将数据堆积到发送队列里延迟发送;(c)如果没有待需要ACK的数据,就立即发送。简单说来,就是在数据没有累积到MSS的大小情况下,整个连接中允许有未ACK的数据。
  Nagel算法本质上就是个时间换带宽的方法,所以对于那些带宽要求不大但对实时性要求高的程序,比如类似网络游戏类,需要使用TCP_NODELAY这个socket选项来关闭这个特性以减小延时发生。不过话外说来,对于这类程序或许使用UDP协议也是个选择。

2. Delayed ACK机制

  TCP协议可靠性传输来源于确认机制:当发送端发送完数据后,接收端通过回复带有下一次待接收序列号的ACK,便实现累计确认收到前面的数据,ACK报文可以单独传输也可以在普通数据包中传输,不过如果ACK仅仅确认而不带有效负载的话,一次通信还是需要额外的40字节头信息,所以单纯确认会造成大量的带宽浪费。
  Delayed ACK的思路就是,在接收到了一个TCP数据包后,不立即进行确认而是进行一个超时等待(Linux的默认40ms,Windows的200ms,而部分实现可能会更长,但是协议规定最多不会超过500ms),直到至少满足以下列条件之一,否则一直等到超时后才进行ACK确认:(1)等待期间内接收端又收到一个TCP包,此时接收端将之前pending的一个ACK与当前这个ACK合并后发送,这样只需要发送一个ACK从而节省了一次ACK操作;(2)在超时之前接收端有数据要返回给发送端,那么在这个普通TCP数据包中仅仅需要设置ACK标志和确认序列号就可以捎带过去,完全搭了一把顺风车。
  该机制是从接收端的角度进行的优化,而且更像是一种赌博:如果在确认超时之内能再次收到发送端的数据,亦或者接收端有数据返回发送端,就可以节省一次ACK确认,否则就要浪费这个超时等待而且得不到任何好处,而且毫无疑问会人为地增加响应延时。
  如果觉得Delayed ACK造成的延时不能接受,则可以通过TCP_QUICKACK选项禁用该特性。

二、使用注意事项

  Nagel和Delayed ACK的初衷都是好的,但是现实中如果服务端开启Delayed ACK,同时客户端也开启Nagel算法的时候,两种机制合用起来就会带来一些问题,举例详细说来:
  比如客户端发起HTTP POST请求,请求体比较大的时候可能需要将HEADER头部和BODY分两次发送(或者HEADER头部和一部分BODY第一次发送,另外一部分BODY第二次发送,和我们这里描述的原理都没有关系)。当客户端发送P1到达服务端;服务端接收到P1后解析HEADER发现需要更多的数据才能响应这个请求,而Delayed ACK机制此时生效,他会等待P2的到来进行再累计ACK,或者等待定时器超时后才主动ACK;因为服务端没有发送ACK1,并且P2如果正巧没有达到MSS,此时客户端Nagel算法生效,不会立即发送P2而是等待服务端的确认。此时客户端和服务端都在等待对方,形成了“死锁”,此时只有等到服务端Delayed ACK定时器超时才会打破这个死锁,主动向客户端ACK后,客户端再次发送剩余的P2,服务端接收到完整数据后产生响应返回给客户端,整个交互响应过程被硬生生的增加了一个Delayed ACK超时。
  所以如果在网络或服务中观察到有许多规律性、固定的几十或者几百毫秒延迟响应出现,那么就该警觉是否可能是上文描述的这种情况了。不过现在很多TCP实现在Nagle算法的实现上也进行了一些变通,他们会按照应用层每次发送而不是每个底层分段来解释Nagle算法,比如MSS为1460字节,应用程序一次写入1600字节,按照原来的算法发送1460后需要等待ACK后才能发送剩余的140字节,而有些实现会将这一整个发送请求来应用Nagle算法,结果就是发送1460字节后会紧接着发送剩余的140字节,那么这样的实现可以规避我们之前描述的问题。

  当然,我们作为开发者平时也应该养成良好的开发习惯,在网络开发中需要多次发送数据的时候,应当避免执行多次小规模的写请求,而是将数据汇总起来形成一个大数据包再一次性发送出去,这样一方面可以降低系统调用的开销,同时等于在用户态实现了类似Nagle算法所需要解决的问题,增加网络利用率的同时还避免了Nagle可能带来的副作用。

本文完!

参考