1. 查看系统网络配置和当前TCP状态
在定位并处理应用程序出现的网络问题时,了解系统默认网络配置是非常必要的。以x86_64平台Linux kernelversion 2.6.9的机器为例,ipv4网络协议的默认配置可以在/proc/sys/net/ipv4/下查看,其中与TCP协议栈相关的配置项均以tcp_xxx命名,关于这些配置项的含义,请参考这里的文档,此外,还可以查看linux源码树中提供的官方文档(src/linux/Documentation/ip-sysctl.txt)。下面列出我机器上几个需重点关注的配置项及其默认值:
cat /proc/sys/net/ipv4/ip_local_port_range 32768 61000
cat /proc/sys/net/ipv4/tcp_max_syn_backlog 1024
cat /proc/sys/net/ipv4/tcp_syn_retries 5
cat /proc/sys/net/ipv4/tcp_max_tw_buckets 180000
cat /proc/sys/net/ipv4/tcp_tw_recycle 0
cat /proc/sys/net/ipv4/tcp_tw_reuse 0
其中,前3项分别说明了local port的分配范围(默认的可用端口数不到3w)、incomplete connection queue的最大长度以及3次握手时SYN的最大重试次数,这3项配置的含义,有个概念即可。后3项配置的含义则需要理解,因为它们在定位、解决问题过程中要用到,下面进行重点说明。
1) tcp_max_tw_buckets
这篇文档 是这样描述的:Maximal number of time wait sockets held by system simultaneously. If this number is exceeded TIME_WAIT socket is immediately destroyed and warning is printed. This limit exists only to prevent simple DoS attacks, you must not lower the limit artificially, but rather increase it (probably, after increasing installed memory), if network conditions require more than default value (180000).
可见,该配置项用来防范简单的DoS攻击 ,在某些情况下,可以适当调大,但绝对不应调小,否则,后果自负。。。
2) tcp_tw_recycle
Enable fast recycling of sockets in TIME-WAIT status. The defaultvalue is 0 (disabled). It should not be changed without advice/request of technical experts.
该配置项可用于快速回收处于TIME_WAIT状态的socket以便重新分配。默认是关闭的,必要时可以开启该配置。但是开启该配置项后,有一些需要注意的地方,本文后面会提到。
3) tcp_tw_reuse
Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. The default value is 0. It should not
be changed without advice/request of technical experts.
开启该选项后,kernel会复用处于TIME_WAIT状态的socket,当然复用的前提是“从协议角度来看,复用是安全的”。关于“ 在什么情况下,协议认为复用是安全的 ”这个问题,这篇文章 从linux kernel源码中挖出了答案,感兴趣的同学可以查看。
2. 网络问题定位思路
参考前篇笔记开始处描述的线上实际问题,收到某台机器无法对外建立新连接的报警时,排查定位问题过程如下:
用netstat –at | grep “TIME_WAIT”统计发现,当时出问题的那台机器上共有10w+处于TIME_WAIT状态的TCP连接,进一步分析发现,由报警模块引起的TIME_WAIT连接有2w+。将netstat输出的统计结果重定位到文件中继续分析,一般会看到本机的port被大量占用。
由本文前面介绍的系统配置项可知,tcp_max_tw_buckets默认值为18w,而ip_local_port_range范围不到3w,大量的TIME_WAIT状态使得local port在TIME_WAIT持续期间不能被再次分配,即没有可用的local port,这将是导致新建连接失败的最大原因。
在这里提醒大家:上面的结论只是我们的初步判断,具体原因还需要根据代码的异常返回值(如socket api的返回值及errno等)和模块日志做进一步确认。无法建立新连接的原因还可能是被其它模块列入黑名单了,本人就有过这方面的教训:程序中用libcurl api请求下游模块失败,初步定位发现机器TIME_WAIT状态很多,于是没仔细分析curl输出日志就认为是TIME_WAIT引起的问题,导致浪费了很多时间,折腾了半天发现不对劲后才想起,下游模块有防攻击机制,而发起请求的机器ip被不在下游模块的访问白名单内,高峰期上游模块通过curl请求下游的次数太过频繁被列入黑名单,新建连接时被下游模块的TCP层直接以RST包断开连接,导致curl api返回”Recv failure: Connection reset by peer”的错误。惨痛的教训呀 =_=
另外,关于何时发送RST包,《Unix Network Programming Volume 1》第4.3节做了说明,作为笔记,摘出如下:
An RST is a type of TCP segment that is sent by TCP when somethingis wrong.Three conditions that generatean RST are:
1) when a SYN arrives for a port that has no listening server;
2) when TCP wants to abort an existing connection;
3) when TCP receives a segment for a connection that does not exist. (TCPv1 [pp.246–250] contains additional information.)
3. 解决方法
可以用两种思路来解决机器TIME_WAIT过多导致无法对外建立新TCP连接的问题。
3.1 修改系统配置
具体来说,需要修改本文前面介绍的tcp_max_tw_buckets、tcp_tw_recycle、tcp_tw_reuse这三个配置项。
1)将tcp_max_tw_buckets调大,从本文第一部分可知,其默认值为18w(不同内核可能有所不同,需以机器实际配置为准),根据文档,我们可以适当调大,至于上限是多少,文档没有给出说明,我也不清楚。个人认为这种方法只能对TIME_WAIT过多的问题起到缓解作用,随着访问压力的持续,该出现的问题迟早还是会出现,治标不治本。
2)开启tcp_tw_recycle选项:在shell终端输入命令”echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle”可以开启该配置。
需要明确的是:其实TIME_WAIT状态的socket是否被快速回收是由tcp_tw_recycle和tcp_timestamps两个配置项共同决定的,只不过由于tcp_timestamps默认就是开启的,故大多数文章只提到设置tcp_tw_recycle为1。更详细的说明(分析kernel源码)可参见这篇文章。
还需要特别注意的是:当client与server之间有如NAT这类网络转换设备时,开启tcp_tw_recycle选项可能会导致server端drop(直接发送RST)来自client的SYN包。具体的案例及原因分析,可以参考这里、这里或这里以及这里的分析,本文不再赘述。
3)开启tcp_tw_reuse选项:echo1 > /proc/sys/net/ipv4/tcp_tw_reuse。该选项也是与tcp_timestamps共同起作用的,另外socket reuse也是有条件的,具体说明请参见这篇文章。查了很多资料,与在用到NAT或FireWall的网络环境下开启tcp_tw_recycle后可能带来副作用相比,貌似没有发现tcp_tw_reuse引起的网络问题。
3.2 修改应用程序
具体来说,可以细分为两种方式:
1)将TCP短连接改造为长连接。通常情况下,如果发起连接的目标也是自己可控制的服务器时,它们自己的TCP通信最好采用长连接,避免大量TCP短连接每次建立/释放产生的各种开销;如果建立连接的目标是不受自己控制的机器时,能否使用长连接就需要考虑对方机器是否支持长连接方式了。
2)通过getsockopt/setsockoptapi设置socket的SO_LINGER选项,关于SO_LINGER选项的设置方法,《UNP Volume1》一书7.5节给出了详细说明,想深入理解的同学可以去查阅该教材,也可以参考这篇文章,讲的还算清楚。
4. 需要补充说明的问题
我们说TIME_WAIT过多可能引起无法对外建立新连接,其实有一个例外但比较常见的情况:S模块作为WebServer部署在服务器上,绑定本地某个端口;客户端与S间为短连接,每次交互完成后由S主动断开连接。这样,当客户端并发访问次数很高时,S模块所在的机器可能会有大量处于TIME_WAIT状态的TCP连接。但由于服务器模块绑定了端口,故在这种情况下,并不会引起“由于TIME_WAIT过多导致无法建立新连接”的问题。也就是说,本文讨论的情况,通常只会在每次由操作系统分配随机端口的程序运行的机器上出现(每次分配随机端口,导致后面无端口可用)。