简介: 解决Tengine健康检查引起的TIME_WAIT堆积问题
一. 问题背景
“服务上云后,我们的TCP端口基本上都处于TIME_WAIT的状态”、“这个问题在线下机房未曾发生过” 这是客户提交问题的描述。
客户环境是自建Tengine作为7层反向代理,后端接约1.8万台NGINX。Tengine上云之后,在服务器上发现大量的TIME_WAIT状态的TCP socket;由于后端较多,潜在可能影响业务可用性。用户对比之前的经验比较担心是否可能是接入阿里云之后导致,所以希望我们对此进行详细的分析。
注:TIME_WAIT状态的监听带来的问题在于主机无法为往外部的连接请求分配动态端口。此时,可以配置net.ipv4.ip_local_port_range,增加其端口选择范围(可以考虑 5000 – 65535),但依然存在 2 MSL 时间内被用完的可能。
二. TIME_WAIT原因分析
首先,如果我们重新回顾下TCP状态机就能知道,TIME_WAIT状态的端口仅出现在主动关闭连接的一方(跟这一方是客户端或者是服务器端无关)。当TCP协议栈进行连接关闭请求时,只有【主动关闭连接方】会进入TIME_WAIT状态。而客户的顾虑也在这里。
一方面,健康检查使用 HTTP1.0 是短连接,逻辑上应该由后端NGINX服务器主动关闭连接,多数TIME_WAIT应该出现在NGINX侧。
另一方面,我们也通过抓包确认了多数连接关闭的第一个FIN请求均由后端NGINX服务器发起,理论上,Tengine服务器的socket 应该直接进入CLOSED状态而不会有这么多的TIME_WAIT 。
抓包情况如下,我们根据Tengine上是TIME_WAIT的socket端口号,进行了过滤。
图1:一次HTTP请求交互过程
虽然上面的抓包结果显示当前 Tengine 行为看起来确实很奇怪,但实际上通过分析,此类情形在逻辑上还是存在的。为了解释这个行为,我们首先应该了解:通过tcpdump抓到的网络数据包,是该数据包在该主机上收发的“结果”。尽管在抓包上看,Tengine侧看起来是【被动接收方】角色,但在操作系统中,这个socket是否属于主动关闭的决定因素在于操作系统内TCP协议栈如何处理这个socket。
针对这个抓包分析,我们的结论就是:可能这里存在一种竞争条件(Race Condition)。如果操作系统关闭socket和收到对方发过来的FIN同时发生,那么决定这个socket进入TIME_WAIT还是CLOSED状态决定于 主动关闭请求(Tengine 程序针对 socket 调用 close 操作系统函数)和 被动关闭请求(操作系统内核线程收到 FIN 后调用的 tcp_v4_do_rcv 处理函数)哪个先发生 。
很多情况下,网络时延,CPU处理能力等各种环境因素不同,可能带来不同的结果。例如,而由于线下环境时延低,被动关闭可能最先发生;自从服务上云之后,Tengine跟后端Nginx的时延因为距离的原因被拉长了,因此Tengine主动关闭的情况更早进行,等等,导致了云上云下不一致的情况。
可是,如果目前的行为看起来都是符合协议标准的情况,那么如何正面解决这个问题就变得比较棘手了。我们无法通过降低Tengine所在的主机性能来延缓主动连接关闭请求,也无法降低因为物理距离而存在的时延消耗加快 FIN 请求的收取。这种情况下,我们会建议通过调整系统配置来缓解问题。
注:现在的Linux系统有很多方法都可以快速缓解该问题,例如,
a) 在timestamps启用的情况下,配置tw_reuse。
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps = 1
b) 配置 max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 5000
缺点就是会往syslog里写: time wait bucket table overflow.
由于用户使用自建 Tengine ,且用户不愿意进行 TIME_WAIT 的强制清理,因此我们考虑通过Tengine的代码分析看看是否有机会在不改动 Tengine 源码的情况下,改变 Tengine 行为来避免socket被Tengine主动关闭。
Tengine version: Tengine/2.3.1
NGINX version: nginx/1.16.0
1、 Tengine code analysis
从之前的抓包,我们可以看出来多数的TIME_WAIT socket是为了后端健康检查而创建的,因此我们主要关注 Tengine的健康检查行为,以下是从ngx_http_upstream_check_module 的开源代码中摘抄出来的关于socket清理的函数。
图2:Tengine 健康检查完成后清理socket过程
从这段逻辑中,我们可以看到,如果满足以下任一条件时,Tengine会在收到数据包之后直接关闭连接。
- c->error != 0
- cf->need_keepalive = false
- c->requests > ucscf->check_keepalive_requ
图3: Tengine 中真正完成socket关闭的函数
这里,如果我们让以上的条件变成不满足,那么就有可能让Tengine所在的操作系统先处理被动关闭请求,进行socket清理,进入CLOSED状态,因为从HTTP1.0的协议上来说,NGINX服务器这一方一定会主动关闭连接。
2、解决方法
一般情况下,我们对于TIME_WAIT的连接无需太过关心,一般2MSL(默认60s) 之后,系统自动释放。如果需要减少,可以考虑长链接模式,或者调整参数。
该case中,客户对协议比较了解,但对于强制释放TIME_WAIT 仍有担心;同时由于后端存在1.8万台主机,长连接模式带来的开销更是无法承受。
因此,我们根据之前的代码分析,通过梳理代码里面的逻辑,推荐客户以下健康检查配置,
check interval=5000 rise=2 fall=2 timeout=3000 type=http default_down=false;
check_http_send “HEAD / HTTP/1.0\r\n\r\n”;
check_keepalive_requests 2
check_http_expect_alive http_2xx http_3xx;
理由很简单,我们需要让之前提到的三个条件不满足。在代码中,我们不考虑 error 情况,而need_keepalive 在代码中默认 enable (如果不是,可以通过配置调整),因此需确保check_keepalive_requests大于1即可进入Tengine的KEEPALIVE逻辑,避免Tengine主动关闭连接。
图4:Tengine健康检查参考配置
因为使用HTTP1.0的HEAD方法,后端服务器收到后会主动关闭连接,因此Tengine创建的socket进入CLOSED状态,避免进入TIME_WAIT而占用动态端口资源。
作者:SRE团队技术小编-小凌
原文链接
本文为阿里云原创内容,未经允许不得转载