使用下面的命令查看处于哪个状态的连接最多。
1 | netstat -tna | awk '{print $5,$6}'| sort | uniq -c | sort -n | tail -n 1 |
发现排名第一的是TCP的TIME_WAIT 状态连接,竟然高达三万个。
解决方法:
1 | 打开 sysctl.conf 文件,修改一下参数 |
以上方法对解决 TIME_WAIT 很好。其中 tw_recylce 和 tw_reuse 一定需要 timestamps 的支持,而且这些配置一般不建议开启。
这样确实暂时性解决了 TIME_WAIT 问题,发现 “bug” 没有了,只不过 “bug” 隐藏在了更深的地方。
- 原理
TIME_WAIT 发生在主动关闭连接的一方, 等待 2MSL 时间,结束 TIME_WAIT, 进入 CLOSED 状态。在一个连接没有进入 CLOSED 状态之前,这个连接是不能重用的。
被动关闭的一方,中间有个时段会进入 CLOSE_WAIT 状态,如果 CLOSE_WAIT 很多,可能程序写的有问题,没有合理关闭 socket,或者是服务器 CPU 处理不过来造成程序没法真正执行 close 操作。
- TIME_WAIT 用处
为了解决网络的丢包和网络不稳定所带来的其他问题,如果没有 TIME_WAIT, 就可能接收到上一个连接迟迟未到的连接,造成错误的接收。
确保被动关闭的一方能够在时间范围内,关闭自己的连接。之所以等待 2MSL 是因为这样可以确保自己上一个包已经被对方正确接收,并且对方没有重新请求。
如果关闭太快,然后立即被复用,重新连接到刚才断开的被动方。 这时,如果主动关闭方在 TIME_WAIT 之前发送的 ACK 包丢失的话,被动方应该会继续等待 ACK,直至超时,但是此时却可能受到 SYN 建立连接的握手包,被动方就会返回 RST,无法连接成功。
如果忽略 TIME_WAIT,就有可能造成数据错乱,或者短暂性连接失败。
查询了一下,大概几百几千的 TIME_WAIT 应该还是比较正常的。但是我这里发现的是 3万多个 TIME_WAIT,并且都是 MySQL 连接,这样肯定有问题。 第一是 TIME_WAIT 实在太多了,第二 MySQL 也不是能承受 3 万并发吧,说明这些连接是并不是并发创建的(基于服务还正常,偶尔服务非常慢)。
- net.ipv4.tcp_timestamps
RFC 1323 在 TCP 可靠性中,引入 timestamp 的 TCP option,两个4字节的时间戳字段,其中第一个4字节字段保存发送该数据包的时间,第二个4字节字段用来保存最近一次接收到对方发送数据的时间。有了这两个字段, 就可以利用 tcp_tw_reuse 和 tcp_tw_recycle 进行优化。
- net.ipv4.tcp_tw_reuse
复用 TIME_WAIT 状态的链接。
主动关闭连接后,如果此时正好有连接需要连接, 就可以复用这个还处于 TIME_WAIT 状态的连接。
tcp_tw_reuse 应用的场景是:某一方,需要不断的通过“短连接”连接其他服务器,并且总是自己先关闭连接(TIME_WAIT 在自己这方),关闭后又不断的重新连接对方。
连接复用后,延迟或者重发的数据包到达,新的链接通过前面提到的两个时间字段记性判断。复用链接后,这条连接的时间被更新为当前的时间,当旧的连接的数据包到达后,只需要比较数据包的时间是否大于刚才新连接的时间,内核就可以快速的判断出,数据包是丢弃还是接收了。
这个配置,依赖于连接双方,同时需要对 timestamps 的支持。同事,这个配置,仅仅影响作为客户端的连接。 连接服务端时服用 TIME_WAIT 的 socket。(主动连接)
- net.ipv4.tcp_tw_recycle
销毁掉 TIME_WAIT。
就是内核会快速回收掉处于 TIME_WAIT 的 socket 连接。这个回收时间,通过RTT动态计算出来的,不是 2MSL,而是一个 RTO(retransmission timeout,数据包重传时间) ,这个时间远远小于 2MSL 。
这个配置主要影响到了 inbound 的连接,即作为服务端校色,客户端连接进来,服务端主动关闭了连接, TIME_WAIT 状态的 socket 处于服务端,服务端快速的回收该状态的连接。(被动连接)
由此产生的问题,依旧利用 timestamps 解决。
总结: 作为客户端,主动进行一些短连接, 开启 net.ipv4.tcp_tw_reuse, 而作为服务器,主要提供被动连接,开启 net.ipv4.tcp_tw_recycle 。
- 具体业务
在比较老的业务下面,如早起的 Django 框架,是不支持连接池的,也就是连接数据库的都是短连接,是产生 TIME_WAIT 的主要原因。
还有就是 CRONTAB 定时任务,由于定时任务执行完了后,进程应该也就销毁了,所有变量都不存在了,每次执行都需要重新连接,每次执行完成都需要进行断开。
解决方法: 对于业务上的,由于依赖关系比较复杂,可以试着从底层加入连接池功能,或者升级 Django 版本(由于稳定性问题,一般是不会升级的),或者自己将 DBUtils 集成进去。
定时任务问题,可以使用专门的定时任务框架,如 APScheduler 框架。
个人认为,与其最开始直接两句话搞定,修改个参数,看似好像也解决了,但是也只是看似,看不到 TIME_WAIT 连接了,但是其实是因为这些连接消失的很快,才看不到而已,并没有从本质上解决问题,也没优化什么性能,优化的只是 Linux 服务器处理的性能。因为每次连接 MySQL 还是需要连接、断开。
所以还是需要在根源找问题,将短连接换成长连接,并且加入连接池来减少连接的创建销毁,来提高响应时间和性能。
再说一个问题:SQLAlchemy长时间未请求,数据库连接会断开,就会导致第一次查询时会报错的,然后第二次就不会报错了。
参考: