FIN_WAIT2 状态是怎么产生的
要理解这个问题,首先需要回顾 TCP 连接关闭时的四次挥手过程。当主动关闭一方发送 FIN 报文并收到对方的 ACK 后,连接就进入 FIN_WAIT2 状态,此时它还在等待对方发送自己的 FIN 报文。只有收到对端的 FIN 并回复 ACK 之后,连接才会真正进入 TIME_WAIT 并最终关闭。换句话说,FIN_WAIT2 表示本端已经完成发送方向的数据传输,但还在等对端关闭。
正常情况下对端会很快发出 FIN,FIN_WAIT2 只是一个短暂的中间状态。但如果对端一直不调用 close,或者对端根本没有收到任何关闭通知,那么主动关闭一方的连接就会长时间停留在 FIN_WAIT2。Linux 内核中有一个 net.ipv4.tcp_fin_timeout 参数,默认值为 60 秒,超过这个时间后内核会强制回收该连接,但这 60 秒内连接占用的文件描述符和端口资源并不会释放。
在 Apache 反向代理加缓存的架构中,Apache 既是客户端(对后端服务)又是服务端(对用户浏览器),两端都可能产生 FIN_WAIT2。尤其当 Apache 与后端之间使用短连接、且缓存命中判断失败需要反复回源时,连接的建立与关闭频率会大幅上升,FIN_WAIT2 堆积的概率也随之增大。
为什么代理缓存场景下问题格外突出
Apache 的 mod_proxy 搭配 mod_cache 使用时,每个请求的处理流程比普通反向代理更复杂。请求到达后,mod_cache 先判断是否有可用的缓存条目;如果缓存过期或不存在,就需要回源到后端获取完整响应,期间还要处理条件请求、缓存校验头等信息。这个过程涉及 Apache 与后端之间连接的频繁开关,一旦某一方处理缓慢或异常断开,就会出现半关闭的连接。
一个典型的坑是 KeepAlive 配置不一致。假设前端用户与 Apache 之间开启了 KeepAlive,而 Apache 到后端之间关闭了 KeepAlive,那么 Apache 在转发完响应后会主动关闭与后端的连接,进入 FIN_WAIT2。如果后端进程由于某种原因(例如响应体未发送完整就异常退出、应用层没有正确关闭 socket)迟迟不发 FIN,Apache 侧的连接就会卡在 FIN_WAIT2 状态等待内核超时。并发量一大,这类连接迅速堆积,可能耗尽文件描述符。
可以用下面的命令快速确认当前系统上 FIN_WAIT2 连接的数量以及对端地址分布:
# 统计各状态的连接数量
netstat -ant | awk '/^tcp/ {print $6}' | sort | uniq -c
# 查看 FIN_WAIT2 连接的对端分布,判断是前端用户还是后端服务导致
netstat -antp | grep FIN_WAIT2 | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
如果 FIN_WAIT2 连接的对端集中指向后端应用服务器的端口,说明问题出在 Apache 与后端之间的连接管理;如果指向大量零散的公网 IP,则更可能是前端用户侧的异常断开行为。
从 Apache 配置层面解决连接堆积
确认问题来源后,可以从 Apache 配置入手进行优化。第一步是统一 KeepAlive 策略。建议 Apache 对前端用户开启 KeepAlive,并设置合理的超时与最大请求数,减少频繁建连的开销:
KeepAlive On KeepAliveTimeout 5 MaxKeepAliveRequests 100
第二步是启用 Apache 到后端的连接池,让 mod_proxy 复用与后端的连接,避免每个请求都经历完整的 TCP 三次握手和四次挥手。这在 httpd 2.4 中通过 connectionpool 相关参数即可实现,同时应配置合理的连接超时,防止后端无响应时连接被无限占用:
<Proxy "balancer://backend">
BalancerMember "http://192.168.0.10:8080" connectiontimeout=5 timeout=30 retry=30
BalancerMember "http://192.168.0.11:8080" connectiontimeout=5 timeout=30 retry=30
</Proxy>
ProxyPass "/" "balancer://backend/"
ProxyPassReverse "/" "balancer://backend/"
# 缓存相关配置
CacheEnable disk "/"
CacheRoot "/var/cache/httpd/proxy"
CacheDirLevels 2
CacheDirLength 1
CacheMaxFileSize 104857600
CacheDefaultExpire 3600
这里需要注意 timeout=30 的含义:它控制 Apache 等待后端响应的最长时间。如果后端处理大文件或慢接口较多,这个值不能设得过小,否则 Apache 会提前断开连接,反而制造更多半关闭状态的 TCP 连接。缓存配置中 CacheMaxFileSize 也很关键,过大的文件不建议写入磁盘缓存,避免回源和写盘时间过长导致连接超时。
内核参数与监控层面的补充手段
除了 Apache 自身配置,调整内核参数也能缓解 FIN_WAIT2 堆积带来的影响。net.ipv4.tcp_fin_timeout 可以适当调小,让异常连接更快被回收;同时增大 fs.file-max 和 Apache 的 MaxRequestWorkers 上限,避免文件描述符耗尽导致新请求被拒绝。修改方式如下:
# 临时生效 sysctl -w net.ipv4.tcp_fin_timeout=30 sysctl -w fs.file-max=1000000 # 持久化写入 /etc/sysctl.conf echo "net.ipv4.tcp_fin_timeout = 30" >> /etc/sysctl.conf echo "fs.file-max = 1000000" >> /etc/sysctl.conf sysctl -p
需要强调的是,调小 tcp_fin_timeout 只是治标,真正的治本方法是找到对端为什么不发 FIN。常见原因包括:后端应用存在 socket 泄漏,处理完请求没有关闭连接;后端服务器前面还有一层负载均衡设备,该设备对半关闭连接的处理策略异常;或者中间防火墙静默丢弃了 FIN 报文。建议在后端服务器上用 tcpdump -i any port 8080 and tcp[tcpflags] & tcp-fin != 0 抓包,观察挥手报文是否正常交互。
长期来看,应该建立对连接状态的持续监控。可以通过 Prometheus 采集 node_netstat_Tcp_CurrEstab 以及自定义脚本输出的各 TCP 状态计数,为 FIN_WAIT2 设置告警阈值,例如持续超过 5000 就触发通知。这样在问题演变成服务故障之前就能介入处理。总结来说,代理缓存场景下的 FIN_WAIT2 问题需要从协议原理、Apache 连接管理配置和内核参数三个层面综合处理,先定位来源,再对症下药,才能让服务长期稳定运行。