生产Nginx现大量TIME-WAIT,连接耗尽,该如何处理?

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

背景说明

在尼恩读者50+交流群中是不是有小伙伴问

尼恩生产环境 Nginx 后端服务大量 TIME-WAIT 该怎么办

除了Nginx进程之外还有其他的后端服务如

尼恩生产环境 Netty、SpringCloud Gateway 后端服务大量 TIME-WAIT 该怎么办

遇到这样的生产环境难题小伙伴们非常头疼。

更为头疼的是这个也是一道场景的面试题。之前有小伙伴反应过他面试科大讯飞的时候遇到了这道题目

生产环境 Nginx 后端服务大量 TIME-WAIT 的解决步骤

这里尼恩给大家做一下系统化、体系化的梳理使得大家可以充分展示一下大家雄厚的 “技术肌肉”让面试官爱到 “不能自已、口水直流”

也一并把这个题目以及参考答案收入咱们的《尼恩Java面试宝典》供后面的小伙伴参考提升大家的 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_WAIT2 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作为反向代理时为了支持长连接需要做到两点

  1. 从client到nginx的连接是长连接
  2. 从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和后端servernginx称为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更不是设置长连接池最大连接数。官方解释

  1. The connections parameter sets the maximum number of idle keepalive connections to upstream servers connections设置到upstream服务器的空闲keepalive连接的最大数量
  2. When this number is exceeded, the least recently used connections are closed. 当这个数量被突破时最近使用最少的连接将被关闭
  3. 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 高并发 配置 一文全懂

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: nginx