接系列文章
前面我们在介绍基础理论知识的时候,【图九 TCP链接建立、传输和关闭示意】中最后四个数据报文就是TCP链接关闭的过程,俗称四次挥手,分手总是难以割舍的,所以链接建立只需三次握手,分手得要四次回首。
TCP设计目标是可靠传输,哪怕在分手时也得确保成功。为此,在TCP链接关闭阶段设计了繁杂的状态机,在【图十四 TCP状态变迁图】的左下角虚线框中的四个状态FIN_WAIT1、FIN_WAIT2、CLOSING、TIME_WAIT,代表着主动关闭TCP链接这一方的可能状态,前三个状态最终都会进入到等待响应最后一个FIN的ACK的这个阶段,即TIME_WAIT状态,并且在此停留2MSL(2倍Maximum Segment Lifetime,2倍报文段最大生存时间,RFC793规定MSL为2分钟,Linux Kernel中TCP/IP协议栈采用的是30秒,这个值的选择是有讲究的,它是一个物理上的约束,表示一个IP数据报文在地球上最长的存活时间,意思就是即便收不到这个ACK,也会给时间让它最终在地球的某个角落里消失)时长。这样处理的原因是在四次挥手过程中,主动关闭方需要确保自己最后发送响应对端FIN的ACK能被对端收到,如果对端出现超时重传了FIN,则意味着自己上次发的ACK丢失了,那么自己还有机会再次发送ACK确认,乘以2就是为了给重传的ACK充裕的到达时间。
真是太缠绵了,感天动地。在创造TCP/IP的年代,窄带宽、高时延、不稳定的网络状态,这样的设计相当必要,要分手也得大家都确认才行,爱情片里太多这样的误会了,不学习网络知识生活中是要吃大亏的。
【图十四 TCP状态变迁图】
回归正题,前面的基础知识告诉我们,只有TCP链接的主动关闭方会进入TIME_WAIT状态,这会给链接主动关闭方所在的TCP/IP协议栈带来什么样的影响呢。归纳一下主要有两个方面:
1) TCP/IP协议栈随机端口资源耗尽
铺垫一个基础知识:TCP对每个链接用一个四元组(TUPLE)来唯一标识,分别是源IP、目标IP、源端口、目标端口。通常在使用一个特定的目标服务时,目标IP(即服务器IP)和目标端口(即服务器知名/私有端口)是固定的,源IP通常也是固定的,因此链接主动发起方TUPLE的最大数量就由源端口的最大数量决定,TCP/IP v4规定端口号是无符号短整型,那么这个最大值就是65535。
假设一个服务器即作为TCP链接的主动打开方(通常是作为客户端角色,它使用本地随机分配的临时端口)又是TCP链接的主动关闭方,则大量主动关闭的链接会进入到TIME_WAIT状态,如果大伙在这个状态都折腾60秒(Linux MSL缺省为30秒,2MSL为60秒),这台机器相关的TUPLE资源会被快速占用、堆积并很快因为(源)端口的65535限制而耗尽,以后该TCP/IP协议栈上运行的其程序作为TCP链接的主动打开方再想链接同一个目标服务器时,就只能等待2MSL释放,从应用角度来看就是链接建立失败,用户要承受精神和肉体双重折磨,无法接受。
2) TCP/IP协议栈TUPLE相关数据结构大量消耗内存
假设一个服务器作为TCP链接的被动打开方(通常是作为服务器角色)和主动关闭方,则大量主动关闭的链接会进入到TIME_WAIT状态,如果大伙在这个状态都折腾60秒,本地机器TCP/IP协议栈维护的TUPLE数据项会快速堆积并占用大量内核内存资源,最关键的是因为此时TCP四元组碰撞概率极低(因为源IP、源端口大多都是不同的),导致TUPLE的积压几乎不受限制而野蛮生长,这对于一个高负载又要求高性能的服务器而言,感情上是相当痛苦的,肉体上勉强能接受。
基于以上分析,为了提高服务器网络效能,一些服务器选择配置启用TCP快速回收(真的需要配置吗,配置真的有效果吗,后面逐步会谈到)来优化性能。然而,新的问题出现了,三藏说:看,现在是妹妹要救姐姐,等一会那个姐姐一定会救妹妹的......恩恩怨怨何时了啊。
【问题1】如果客户端通过同一个NAT链接应用服务器时,客户端TCP链接可能被RESET拒绝或者无响应、响应缓慢。我们来具体分析一下成因,NAT作为代理层面向服务器时,客户端侧的源IP会被收敛成NAT的地址,通常有三种情况:
1) NAT为公网代理,比如公司内大伙用手机通过WIFI上网就属于这种模式,逻辑结构类似【图十五 客户端通过NAT上网示意】。另有一点背景交待:我们上网冲浪时发起的链接绝大多数都是短链接;
【图十五 客户端通过NAT上网示意】
2) NAT为后端服务器集群做四层或七层Load Balance(以下简称LB),比如HAProxy或LVS的四层NAT模式、Nginx的七层LB模式,典型场景是客户端HTTP请求经过LB转发到后端的服务器集群。LB与服务器集群之间大多也是采用短链接,逻辑结构类似【图十六 服务器通过NAT做LB】;
【图十六 服务器通过NAT做LB】
3) 上述第1和第2中情况的组合,具体可以参考【图十七 典型客户端连接服务器链路示意】,后面会有专门的讨论,此处不再赘述;
Linux Kernel的TCP/IP协议栈在开启TCP链接TIME_WAIT状态快速回收时,只需等待一个重传时间(RTO可能很短,甚至都来不及在netstat -ant中看到TIME_WAIT状态)后就释放而无需等待通常的2MSL超时。被释放的TCP链接的TUPLE信息同时也就就清除了。那么,问题来了,如果短时间内有新的TCP链接复用了这个TUPLE,就有可能会因为收到之前已释放的链接上,因延迟而刚刚到达的FIN,从而导致新链接被意外关闭。实际上,还会有链路被串接的问题。
为了规避这些问题,TCP/IP协议栈在快速回收释放TUPLE后,又利用IP层PEER(TCP/IP协议栈中维护的链接对端数据结构)信息中的对端IP、PEER最后一次TCP数据报文时间戳等信息(注:对端端口信息此时已经在TCP层被清除掉了),对TCP链接通过快速回收和重用TUPLE到新链接上做了一系列约束,在RFC1323中有相应的描述。简单讲就是在同时满足以下条件时,不能重用从TIME_WAIT状态快速回收的TUPLE,此时的表现是不响应或对SYN请求响应RESET:
1)来自同一台PEER机器的TCP链接数据报文中携带时间戳字段;
2)之前同一台PEER机器(仅仅指IP,端口信息因链接被TCP快速释放而缺失) 的某个TCP报文曾在60秒之内到过本服务器;
3)新链接的时间戳小于PEER机器上次TCP到来时的时间戳;
条件已经相当苛刻,碰撞概率应该很低了。但由于只有PEER的IP而缺少PEER的端口信息作为判断TCP链接另一端唯一性的约束,不能重用的概率便放大了65535倍。假设PEER是一台单独的机器,问题不大,因为一台机器上的时间戳是单调增长的,一旦出现时光倒流,则可以确定是旧的数据报文延迟了,直接丢掉即可。但是,如果很多客户端通过同一台NAT设备接入进来,那么问题就严重了,因为工作在四层的NAT不会修改客户端发送的TCP报文内的时间戳,而客户端们各自的时间戳又无法保持一致,服务器只认时间戳最大的那个,其它通通丢掉或者对SYN请求直接响应RESET,太冤了。
我们的业务服务中,典型模式是客户端使用HTTP短链接通过接入服务器使用业务服务,且这些接入服务器基本都是以LB方式在运行,接入服务器与业务服务器之间则大多为直接链接或通过代理调度,无论是有线互联网的B/S架构,还是移动互联网的C/S架构都是如此。客户端用户也大多数都是通过NAT上网的。参考【图十七 典型客户端连接服务器链路示意】可以有更直观的了解。
【图十七 典型客户端连接服务器链路示意】
基于前述知识,我们以【图十七 典型客户端连接服务器链路示意】为基础来观察,可以分三种情况讨论快速回收配置参数的合理使用:
1) 链接主动打开方和主动关闭方均为客户端
a. 如服务器LB工作在七层且在公网提供服务,则它与HTTP服务器集群之间一般都是短链接,此时,服务器LB符合随机端口资源耗尽的模式。因为它的时间戳是单调递增的,故无需担心链接碰撞,符合 TCP快速回收重用的条件,但由于服务器LB部署在公网对客户端提供服务,客户端有可能通过NAT代理访问外部网络,便无法保证时间戳单调递增,故建议关闭TCP快速回收选项;
b. 如服务器LB工作在四层模式,自身不受影响,故关闭TCP快速回收选项;
c. HTTP服务器集群与层级靠后的业务服务器之间大多都是短链接,HTTP服务器的情况与前述第a点类似,如果它在七层服务器LB之后部署,且与层级靠后的业务服务器之间没有NAT,则可以考虑启用TCP快速回收选项,除此之外,都建议关闭TCP快速回收选项;
2) 内网服务器(业务服务器、逻辑代理服务器等)之间有相互调用时,建议优先采用长链接方案。如果确实需要使用短链接方案时,则层级靠前的服务器往往即是链接的主动打开方,又是链接的主动关闭方,符合随机端口资源耗尽的模式。考虑到单台服务器能确保自己时间戳单调递增,开启tcp_tw_recycle也能符合TCP快速回收重用的条件,且不用担心碰撞,因此建议启用TCP快速回收选项。这里需要注意两个特殊情况:
a. 如果层级靠前的服务器有一端直接在公网为客户端提供服务,而客户端有可能通过NAT代理访问外部网络,则不宜启用TCP快速回收选项;
b. 如果层级靠前的服务器与层级靠后的服务器之间有四层NAT隔离,也需要谨慎考虑。除非服务器间系统时钟同步精准,能确保层级靠前的服务器集群总体时间戳在毫秒级的精度上能单调递增,否则建议关 闭TCP快速回收选项;
3) 服务器集群被模拟客户端逻辑攻击,此时服务器会主动关闭链接,从而导致大量出现TIME_WAIT状态,服务器因此符合TCP/IP协议栈TUPLE相关数据结构内存大量消耗的模式但,考虑到客户端可能处在NAT之后,建议保持关闭TCP快速回收选项。我们应利用提前部署的安全机制在TCP三次握手期间及早拒绝链接来解决此类问题;
服务端系统架构千变万化,较难穷举,总结一下上述的讨论:
1) 服务器如果直接在公网服务于客户端时,因为客户端有可能通过NAT代理访问外部网络,故建议关闭TCP快速回收选项;
2) 服务器各层级在内网互联时,同时作为链接的主动发起方和链接的主动关闭方,建议开启TCP快速回收。上述建议例外场景是:如服务器层级之间有4层NAT,则需要考察层级靠前的服务器集群时钟同步的精度水平是否能到毫秒级,通常建议关闭TCP快速回收选项;
【问题2】CMWAP转发的包时间戳有乱跳的情况,也会遇到类似问题1的现象。因为现在WAP的用户越来罕见,就不展开了;
⑥ HTTP协议:打开SOCKET的TCP_NODELAY选项
TCP/IP协议栈为了提升传输效率,避免大量小的数据报文在网络中流窜造成拥塞,设计了一套相互协同的机制,那就是Nagle's Algorithm和TCP Delayed Acknoledgement。
Nagle算法(Nagle's Algorithm)是以发明人John Nagle的名字来命名。John Nagle在1984年首次用这个算法来尝试解决福特汽车公司的网络拥塞问题(RFC 896),该问题的具体描述是:如果我们的应用程序一次产生1个字节的数据(典型的如telnet、XWindows等应用),而这个1个字节数据又以网络数据包的形式发送到远端服务器,那么就很容易使网络中有太多微小分组而导致过载。
因为传输1个字节有效数据的微小分组却需花费40个字节的额外开销(即IP包头20字节 + TCP包头20字节),这种有效载荷利用率极其低下的情况被统称为愚蠢窗口症候群(Silly Window Syndrome),前面我们在谈MSS时也提到过,如果为一头猪开个大卡车跑一趟,也够愚钝的。对于轻负载广域网或者局域网来说,尚可接受,但是对于重负载的广域网而言,就极有可能引起网络拥塞导致瘫痪。
Nagle算法要求一个TCP链接上最多只能有一个未被确认的小分组(数据长度小于MSS的数据包),在该分组的确认到达之前不能再发送其它小分组。此时如果应用层再有新的写入数据,TCP/IP协议栈会搜集这些小分组并缓存下来,待以下时机发出:
1) 收到接收端对前一个数据报文的ACK确认;
2) 当前数据属于紧急数据;
3) 搜集的数据达到或超过MSS;
【图十八 Nagle算法未开启和开启数据报文交互示意】对比了Nagle算法未开启(左侧图示)和开启(右侧图示)的数据报文交互过程。
【图十八 Nagle算法未开启和开启数据报文交互示意】
TCP Delayed Acknoledgement 也是为了类似的目的被设计出来的,它的作用就是延迟ACK包的发送,使得TCP/IP协议栈有机会合并多个ACK或者使ACK可以随着响应数据一起返回,从而提高网络性能。TCP Delayed Acknoledgement定义了一个超时机制,默认超时时间是40ms,超过这个时间,则不再等待立即发送延迟的ACK。
如果一个TCP连接的一端启用了Nagle's Algorithm,而另一端启用了TCP Delayed Acknoledgement,而发送的数据包又比较小,则可能会出现这样的情况:发送端在等待接收端对上一个数据报文的ACK才发送新的数据报文,而接收端则正好延迟了这个ACK的发送,那么正要被发送的新数据报文也就同样被延迟了。
上述情况出现的前提是TCP连接的发送端连续两次调用写SOCKET接口,然后立即调用读SOCKET接口时才会出现。那么为什么只有 Write-Write-Read 时才会出现问题,我们可以分析一下Nagle's Algorithm的伪代码:
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
代码显示,当待发送的数据比 MSS 小时,先判断此时是否还有未ACK确认的数据报文,如果有则把当前写的数据放入写缓冲区,等待上个数据报文的ACK到来。否则立即发送数据。对于Write-Write-Read的调用秩序,发送端第一个Write会被立刻发送,此时接收端TCP Delayed Acknoledgement机制期待更多的数据到来,于是延迟ACK的发送。发送端第二个Write会命中发送队列中还有未被ACK确认的数据的逻辑,所以数据被缓存起来。这个时候,发送端在等待接收端的ACK,接收端则延迟了这个ACK,形成互相等待的局面。后面等到接收端延迟ACK超时(比如40ms),接收端就会立即发出这个ACK,这才能触使发送端缓存的数据报文被立即发出。
现代TCP/IP 协议栈默认几乎都启用了这两个功能。
我们在移动APP的设计实现中,请求大部分都很轻(数据大小不超过MSS),为了避免上述分析的问题,建议开启SOCKET的TCP_NODELAY选项,同时,我们在编程时对写数据尤其要注意,一个有效指令做到一次完整写入(后面会讲协议合并,是多个指令一次完整写入的设计思想),这样服务器会马上有响应数据返回,顺便也就捎上ACK了。
接下文《 海量之道系列文章之弱联网优化 (五)》
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。