一、问题描述
(一)现象描述
服务器有两个现象,第一是 TCP 连接数不多,不超过 10 个,但是 TIME_WAIT 状态的连接却有 2000。第二是按照以往的性质,在很少用户访问的情况下,服务器的资源几乎没有使用,比如 CPU 不超过 5%。而现在没有什么用户的情况下,CPU 损耗持续在 40% 左右,夜间也不停歇。里面运行着好几个 web 项目,都用 docker 启动的容器分开。
(二)相关知识
TCP 连接有 3 次握手,断开有 4 次挥手(整个流程的图解可以看《三次握手与四次挥手》)。
三次握手中第一次,是主动端发出 SYN 信号给正在 listen 的被动端,然后自己变成了 SYN-SENT 状态;第二次是被动端发送 ACK 确认收到信号和 SYN 信号;第三次是主动端发出 ACK 信号确认已经收到了被动端的 SYN。然后双双进入了 ESTABLISHED 状态,便是已经连接成功。
四次挥手中的第一次就是主动端断开,发送 FIN 信号,变成 FIN-WAIT-1 状态;第二次是被动方收到 FIN 信号,就变成 CLOSE_WAIT 状态,然后赶紧发送 ACK 信号给主动方确认,这时候主动方变为 FIN-WAIT-2 状态;第三次还是被动方等自己的应用断开连接的时候,发送 FIN 信号给主动方,被动方的状态变成 LAST-ACK;第四次是主动方收到被动方的 FIN 信号,然后发送的 ACK 信号,瞬间自己变成 TIME_WAIT 状态,然后等待回收。
就是说,谁有 TIME_WAIT,谁就是主动方。这点可以排除用户频繁关闭网页的可能。意思就是说这都是服务器主动请求断开连接的,而 TIME_WAIT 状态的连接也没有回收。
二、问题推测
(一)网络
网络上面的就是网络不好,或者被攻击。
(二)应用
中间件的参数不对,导致有中间件断开的连接,或者应用程序错误造成的主动断开连接。或者也是应用方面导致消耗资源太多。
三、排查
这个服务器有三个项目,每个项目的架构都是 lanmp(Linux + Apache + Nginx + MySQL + PHP)。问题复杂在于服务器里面好几个项目,每个项目都用了一个反向代理。好的一点是后端是 docker 容器,分开的。
(一)TCP 连接上的 IP
1.先看各容器的 IP。
2.再看连接中本地的 IP。排名第一的是我们本地 IP,6601 是 API 项目的监听端口,从这里可以看出在所有的 TIME_WAIT 状态的 TCP 里面,API 项目的后端是被请求最多的那个,估计反向代理服务器也被请求了很多。
3.接着看连接本地 API 项目的对端 IP。可以看出,请求连接 API 后端的全部都是 nginx 的 IP,这也很容易理解,nginx 反向代理是入口嘛。下面就看看到底是谁对 nginx 发出请求。
4.然后是连接中外地的 IP。对 API 的请求是 600,对 nginx 的请求是 300,说明所有的 TIME_WAIT,一部分是请求 nginx 的,一部分是 nginx 请求 API 的。
5.最后看看到底是谁在请求 web 前端 nginx。原来是 192.168.42.1 这个 IP 的请求。其实 192.168.42.1 这个 IP 是 docker 的虚拟网卡的 IP,作为全部容器的网关,也就是说反正这就是这些容器发出的请求,但是不能确定是哪一个。
综上所述,可以排除网络问题;中间件 apache 的参数没有改,但是对 web 前端 nginx 的请求那么多,可以说明问题不是出在 apache 的请求上面。那就往代码错误方面考虑。
(二)宿主机上的容器
1.应用和网络的关系
可能 TIME_WAIT 的问题就是后端程序乱发请求,apache 是主项目的后端容器,apache-api 就是 api 的后端程序。webserver 占用的 CPU 上升,刚好就说明容器使用的系统资源就是由这种请求引起的。下面用 tail 看看 API 的 access 日志。
实时监测,发现 API 一秒钟被请求 12 次左右,根据业务性质和 docker 的状态显示,可以断定是主项目的循环请求造成的系统资源内耗。而每次请求 API 项目就返回了 access_token,API 返回数据之后就发出断开信号,逻辑和现象很符合,也可以断定 TIME_WAIT 的状态也是这请求引起。而 TIME_WAIT 不是不回收,而是回收了,但不断地生成(协议为什么要这么设计,可以看《为什么需要 TIME_WAIT》)。
四、结论
最终的结论和推测对得上:主项目在循环请求 API 项目,API 每次返回 access_token 之后就主动断开连接,服务端这边于是不断地生成 TIME_WAIT——不是不回收,而是生成速度一直压着回收速度,顺带把 CPU 也耗在了这些内部请求上。
回头看这条排查路径:先通过 TIME_WAIT 连接两端的 IP 弄清楚谁在连谁,再用 access 日志确认请求的频率和来源,最后回到业务代码里找原因,问题就能落到底。