TCP流量控制
延迟确认
根据TCP协议的设定,发送端每发出一个数据段,接收端就要返回一个ACK。如果发送端和接收端本就存在频繁的数据互动,不携带数据的ACK
无疑会浪费半个RTT
的时间。
所以,可以在接收到数据之后,稍微等待一段时间(少于200ms),如果这时接收端正好也有数据要发送,将其ACK
位置1,这样我们就同时完成了数据发送和对之前的确认。
这看起来会会造成一定的延时,但现实中一些基于TCP的"回合制"协议,如HTTP协议,客户端向服务器发送请求后,服务器必须直到客户端的请求,才能给予回应(状态码+Body等),当服务器从缓冲区拿出请求时,这部分信息必然已经被确认,才可能来到应用层,这时启用延迟确认并无意义。
与延迟确认(Delay Ack)对应的,是快速确认(Quick Ack),Linux在默认情况下打开延迟确认,可以手动设置TCP_QUICKACK
来禁用延迟确认, TCP_QUICKACK
默认关闭,需要用时每次都要手动重置。
#define TCP_DELACK_MAX ((unsigned)(HZ/5)) /* maximal time to delay before sending an ACK */
#if HZ >= 100
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
Nagle算法
Nagle算法要求一个TCP连接中有在传数据时,小的报文段就不能被发送(小于SMSS),当发送数据还没有完全得到确认之前,尽可能收集小的数据段,并在下次将其一起发送。这是一种自时钟算法,RTT越小,ACK就越快,发送速度也就越快,在低RTT环境中,Nagle并不会有显著影响。
Golang中默认禁用Nagle算法,需要时应当手动打开:
tcpConn, _ := conn.(*net.TCPConn)
tcpConn.SetNoDelay(noDelay)
Nagle算法能够在RTT较高的环境中避免传输太小的数据报,在与延迟确认结合使用时,由于TCP连接中只要有在传数据就不能发送小报文段,会让TCP连接变成一种类似竞态资源的存在,最长可能延迟到TCP_DELACK_MAX
,即最大延迟确认的超时,在Linux中HZ=100
时,为HZ/5=20ms
。
这使得Nagle算法并不适合小数据报文需要及时传输的场景,例如游戏中的按键操作。Nagle对于传输大文件是无效的,对于HTTP 1.1这种一次性会话也是无效的。
流量控制与窗口管理
TCP的流量控制目的是协调发送端与接收端两者之间的发送/接收速率,使之能够匹配对方的能力。如果接收端缓冲区已满,将会丢弃后续接收到的数据包,白白浪费发送效率。两端的收发数据量是通过窗口结构来协调的,就像一卷很长的胶卷,我们通过一个小窗不断在胶卷移动,发送端在这个窗口里放已经发送但还没确认的数据,以及待发送的数据;接收端在窗口里存放还没有上传到应用层的数据包,比如还没有排序的数据,这就是滑动窗口。
当接收端缓冲区已满时,接收端将会向发送端发送一个零窗口通告,告知其停止继续发送数据。当发送端收到零窗口通告后,停止发送数据,并随后每隔一段时间发送窗口探测,来尝试恢复传输。
糊涂窗口综合征(Silly Window Syndrome, SWS),是指由于发送端和接收端的一些行为,导致窗口小于实际可用窗口,或在窗口中每次只发送很少的数据,导致数据传输效率降低的现象。接收端可以避免通告较小的窗口,发送端可以尝试组合较小的报文使其成为全长。
Linux的缓存自动调优
流量缓存是在内核层面进行调控的,默认情况下,Linux对于每个TCP连接都有自动调优机制。
通过sysctl <参数名>
可以查看默认的TCP窗口调优参数
接收缓存
TCP接收缓存,分为 min, default, max三个值, 单位为字节。
其中 default 会覆盖 net.core.rmem_default
max 不能超过 net.core.rmem_max
,如果通过设置SO_RCVBUF
设置了具体值,这个最大值就无法生效。
net.ipv4.tcp_rmem = min default max
发送缓存
发送缓存与接收类似, 也分为三个值, max不能高于 net.core.wmem_max
, default
会覆盖net.core.rmem_default
。
如果通过设置SO_SNDBUF
来设定了缓存大小,最大值设定会失效,转而使用设置的值,
net.ipv4.tcp_wmem = min default max
大内存与大宽带的调优
在内存比较充裕、宽带足够的情况下,可以调大上述参数来进行调优。在Golang中,可以通过手动设定缓存大小来覆盖掉系统中的最大缓存:
conn.SetReadBuffer(1024)
conn.SetWriteBuffer(1024)
每个TCP连接都会消耗一定内存。需要注意的是在传输效率不高的情况下,调高缓存并不能有效的提高传输效率。在带宽较高时,我们可以通报较大的初始窗口(min, default),避免窗口过小导致传输效率下降。