我们来说一说TCP神奇的40ms

51次阅读

共计 2299 个字符,预计需要花费 6 分钟才能阅读完成。

本文由云 + 社区发表
TCP 是一个复杂的协议,每个机制在带来优势的同时也会引入其他的问题。Nagel 算法和 delay ack 机制是减少发送端和接收端包量的两个机制,可以有效减少网络包量,避免拥塞。但是,在特定场景下,Nagel 算法要求网络中只有一个未确认的包,而 delay ack 机制需要等待更多的数据包,再发送 ACK 回包,导致发送和接收端等待对方发送数据,造成死锁,只有当 delay ack 超时后才能解开死锁,进而导致应用侧对外的延时高。其他文字已经介绍了相关的机制,已经有一些文章介绍这种时延的场景。本文结合具体的 tcpdump 包,分析触发 delay ack 的场景,相关的内核参数,以及规避的方案。
背景
给 redis 加了一个 proxy 层,压测的时候发现,对写入命令,数据长度大于 2k 后,性能下降非常明显,只有直连 redis-server 的 1 /10. 而 get 请求影响并不是那么明显。

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

22 号 tcp 请求包,42ms 后服务端才返回了 ack。初步怀疑是网络层的延时导致了耗时增加。Google 和 km 上找资料,大概的解释是这样:由于客户端打开了 Nagel 算法,服务端未关闭延迟 ack,会导致延迟 ack 超时后,再发送 ack,引起超时。
原理
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 个 MSS,立即发送;
如果之前没有包未被确认,立即发送;
如果之前有包未被确认,缓存发送内容;
如果收到 ack,立即发送缓存的内容。

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

基本原理是:

如果收到的数据内容大于一个 MSS,发送 ACK;
如果收到了接收窗口以为的数据,发送 ACK;
如果处于 quick mode,发送 ACK;
如果收到乱序的数据,发送 ACK;
其他,延迟发送 ACK

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

影响 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 的两端来入手。
服务端:
关闭 tcp_delay_ack, 这样,每个 tcp 请求包都会有一个 ack 及时响应,不会出现延迟的情况。操作方式:echo 1 > /proc/sys/net/ipv4/tcp_no_delay_ack 但是,每个 tcp 请求都返回一个 ack 包,导致网络包量的增加,关闭 tcp 延迟确认后,网络包量大概增加了 80%,在高峰期影响还是比较明显。

2. 设置 TCP_QUICKACK 属性。但是需要每次 recv 后再设置一次。对应我们的场景不太适合,需要修改服务端 redis 源码。
客户端:

关闭 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)); }

避免多次写,再读取的场景,合并成一个大包的写;避免一次请求分成多个包发送,最开始发送的包小于一个 MSS,对我们的场景,把第 22 号包的 1424 个字节缓存起来,大于一个 MSS 的时候,再发送出去,服务端立即返回响应,客户端继续发送后续的数据,完成交互,避免时延。

此文已由作者授权腾讯云 + 社区发布

正文完
 0