背景说明:
在尼恩读者50+交流群中,是不是有小伙伴问:
尼恩,生产环境 Nginx 后端服务大量 TIME-WAIT , 该怎么办?
除了Nginx进程之外,还有其他的后端服务如:
尼恩,生产环境 Netty、SpringCloud Gateway 后端服务大量 TIME-WAIT , 该怎么办?
遇到这样的生产环境难题,小伙伴们非常头疼。
更为头疼的是,这个也是一道场景的面试题。之前有小伙伴反应过,他面试科大讯飞的时候,遇到了这道题目:
生产环境 Nginx 后端服务大量 TIME-WAIT 的解决步骤
这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的《尼恩Java面试宝典》V46版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从这里获取:码云
基础知识:
TIME_WAIT是什么?
在构建TCP客户端服务器系统时,很容易犯简单的错误,这些错误会严重限制可伸缩性。
其中一个错误是没有考虑到TIME_WAIT
状态。
为什么TIME_WAIT
存在,它可能导致的问题,如何解决它,以及什么时候不应该。
TIME_WAIT
是 TCP 状态转换图中经常被误解的状态。
这是某些套接字可以进入并保持相对较长时间的状态,如果您有足够的套接字,那么您创建新套接字连接的能力可能会受到影响,这可能会影响客户端服务器系统的可伸缩性。
关于套接字如何以及为什么首先进入TIME_WAIT
,经常存在一些误解,不应该有,这并不神奇。从下面的TCP状态转换图中可以看出TIME_WAIT
,是TCP客户端通常最终处于的最终状态。
虽然状态转换图显示TIME_WAIT
为客户端的最终状态,但它不一定是客户端TIME_WAIT
。事实上,最终状态是启动“主动关闭”的对等端最终进入,这可以是客户端或服务器。那么,发布“主动关闭”是什么意思?
如果TCP对等方是第一个呼叫Close()
连接的对等方,则TCP对等方会启动“主动关闭” 。
在许多协议和客户端/服务器设计中,这是客户端。
在HTTP和FTP服务器中,这通常是服务器。
导致同伴结束的事件的实际顺序TIME_WAIT
如下。
现在我们知道套接字是如何结束TIME_WAIT
的,理解为什么这个状态存在以及它为什么会成为一个潜在的问题是有用的。
TIME_WAIT
通常也被称为2MSL等待状态。
这是因为转换到的套接字在此期间的持续时间TIME_WAIT
为2 x最大段寿命。
MSL是任何段的最大时间量,对于构成TCP协议一部分的数据报的所有意图和目的而言,在丢弃之前,网络可以在网络上保持有效。这个时间限制最终以用于传输TCP段的IP数据报中的TTL字段为界。
不同的实现为MSL选择不同的值,常用值为30秒,1分钟或2分钟。
RFC 793指定MSL为2分钟,Windows系统默认为此值,但可以使用TcpTimedWaitDelay注册表设置进行调整。
TIME_WAIT
可能影响系统可伸缩性 的原因是,TCP连接中一个完全关闭的套接字将保持TIME_WAIT
约4分钟的状态。
如果许多连接正在快速打开和关闭,那么套接字TIME_WAIT
可能会开始累积在系统上; 您可以TIME_WAIT
使用netstat查看套接字。一次可以建立有限数量的套接字连接,限制此数量的其中一个因素是可用本地端口的数量。
如果TIME_WAIT
插入太多的套接字,您会发现很难建立新的出站连接,因为缺少可用于新连接的本地端口。
但为什么TIME_WAIT
存在呢?有两个原因需要TIME_WAIT
。
首先是防止一个连接的延迟段被误解为后续连接的一部分。丢弃在连接处于2MSL等待状态时到达的任何段。
在上图中,我们有两个从终点1到终点2的连接。每个终点的地址和端口在每个连接中都是相同的。第一个连接终止于由端点2启动的主动关闭。如果端点2没有保持TIME_WAIT
足够长的时间以确保来自先前连接的所有分段已经失效,则延迟的分段(具有适当的序列号)可以是误认为是第二个连接的一部分…
请注意,延迟片段很可能会导致这样的问题。首先,每个端点的地址和端口需要相同; 这通常不太可能,因为操作系统通常从短暂端口范围为您选择客户端端口,从而在连接之间进行更改。其次,延迟段的序列号需要在新连接中有效,这也是不太可能的。但是,如果出现这两种情况,TIME_WAIT
则会阻止新连接的数据被破坏。
需要TIME_WAIT
第二个原因是可靠地实施TCP的全双工连接终端。
如果ACK
从终点2开始的终点被丢弃,则终点1将重新发送终点FIN
。如果连接已经过渡到CLOSED
终点2,那么唯一可能的应答是发送一个,RST
因为重发FIN
是意外的。即使所有数据传输正确,这也会导致终点1接收到错误。
不幸的是,一些操作系统实现的方式TIME_WAIT
似乎有点幼稚。
只有TIME_WAIT
通过阻塞才能提供保护的连接与需要的socket完全匹配TIME_WAIT
。这意味着由客户端地址,客户端端口,服务器地址和服务器端口标识的连接。但是,某些操作系统会施加更严格的限制,并且阻止本地端口号被重新使用,而该端口号包含在连接中TIME_WAIT
。如果有足够的套接字结束,TIME_WAIT
则不能建立新的出站连接,因为没有剩余本地端口分配给新连接。
Windows不会执行此操作,只会阻止与其中的连接完全匹配的出站连接TIME_WAIT
。
入站连接受影响较小TIME_WAIT
。
虽然由服务器主动关闭的TIME_WAIT
连接与客户端连接完全一样,但是服务器正在侦听的本地端口不会阻止其成为新的入站连接的一部分。在Windows上,服务器正在监听的众所周知的端口可以构成随后接受的连接的一部分,并且如果从远程地址和端口建立了新的连接,该连接当前构成TIME_WAIT
该本地地址和端口的连接的一部分,则只要新序列号大于当前连接的最终序列号,就允许连接TIME_WAIT
。然而,TIME_WAIT
在服务器上累积可能会影响性能和资源使用,因为TIME_WAIT
最终需要超时的连接需要进行一些工作,直到TIME_WAIT
状态结束,连接仍占用(少量)服务器资源。
鉴于TIME_WAIT
由于本地端口号耗尽而影响出站连接的建立,并且这些连接通常使用由临时端口范围由操作系统自动分配的本地端口,因此您可以做的第一件事情是确保你正在使用一个体面的短暂端口范围。在Windows上,您可以通过调整MaxUserPort
注册表设置来完成此操作。请注意,默认情况下,许多Windows系统的临时端口范围大约为4000,这对于许多客户端服务器系统来说可能太低。
虽然有可能缩短套接字在TIME_WAIT
这方面的花费时间通常不会有帮助。鉴于TIME_WAIT
当许多连接建立并且主动关闭时,这只是一个问题,调整2MSL等待周期通常会导致在给定时间内建立和关闭更多连接的情况,因此必须不断调整2MSL直到由于延迟片段似乎是后来连接的一部分,因此它可能会开始出现问题; 如果您连接到相同的远程地址和端口,并且非常快速地使用所有本地端口范围,或者如果连接到相同的远程地址和端口并将本地端口绑定到固定值,这种情况才会变得可能。
更改2MSL延迟通常是机器范围内的配置更改。您可以尝试TIME_WAIT
使用SO_REUSEADDR
套接字选项解决套接字级别问题。这允许在具有相同地址和端口的现有套接字已经存在的同时创建套接字。新的套接字基本上劫持了旧的套接字。您可以使用它SO_REUSEADDR
来允许创建套接字,同时具有相同端口的套接字已经在其中,TIME_WAIT
但这也会导致诸如拒绝服务攻击或数据窃取等问题。在Windows平台上另一套接字选项,SO_EXCLUSIVEADDRUSE
可以帮助防止某些缺点的SO_REUSEADDR
,但在我看来,最好避免这些尝试在各地工作TIME_WAIT
,而是设计系统,使TIME_WAIT
不是问题。
上面的TCP状态转换图都显示有序的连接终止。还有另一种方法来终止TCP连接,这是通过中止连接并发送一个RST
而不是一个FIN
。这通常通过将SO_LINGER
套接字选项设置为0 来实现。这会导致挂起的数据被丢弃,并且连接将被中止,RST
而不是等待传输的数据,并且使用a清除干净的连接FIN
。重要的是要认识到,当连接被中止时,可能在对等体之间流动的任何数据将被丢弃,RST
直接送达; 通常作为表示“连接已被同级重置”的事实的错误。远程节点知道连接被中止,并且两个节点都没有进入TIME_WAIT
。
当然,已经中止使用的连接的新化身RST
可能成为TIME_WAIT
阻碍的延迟段问题的受害者,但是成为问题所需的条件是不太可能的,无论如何,参见上面的更多细节。为了防止已经中止的连接导致延迟段问题,两个对等端都必须转换到TIME_WAIT
连接关闭可能由诸如路由器之类的中介引起。但是,这不会发生,连接的两端都会关闭。
有几件事你可以做,以避免TIME_WAIT
成为你的问题。其中一些假定您有能力更改您的客户端和服务器之间所说的协议,但是对于自定义服务器设计,您通常会这样做。
对于永远不会建立自己的出站连接的服务器,除了维护连接的资源和性能意义之外TIME_WAIT
,您不必过分担心。
对于确实建立出站连接并接受入站连接的服务器,黄金法则是始终确保如果TIME_WAIT
需要发生这种情况,则它会在另一个对等而不是服务器上结束。做到这一点的最佳方式是永远不要从服务器发起主动关闭,无论原因是什么。如果您的对等方超时,则以中止连接RST
而不是关闭连接。如果你的对方发送无效数据,中止连接等等。
这个想法是,如果你的服务器永远不会启动一个主动关闭,它永远不会累积TIME_WAIT
套接字,因此永远不会受到它们导致的可伸缩性问题的困扰。虽然在出现错误情况时很容易看到如何中止连接,但正常连接终止的情况如何?理想情况下,您应该为协议设计一种方式,让服务器告诉客户端它应该断开连接,而不是简单地让服务器发起主动关闭。因此,如果服务器需要终止连接,服务器会发送一个应用程序级别“我们已经完成”的消息,客户将此消息作为关闭连接的原因。如果客户端未能在合理的时间内关闭连接,则服务器会中止连接。
在客户端上,事情稍微复杂一些,毕竟,有人必须发起一个主动关闭来干净地终止TCP连接,并且如果它是客户端,那么这就是TIME_WAIT
最终会结束的地方。然而,TIME_WAIT
最终在客户端有几个优点。
首先,如果由于某种原因,客户端由于套接字在TIME_WAIT
一个客户端中的积累而最终导致连接问题。其他客户不会受到影响。
其次,快速打开和关闭TCP连接到同一台服务器是没有效率的,因此超出了问题的意义TIME_WAIT
尝试维持更长时间的连接而不是更短的时间。不要设计一个协议,客户端每分钟连接一次,并打开一个新的连接。而是使用持久连接设计,并且只有在连接失败时才重新连接,如果中间路由器拒绝在没有数据流的情况下保持连接处于打开状态,则可以实现应用程序级别ping,使用TCP保持活动状态或仅接受路由器重置连接; 好处是你没有积累TIME_WAIT
插座。如果你在连接上做的工作自然是短暂的,那么考虑某种形式的“连接池”设计,从而保持连接的开放性和重用性。
最后,如果您绝对必须快速地从客户端打开和关闭连接到同一台服务器,那么也许您可以设计一个可以使用的应用程序级别关闭顺序,然后按照此顺序关闭。您的客户可能会发送“我完成了”消息,您的服务器可能会发送“再见”消息,然后客户端可能会中止连接。
TIME_WAIT
存在原因并通过缩短2MSL周期或允许地址重用来解决这个问题SO_REUSEADDR
并不总是一个好主意。
如果你能够设计你的协议TIME_WAIT
避免在脑海中,那么你通常可以完全避免这个问题。
TIME_WAIT的4种查询方式
1、netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’
2、ss -s
3、netstat -nat |awk ‘{print $6}’|sort|uniq -c|sort -rn
4、 统计TIME_WAIT 连接的本地地址
netstat -an | grep TIME_WAIT | awk ‘{print $4}’ | sort | uniq -c | sort -n -k1
尝试抓取 tcp 包
tcpdump tcp -i any -nn port 8080 | grep "172.11.11.11"
系统参数优化
处理方法:需要修改内核参数开启重启:
net.ipv4.tcp_tw_reuse = 1,开启快速回收
net.ipv4.tcp_tw_recycle = 1 (在NAT网络中不建议开启,要设置为0),并且开启net.ipv4.tcp_timestamps = 1以上两个参数才生产。
具体操作方法如下:
echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf;
echo "net.ipv4.tcp_tw_recycle = 1" >> /etc/sysctl.conf;
echo "net.ipv4.tcp_timestamps = 1" >> /etc/sysctl.conf;
sysctl -p;
nginx 配置优化
当使用nginx作为反向代理时,为了支持长连接,需要做到两点:
- 从client到nginx的连接是长连接
- 从nginx到server的连接是长连接
保持和client的长连接:
http {
keepalive_timeout 120s 120s;
keepalive_requests 1000;
}
keepalive_timeout设置
语法
keepalive_timeout timeout [header_timeout];
第一个参数:设置keep-alive客户端(浏览器)连接在服务器端(nginx端)保持开启的超时值(默认75s);值为0会禁用keep-alive客户端连接;
第二个参数:可选、在响应的header域中设置一个值“Keep-Alive: timeout=time”;通常可以不用设置;
这个keepalive_timeout针对的是浏览器和nginx建立的一个tcp通道,没有数据传输时最长等待该时候后就关闭.
nginx和upstream中的keepalive_timeout则受到tomcat连接器的控制,tomcat中也有一个类似的keepalive_timeout参数
keepalive_requests理解设置
keepalive_requests指令用于设置一个keep-alive连接上可以服务的请求的最大数量,当最大请求数量达到时,连接被关闭。默认是100。
这个参数的真实含义,是指一个keep alive建立之后,nginx就会为这个连接设置一个计数器,记录这个keep alive的长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则nginx会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。
保持和server的长连接
为了让nginx和后端server(nginx称为upstream)之间保持长连接,典型设置如下:(默认nginx访问后端都是用的短连接(HTTP1.0),一个请求来了,Nginx 新开一个端口和后端建立连接,后端执行完毕后主动关闭该链接)
location / {
proxy_http_version 1.1;
proxy_set_header Connection keep-alive;
proxy_pass http://httpurl;
}
HTTP协议中对长连接的支持是从1.1版本之后才有的,因此最好通过proxy_http_version指令设置为”1.1”;
从client过来的http header,因为即使是client和nginx之间是短连接,nginx和upstream之间也是可以开启长连接的。
keepalive理解设置
此处keepalive的含义不是开启、关闭长连接的开关;也不是用来设置超时的timeout;更不是设置长连接池最大连接数。官方解释:
- The connections parameter sets the maximum number of idle keepalive connections to upstream servers connections(设置到upstream服务器的空闲keepalive连接的最大数量)
- When this number is exceeded, the least recently used connections are closed. (当这个数量被突破时,最近使用最少的连接将被关闭)
- It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open.(特别提醒:keepalive指令不会限制一个nginx worker进程到upstream服务器连接的总数量)
总结:
keepalive 这个参数一定要小心设置,尤其对于QPS比较高的场景,推荐先做一下估算,根据QPS和平均响应时间大体能计算出需要的长连接的数量。
比如前面10000 QPS和100毫秒响应时间就可以推算出需要的长连接数量大概是1000. 然后将keepalive设置为这个长连接数量的10%到30%。比较懒的同学,可以直接设置为keepalive=1000之类的,一般都OK的了。
综上,出现大量TIME_WAIT
的情况
1)导致 nginx端出现大量TIME_WAIT
的情况有两种:
keepalive_requests设置比较小,高并发下超过此值后nginx会强制关闭和客户端保持的keepalive长连接;(主动关闭连接后导致nginx出现TIME_WAIT
)
keepalive设置的比较小(空闲数太小),导致高并发下nginx会频繁出现连接数震荡(超过该值会关闭连接),不停的关闭、开启和后端server保持的keepalive长连接;
2)导致后端server端出现大量TIME_WAIT
的情况:
nginx没有打开和后端的长连接,即:没有设置proxy_http_version 1.1;
和proxy_set_header Connection “”;
从而导致后端server每次关闭连接,高并发下就会出现server端出现大量TIME_WAIT
推荐阅读:
《网易二面:CPU狂飙900%,该怎么处理?》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《场景题:假设10W人突访,你的系统如何做到不 雪崩?》
《2个大厂 100亿级 超大流量 红包 架构方案》
《Nginx面试题(史上最全 + 持续更新)》
《K8S面试题(史上最全 + 持续更新)》
《操作系统面试题(史上最全、持续更新)》
《Docker面试题(史上最全 + 持续更新)》
《Springcloud gateway 底层原理、核心实战 (史上最全)》
《Flux、Mono、Reactor 实战(史上最全)》
《sentinel (史上最全)》
《Nacos (史上最全)》
《TCP协议详解 (史上最全)》
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《clickhouse 超底层原理 + 高可用实操 (史上最全)》
《nacos高可用(图解+秒懂+史上最全)》
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《环形队列、 条带环形队列 Striped-RingBuffer (史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
《单例模式(史上最全)》
《红黑树( 图解 + 秒懂 + 史上最全)》
《分布式事务 (秒懂)》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《缓存之王:Caffeine 的使用(史上最全)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》
《Docker原理(图解+秒懂+史上最全)》
《Redis分布式锁(图解 - 秒懂 - 史上最全)》
《Zookeeper 分布式锁 - 图解 - 秒懂》
《Zookeeper Curator 事件监听 - 10分钟看懂》
《Netty 粘包 拆包 | 史上最全解读》
《Netty 100万级高并发服务器配置》
《Springcloud 高并发 配置 (一文全懂)》## 推荐阅读:
《网易二面:CPU狂飙900%,该怎么处理?》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《场景题:假设10W人突访,你的系统如何做到不 雪崩?》
《2个大厂 100亿级 超大流量 红包 架构方案》
《Nginx面试题(史上最全 + 持续更新)》
《K8S面试题(史上最全 + 持续更新)》
《操作系统面试题(史上最全、持续更新)》
《Docker面试题(史上最全 + 持续更新)》
《Springcloud gateway 底层原理、核心实战 (史上最全)》
《Flux、Mono、Reactor 实战(史上最全)》
《sentinel (史上最全)》
《Nacos (史上最全)》
《TCP协议详解 (史上最全)》
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《clickhouse 超底层原理 + 高可用实操 (史上最全)》
《nacos高可用(图解+秒懂+史上最全)》
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《环形队列、 条带环形队列 Striped-RingBuffer (史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
《单例模式(史上最全)》
《红黑树( 图解 + 秒懂 + 史上最全)》
《分布式事务 (秒懂)》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《缓存之王:Caffeine 的使用(史上最全)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》
《Docker原理(图解+秒懂+史上最全)》
《Redis分布式锁(图解 - 秒懂 - 史上最全)》
《Zookeeper 分布式锁 - 图解 - 秒懂》
《Zookeeper Curator 事件监听 - 10分钟看懂》
《Netty 粘包 拆包 | 史上最全解读》
《Netty 100万级高并发服务器配置》
《Springcloud 高并发 配置 (一文全懂)》