3

服务端不回应客户端的syn握手,连接建立失败原因排查

 1 year ago
source link: https://www.cnblogs.com/grey-wolf/p/17636444.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

背景#

测试环境有一个后台服务,部署在内网服务器A上(无外网地址),给app提供接口。app访问这个后台服务时,ip地址是公网地址,那这个请求是如何到达我们的内网服务器A呢,这块我咨询了网络同事,我画了简图如下:

image-20230816152413599

请求会直接打到防火墙上,防火墙对请求先做了DNAT转换(将目的地址转换为后台服务器的地址192.168.1.3),另外,为了确保后台服务处理完请求后,能正常返回响应,所以,防火墙还做了SNAT转换(将源地址转换为防火墙的内网地址192.168.1.2)。

其实我之前测试过,不做SNAT也可以正常回包,但我只是一个开发,网工这块并不了解,所以网络同事肯定是有自己的其他考虑,总之,外网发来的进入后台服务器的报文,其源ip都变成了防火墙的ip。

简单而言,这也是一个典型的NAT环境。

再来说,我们遇到啥问题。我们这次变更,在192.168.1.3这个服务器上,加了个openresty(nginx增强版本),由openresty承接请求,然后反向代理到后台服务。

结果,测试同事反馈,app发出去的一些包,在三次握手的第一次握手就失败了。

当时,是在后台服务的机器上抓包,发现:app侧,好几个请求发了syn,但是后端没有回应,然后一直重传syn,重传n次后放弃。

image-20230816153806813

我一想,这难道是本次引入的openresty组件的问题?这要是上线了还得了,赶紧查查。

排查过程#

先是自己在本地开发环境试了好久,app被我玩得死去活来,并没有复现问题。

由于本周测试同事休假了,然后,我自己在测试环境又玩了好久,还是没有复现问题,但是之前,测试同事复现了好些次,我当时也在场。怎么,这次我自己就复现不出来呢?

复现不出来就在网上随便逛逛,然后找到一些文章,说了一些可能的原因,至于是哪个原因,那得执行命令来确诊。

// 检查指标,看看有没有因为时间戳丢弃syn包的情况
netstat -s |egrep -e SYNs -e "time stamp"

image-20230816154705224

我一看,果然和网上说的对得上:tcp_tw_recycle参数在nat环境下,触发了linux的paws机制,导致丢包。

这个paws机制在nat环境下丢包,开启的前提是,服务器上打开了如下参数:

[root@VM-0-6-centos ~]# sysctl -a |egrep -e tcp_tw_recycle -e tcp_timestamps
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_recycle = 1

此时,我大概感觉就是这么个事情了,然后了解了这个问题场景后,果然在本地复现了,复现后就是照着改,然后问题就解决了。

解决不代表结束,我们得详细了解下这个机制,这个机制启动的参数中有一个net.ipv4.tcp_tw_recycle,这个参数,乍一看,是回收time_wait状态的socket,而在网上搜索linux time_wait时,出来的第一页的答案,很多都会跟你说,改下面的参数,改了就好了:

vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30

改了有什么后果呢,不知道。这就在某些场景下埋下了隐患。要讲清楚这个问题,还得先了解下这个time_wait。

time_wait#

time_wait是什么#

请看状态变迁图:

tcp状态变迁

大家知道四次挥手关闭连接吧,其中,首先发起挥手的一方(或者叫:首先发起关闭连接的一方),在经历FIN_WAIT_1/CLOSING/FIN_WAIT_2等路径后,最终会进入time_wait状态。

其实,此时已经完成了4次挥手,为什么连接不直接进入关闭状态呢,为啥还要等到2MSL后,才能进入关闭状态呢?

既然tcp协议组设计了这么一个状态,自然是为了解决某些问题。在讲它能解决的问题之前,我们先简单实践下,看看什么情况下会出现该状态。

在什么地方出现该状态#

在我们传统的cs模型里,app/web网页是客户端,后端是服务端,那么,一定是app端/web网页发起主动关闭吗,不见得。后端也可以主动发起挥手。

按照我们上面的说法,主动关闭方最终进入time_wait状态。

看下面的例子,我本地telnet 10.80.121.114 9900(9900是一个nginx进程在监听)后,在dos框里随便输,这时就会导致后端主动关闭连接。

image-20230816162204316

此时,在后端就会出现time_wait:

[root@xxx-access ~]# netstat -ntp|grep 9900
tcp        0      0 10.80.121.114:9900      10.0.235.78:14966       TIME_WAIT   -   

所以,因为后端发起挥手,所以后端进入time_wait。

另外,测试了下本机发起http短连接的场景,此时也是由后端主动发起挥手的,因此,也是后端进入time_wait。

GET / HTTP/1.1
Connection: close
User-Agent: PostmanRuntime/7.32.3
Accept: */*
Host: 10.80.121.114:9900
Accept-Encoding: gzip, deflate, br

image-20230816174229925

time_wait的危害#

发起挥手的一端,进入time_wait后,会在该状态下持续一段时间,协议规定这个时间等于2MSL,这个时间还是比较长的,以分钟为单位。

在这个时间段内,不能出现重复的四元组。大家看如下的例子。

我在服务器上去telnet 百度,正常来说,我可以打开n个和百度服务器之间的tcp连接。但每个连接都需要耗费一个本地的端口。如果我们在耗尽本地可用的端口后,会出现什么事情呢?

我们先设置本机只运行使用两个端口:

echo "61000 61001" > /proc/sys/net/ipv4/ip_local_port_range

首先,打开一个shell,执行:

## 110.242.68.4是www.baidu.com后的某个ip
[root@VM-0-6-centos ~]# telnet 110.242.68.4 443
Trying 110.242.68.4...
Connected to 110.242.68.4.
Escape character is '^]'.

查看状态:

[root@VM-0-6-centos ~]# netstat -ntp|grep 443
tcp        0      0 10.0.0.6:61000          110.242.68.4:443        ESTABLISHED 987835/telnet       

再打开一个shell,进行同样动作后,查看:

[root@VM-0-6-centos ~]# netstat -ntp|grep 443
tcp        0      0 10.0.0.6:61000          110.242.68.4:443        ESTABLISHED 987835/telnet       
tcp        0      0 10.0.0.6:61001          110.242.68.4:443        ESTABLISHED 987986/telnet 

此时,本机的61000/61001端口都已被使用,此时,本机已经没有端口可用了,再执行telnet:

[root@VM-0-6-centos ~]# telnet 110.242.68.4 443
Trying 110.242.68.4...
telnet: connect to address 110.242.68.4: Cannot assign requested address

这里,我们模拟的不是time_wait状态的socket,而是established状态,但结果是一样的,因为,socket四元组不能完全一致,在服务端ip+端口+本地ip已经确定的情况下,唯一可以发生变化的就是本地端口,但是本地端口已经全被占用,因此,新的连接就无法建立。

但是,只要我们四元组其他部分可以改变,就还是可以建立连接,比如我们对www.baidu.com后的另一个ip来连接,就可以连上了:

[root@VM-0-6-centos ~]# telnet 110.242.68.3 443
Trying 110.242.68.3...
Connected to 110.242.68.3.
Escape character is '^]'.

此时状态:

[root@VM-0-6-centos ~]# netstat -ntp|grep 443
tcp        0      0 10.0.0.6:61000          110.242.68.3:443        ESTABLISHED 990953/telnet       
tcp        0      0 10.0.0.6:61000          110.242.68.4:443        ESTABLISHED 990886/telnet       
tcp        0      0 10.0.0.6:61001          110.242.68.4:443        ESTABLISHED 990927/telnet  

这里,简单总结下,time_wait出现在主动关闭端,如果该端短时间内和对端建立了大量连接,然后又主动关闭,就会导致该端的大量端口被占用(由于端口号最大为65535,除去1-1024这些著名端口,可用的就是64000多个,也就是说短时间内,该端和对端最多建立6w多个连接再关闭,就会把这些端口全耗尽);此时,该端再想和对端建立连接,就会失败。

除了这部分的危害,其余的会额外占用内存、cpu之类的,基本不是什么太大的事情(除非在某些嵌入式设备上,我工作反正不涉及这块)。

出现大量time_wait的场景#

再背一遍:只有主动关闭方才会进入time_wait。

1、外网访问我方服务#

典型场景,服务提供给外网访问,且,我方服务端主动关闭连接。此时的四元组:

本端:localip + 服务端口,对端:用户外网ip + 随机端口。

但此时,由于对端ip和端口都是用户真实ip+端口,虽然出现大量time_wait,但因为四元组不重复,此时,不会导致用户连接不能建立的问题。

2、防火墙/lvs等访问业务接入层#

此时,防火墙或者lvs的ip作为客户端,访问后台业务接入层nginx等。此时:

本端:防火墙 ip + 本地端口,对端:nginx + 固定端口,此时,对端ip端口固定,本端ip固定,可变的唯有本地端口,如果此时是本端主动关闭,本端就会出现大量time_wait,影响到和接入层的新连接建立。

3、我方服务接入层,访问后端真实服务#

典型场景,nginx机器(本端)将请求反向代理给后端,且本端主动关闭连接。此时的四元组:

本端:nginx ip + 本地端口,对端:后端机器ip + 固定端口,此时,对端的ip和端口是固定的,本端ip固定,如果和后端发生大量短连接,就可能导致本地端口耗尽,无法建立新的连接。

4、我方后端服务,访问依赖的服务、中间件、db、第三方服务等#

该场景下,如果也是我方主动关闭连接,陷入time_wait的话。此时的四元组:

本端:后端服务ip + 本地端口,对端:中间件、db等ip + 固定端口,此时,面临的是和上面2/3类似的问题。

怎么解决大量time_wait的问题#

增加四元组的方式#

先看看,到底需不需要解决,如上面的第一种场景,如果只考虑新连接不能建立的问题,那么,是不需要解决time_wait过多的问题;

2/3/4,理论上需要解决,如果真的有这么大的量,导致新连接无法建立的话。但是,解决的办法,很多,不考虑系统内核参数的话,只需要保证四元组不重复即可。

就像我们上面那个telnet百度的实验一样,百度有多个ip,在ip1:443上耗尽了本地端口,那可以换到百度的ip2上。

大家可以参考下面这个文章,写得很好:https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

The solution is more quadruplets.5 This can be done in several ways (in the order of difficulty to setup):

  • use more client ports by setting net.ipv4.ip_local_port_range to a wider range;
  • use more server ports by asking the web server to listen to several additional ports (81, 82, 83, …);
  • use more client IP by configuring additional IP on the load balancer and use them in a round-robin fashion;6 or
  • use more server IP by configuring additional IP on the web server.

翻译下就是:

解决办法就是拥有更多的四元组即可。

  • 更多的本地端口可供使用,通过net.ipv4.ip_local_port_range,但最大也就65535
  • 更多的服务端端口,如服务端可以监听81/82/83,多个端口都可以处理请求
  • 更多的客户端ip
  • 更多的服务端ip

复用time_wait(仅适用连接发起方)#

上面我们说,time_wait会导致新连接无法建立。但是,如果打开了参数:net.ipv4.tcp_tw_reuse = 1,新连接建立的时候,就可以在time_wait状态下已持续超过1s的那些socket中选一个来用。

我们怎么知道哪些socket在time_wait状态已经持续超过1s了呢,那就依赖另一个参数:

net.ipv4.tcp_timestamps = 1 (默认就是1)

这个参数是默认打开的,它会给socket关联上一个时间戳。

这块具体的,还是参考文章吧:

https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

总之,这是一个推荐的方案,适用于time_wait过多,新连接建立不起来的问题。但是,注意,这个只适用于发起连接的一方,不适用于接收连接的一方。

回收time_wait(适用于连接发起方、连接接收方)#

终于来到了恶名昭彰的参数:net.ipv4.tcp_tw_recycle. 它也是依托于net.ipv4.tcp_timestamps参数才能生效。

这个参数,首先会加快time_wait状态socket的回收,如果你在服务器上执行netstat,经常看不到time_wait状态的socket,那就很可能是打开了这个参数。

其次,它还有个副作用,它会启用一个叫做PAWS(PAWS : Protection Against Wrapping Sequence)的机制。

这个机制就是解决sequence回绕的问题,比如,本端和对端建立了一个连接,此时开始发送请求,假设seq为1,数据包长度100. 但这个包在路由给对端的时候,可能进入了某个异常的路由器,被阻塞了,迟迟未能到达对端。

这时,本端就会重传这个seq=1的包,本次,走了一条比较快的路由,到达对端了。

接下来,我们又和对端进行了很多交互,seq来到了最大值附近,最大为2*32次方 - 1。此时,我们关闭本次连接。

接下来,我们又建立了一个新的连接(正巧,四元组和刚关闭的这个一致),由于seq已经最大,发生了回绕,变成了从头开始,此时,我们又发了一个seq为1,数据包长度200的包给对端;而此时,之前上一轮那个走了歧路的包,意外到达对端了,此时,对端就会认为第一轮那个包是ok的,反而把我们本轮的包给丢了。

我找了个网图(侵删):

Image

为了解决这个问题,就引入了时间戳机制,每个包中都带了自己本地生成的一个时间戳,而且,这个时间戳就是本地生成的,但是是递增的,比如,下面的第一个包,时间戳为96913730,第二包为:96913734

image-20230816222836776

image-20230816222924005

引入这个时间戳机制后,就可以解决上面的seq回绕问题了,因为每次收到这种带时间戳的包时,服务端都会维护下我方的最新时间戳,当收到在第一轮中误入歧途的包时,由于其时间戳比较小(比较老),比当前服务端维护的时间戳小,就会认定这个包有问题,直接丢弃。

但是,服务端(对端)是针对我方ip维护了一个时间戳,回到开头的例子,我方ip在通过防火墙以后,访问到后端服务时,后端服务看到的ip是防火墙的ip;那么,后端服务器就只会维护一个防火墙的最新时间戳,这是有问题的。

比如我们两个人各自用app访问服务,此时,各自本地生成的时间戳是不一致的,假设A生成的时间戳较大,此时,服务端维护的时间戳就是A生成的,接到B生成的时间戳较小的包时,就会直接丢弃。

比如,下面的第807包,时间戳为12亿左右:

image-20230816224223361

而到了808包,时间戳到了2亿,这就会导致错乱:

image-20230816224320263

在这期间,服务端的netstat统计可以看到,很多被拒绝的syn:

image-20230816224521807

在处理三次握手的第一次握手时,协议栈相关代码中根据时间戳丢弃syn的逻辑:

image-20230816224725383

最终的解决办法#

我是直接关闭了net.ipv4.tcp_tw_recycle参数,关闭后,再测试多手机同时使用app,已经没有拒绝syn的指标继续增长的情况了。

image-20230816224922889

net.ipv4.tcp_tw_recycle在新版本被删除#

由于其在nat环境存在的巨大问题,基本就只剩下很少场景可以用了。后来linux 4.1内核又上了一个特性,导致这个参数彻底失效,然后在4.2版本被删除。

image-20230816225208165

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc

参考资料#

https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

https://zhuanlan.zhihu.com/p/356087235

https://elixir.bootlin.com/linux/v3.10/source/net/ipv4/tcp_ipv4.c#L203

https://stackoverflow.com/questions/6426253/tcp-tw-reuse-vs-tcp-tw-recycle-which-to-use-or-both

https://blog.csdn.net/qq_25046827/article/details/131839126

https://www.ietf.org/rfc/rfc1323.txt

https://www.cnxct.com/coping-with-the-tcp-time_wait-state-on-busy-linux-servers-in-chinese-and-dont-enable-tcp_tw_recycle/#ftoc-heading-11

https://github.com/y123456yz/Reading-and-comprehense-linux-Kernel-network-protocol-stack

https://mp.weixin.qq.com/s/2xkYbczdHKgpUnicBw0pkA

https://www.suse.com/support/kb/doc/?id=000019286

博客分类导航贴:博客目录导航,让我们一起学起来吧(持续更新)

我是逐日, 鹅厂大头兵一枚,成都深圳反复横跳,鹅厂内推可以找我,可以关注我公号,想起来了就更新;或者右下角有个加号,咱们博客园见

20210108143914.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK