Linux很多TIME_WAIT问题解决
目录
0x00 问题
一个服务器运行用nginx的web服务,由于php需要频繁的访问数据库,而且使用的都是短链接,因此一段时间内产生并保持大量的TIME_WAIT。
$ netstat -an | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
LAST_ACK 11
LISTEN 11
SYN_RECV 42
ESTABLISHED 172
FIN_WAIT1 13
FIN_WAIT2 11
CLOSING 1
SYN_SENT 38
TIME_WAIT 3792
0x01 危害
Linux很多TIME_WAIT,相应的会占用大量的系统资源,同时还会占用大量的端口和连接数,导致新来的请求无法被服务器及时响应处理;默认情况下,linux临时端口号范围是(32768,61000),本机可用于调用的端口约3万个,进而导致调用后端服务阻塞,页面响应变慢;
0x02 解决办法
根据以上分析,需要对系统内核参数进行优化,启用TIME_WAIT连接重用,TIME_WAIT连接回收、缩短连接保持时间、增加可用端口数:
vi /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1 # 连接重用
net.ipv4.tcp_tw_recycle = 1 # 连接回收
net.ipv4.tcp_fin_timeout = 30 # 连接保持时间
net.ipv4.ip_local_port_range=1024 65000 # 增加端口数量
sysctl -p # 生效
通常情况下,设置前三条即可,减少了TIME_WAIT数量,端口占用也会缓解,但如果服务的并发量本来就很大,增加端口数量就很有必要;
优化生效后,TIME_WAIT明显减少:
$ netstat -an | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
LAST_ACK 17
LISTEN 11
SYN_RECV 25
ESTABLISHED 107
FIN_WAIT1 9
FIN_WAIT2 23
CLOSING 2
SYN_SENT 14
TIME_WAIT 217
0x03 注意
- 需要确认
net.ipv4.tcp_timestamps = 1
是打开的(默认是打开的),因为只有timestamps打开了,net.ipv4.tcp_tw_recycle = 1
才会生效; - 当
net.ipv4.tcp_timestamps
和net.ipv4.tcp_tw_recycle
同时打开,对通过NAT网关访问服务器的连接会有问题,因为tcp_tw_recycle/tcp_timestamps
都开启的条件下,60s内同一源ip主机的socket connect请求中的timestamp必须是递增的。而timestamp时间为系统启动到当前的时间,因此,不同客户端的timestamp不相同,timestamp 大的客户端可以正常访问,timestamp小的客户端则访问失败; 这种情况建议关闭tcp_tw_recycle
选项,而不是timestamp
;因为 在tcp timestamp关闭的条件下,开启tcp_tw_recycle是不起作用的;而tcp timestamp可以独立开启并起作用。
0x04 总结
- 使用过多的短连接,导致了大量的TIME_WAIT;在服务设计时,应尽量采用长连接,连接池,KEEPALIVE等技术减少对后端的连接次数,提高连接的效率。
- 这次的问题,因PHP本身的特性,一个页面处理完成后,所有相关连接就断了,不太好使用长连接,连接池,KEEPALIVE技术;
0x05 确有频繁访问时,以上设置并不能减少 TIME_WAIT
- 当服务器却又很多用户频繁访问,以上针对系统的优化并不能达到效果。
- 原因试试php页面访问mysql采用的是短连接,用完就断开,频繁访问必然产生很多TIME_WAIT;
- 解决办法, 采用长连接的方式访问数据库,这里采用php的PDO扩展访问mysql,长连接还能提高性能:
$db_host="localhost:3306"; $db_username="user"; $db_password=""; $dsn = "mysql:host=$db_host;dbname=$database;"; try { $sql = new PDO($dsn,$db_username,$db_password,array(PDO::ATTR_PERSISTENT => true));//PDO::ATTR_PERSISTENT => true 表示采用持久化连接; } catch(PDOException $e){ die($e->getMessage()); } try{ $result = $sql->query("select * from accounts where id = '".$sn."';"); } catch(PDOException $e){ die($e->getMessage()); } $rows = $result->fetchAll(PDO::FETCH_ASSOC);// 这里要注意,查询结果只能调用一次fetch,后面再调用就返回空了,所以这个地方第一次调用就要保存下来给后续使用; if(count($rows) == 1){ $row = $rows[0];// 多个结果时,foreach 遍历即可 ..... } 或者: foreach($dbh->query('SELECT * from FOO') as $row) { print_r($row); }
- php7.2 设置支持PDO扩展
vim /etc/php/7.2/cli/php.ini 这个配置文件是配置php命令行运行环境;
vim /etc/php/7.2/fpm/php.ini 这个配置 php-fpm,里面 extension=pdo_mysql 是注释掉的,但是 /etc/php/7.2/fpm/conf.d/20-pdo_mysql.ini 中有定义,所以应该是默认支持pdo的;
打开注释: extension=pdo_mysql 即可;
系统优化后,再使用PDO长连接访问mysql,TIME_WAIT连接数,直线下跌几乎没有;