解决Tengine健康检查导致TIME_WAIT累积的问题Thedownroomneverhappened》这是客户提交的问题描述,客户环境为自建Tengine做7层反向代理,NGINX18000左右连接到后端,Tengine上云后,发现服务端存在大量TIME_WAIT状态的TCPsocket,由于后端数量多,可能会影响业务的可用性。对比之前的经验,用户比较担心是不是连接阿里云导致的,所以希望我们可以对此进行详细的分析。注意:TIME_WAIT状态的监控导致的问题是宿主机无法分配动态端口给外部连接请求,此时可以配置net.ipv4.ip_local_port_range增加它的端口选择范围(5000-65535可以考虑),但是还是有可能t2MSL时间将用完。2.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。对于这次抓包分析,我们的结论是:可能存在竞争条件(RaceCondition)。如果操作系统在关闭socket的同时收到对方发送的FIN,那么socket是进入TIME_WAIT还是CLOSED状态的决定取决于主动关闭请求(Tengine程序调用close操作系统函数用于socket)和被动关闭请求(操作系统内核线程收到FIN后调用的tcp_v4_do_rcv处理函数)以先发生者为准。很多时候网络延迟、CPU处理能力等各种环境因素不同,可能会带来不同的结果。例如,由于离线环境的低延迟,可能首先发生被动关闭;由于服务迁移到云端,Tengine和后端Nginx之间的延迟因为距离拉长,所以Tengine的主动关闭会提前执行等等,导致云端和云端不一致。但是,如果现在的行为看起来符合协议的标准,那么如何正面解决这个问题就变得棘手了。我们无法通过降低Tengine所在主机的性能来延迟主动关闭连接请求,也无法通过降低因物理距离造成的延迟消耗来加快FIN请求的收集。在这种情况下,我们建议调整系统配置以缓解问题。注意:在目前的Linux系统中有很多方法可以快速缓解这个问题,例如,a)在启用时间戳的情况下,配置tw_reuse。net.ipv4.tcp_tw_reuse=1net.ipv4.tcp_timestamps=1b)配置max_tw_bucketsnet.ipv4.tcp_max_tw_buckets=5000缺点是会写入syslog:timewaitbuckettableoverflow。因为用户使用的是自建的Tengine,用户不愿意进行TIME_WAIT强制清理,所以我们考虑使用Tengine的代码分析,看看有没有机会在不改变Tengine源码的情况下改变Tengine的行为,以防止套接字被Tengine主动关闭。Tengine版本:Tengine/2.3.1NGINX版本:nginx/1.16.01,Tengine代码分析从前面的抓包可以看出,TIME_WAITsockets大部分是为了后端健康检查而创建的,所以我们主要关注TengineCheck行为,以下是摘自ngx_http_upstream_check_module开源代码的socket清理函数。图2:Tengine在健康检查完成后清理socket进程从这个逻辑我们可以看出,如果满足以下任一条件,Tengine会在收到数据包后直接关闭连接。c->error!=0cf->need_keepalive=false->requests>ucscf->check_keepalive_requ图3:Tengine中实际关闭socket的函数这里,如果我们让上面的条件都不满足,那么Tengine就有可能进行操作所在系统首先处理被动关闭请求,清理socket,进入CLOSED状态,因为在HTTP1.0协议中,NGINX服务器必须主动关闭连接。2、解决方案一般情况下,我们不需要太在意TIME_WAIT的连接。一般在2MSL(默认60s)后,系统会自动释放。如果需要减少,可以考虑长链接模式,或者调整参数。这种情况下,client对协议理解的很好,但是还是担心TIME_WAIT被强行释放;同时,由于后端有18000台主机,长连接模式带来的开销更是难以承受。因此,根据前面的代码分析,我们通过梳理代码中的逻辑,为客户推荐如下健康检查配置,checkinterval=5000rise=2fall=2timeout=3000type=httpdefault_down=false;check_http_send"HEAD/HTTP/1.0rnrn";check_keepalive_requests2check_http_expect_alivehttp_2xxhttp_3xx;道理很简单,我们要让前面说的三个条件都不满足。代码中我们不考虑报错情况,代码中默认开启need_keepalive(如果不开启可以通过配置调整),所以需要保证check_keepalive_requests大于1才能进入Tengine的KEEPALIVE逻辑防止Tengine主动关闭连接。图4:Tengine健康检查参考配置因为使用了HTTP1.0的HEAD方式,后端服务器收到后会主动关闭连接,所以Tengine创建的socket进入CLOSED状态,避免进入TIME_WAIT占用动态端口资源.作者:SRE团队技术编辑-小玲原文链接本文为阿里云原创内容,未经允许不得转载
